diff --git a/.gitignore b/.gitignore
index 4388c0f2..44e0668d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,3 +55,4 @@ target
# Target ant folder
build
/tmp/
+/.vscode/
diff --git a/pom.xml b/pom.xml
index ff03e523..8c5f199f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -109,6 +109,7 @@
dev.vality
damsel
+ 1.681-7a97267
io.opentelemetry
diff --git a/src/main/java/dev/vality/fraudbusters/config/CachingConfig.java b/src/main/java/dev/vality/fraudbusters/config/CachingConfig.java
index 1d8ddc87..c7458093 100644
--- a/src/main/java/dev/vality/fraudbusters/config/CachingConfig.java
+++ b/src/main/java/dev/vality/fraudbusters/config/CachingConfig.java
@@ -1,11 +1,13 @@
package dev.vality.fraudbusters.config;
import com.github.benmanes.caffeine.cache.Caffeine;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
import java.util.concurrent.TimeUnit;
@@ -13,13 +15,29 @@
@EnableCaching
public class CachingConfig {
+ @Value("${cache.expire-after-access-seconds:100}")
+ private int expireAfterAccessSeconds;
+ @Value("${cache.inspect-user-expire-after-access-seconds:300}")
+ private int inspectUserExpireAfterAccessSeconds;
+
@Bean
+ @Primary
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("resolveCountry", "isNewShop");
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(200)
.maximumSize(500)
- .expireAfterAccess(100, TimeUnit.SECONDS));
+ .expireAfterAccess(expireAfterAccessSeconds, TimeUnit.SECONDS));
+ return cacheManager;
+ }
+
+ @Bean(name = "inspectUserCacheManager")
+ public CacheManager inspectUserCacheManager() {
+ CaffeineCacheManager cacheManager = new CaffeineCacheManager("inspectUser");
+ cacheManager.setCaffeine(Caffeine.newBuilder()
+ .initialCapacity(100)
+ .maximumSize(1000)
+ .expireAfterAccess(inspectUserExpireAfterAccessSeconds, TimeUnit.SECONDS));
return cacheManager;
}
}
diff --git a/src/main/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandler.java b/src/main/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandler.java
index e66412b6..fca898fe 100644
--- a/src/main/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandler.java
+++ b/src/main/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandler.java
@@ -2,9 +2,7 @@
import dev.vality.damsel.base.InvalidRequest;
import dev.vality.damsel.domain.RiskScore;
-import dev.vality.damsel.proxy_inspector.BlackListContext;
-import dev.vality.damsel.proxy_inspector.Context;
-import dev.vality.damsel.proxy_inspector.InspectorProxySrv;
+import dev.vality.damsel.proxy_inspector.*;
import dev.vality.damsel.wb_list.*;
import dev.vality.fraudbusters.converter.CheckedResultToRiskScoreConverter;
import dev.vality.fraudbusters.converter.ContextToFraudRequestConverter;
@@ -13,10 +11,20 @@
import dev.vality.fraudbusters.domain.FraudResult;
import dev.vality.fraudbusters.fraud.model.PaymentModel;
import dev.vality.fraudbusters.stream.TemplateVisitor;
+import dev.vality.fraudbusters.util.PaymentModelFactory;
+import dev.vality.fraudbusters.util.UserCacheKeyUtil;
+import dev.vality.fraudo.constant.ResultStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.util.CollectionUtils;
+
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
@@ -64,4 +72,44 @@ public boolean isBlacklisted(BlackListContext blackListContext) throws InvalidRe
}
}
+ @Override
+ @Cacheable(
+ cacheManager = "inspectUserCacheManager",
+ cacheNames = "inspectUser",
+ key = "#root.target.buildInspectUserCacheKey(#context)"
+ )
+ public BlockedShops inspectUser(InspectUserContext context) throws InvalidRequest, TException {
+ if (CollectionUtils.isEmpty(context.getShopList())) {
+ log.warn("FraudInspectorHandler inspectUser with empty shopList: {}", context);
+ return new BlockedShops().setShopList(Collections.emptyList());
+ }
+ try {
+ List blockedShops = context.getShopList().stream()
+ .map(shopContext -> {
+ PaymentModel paymentModel = PaymentModelFactory.buildPaymentModel(context, shopContext);
+ CheckedResultModel result = templateVisitor.visit(paymentModel);
+ return new AbstractMap.SimpleEntry<>(shopContext, result);
+ })
+ .filter(entry -> isDeclineResult(entry.getValue()))
+ .map(AbstractMap.SimpleEntry::getKey)
+ .collect(Collectors.toList());
+ log.debug("FraudInspectorHandler inspectUser result blockedShops: {}", blockedShops);
+ return new BlockedShops().setShopList(blockedShops);
+ } catch (Exception e) {
+ log.warn("FraudInspectorHandler error when inspectUser e: ", e);
+ return new BlockedShops().setShopList(Collections.emptyList());
+ }
+ }
+
+ public String buildInspectUserCacheKey(InspectUserContext context) {
+ return UserCacheKeyUtil.buildInspectUserCacheKey(context);
+ }
+
+ private static boolean isDeclineResult(CheckedResultModel result) {
+ return result != null
+ && result.getResultModel() != null
+ && (ResultStatus.DECLINE.equals(result.getResultModel().getResultStatus())
+ || ResultStatus.DECLINE_AND_NOTIFY.equals(result.getResultModel().getResultStatus()));
+ }
+
}
diff --git a/src/main/java/dev/vality/fraudbusters/util/PaymentModelFactory.java b/src/main/java/dev/vality/fraudbusters/util/PaymentModelFactory.java
new file mode 100644
index 00000000..87c5aafe
--- /dev/null
+++ b/src/main/java/dev/vality/fraudbusters/util/PaymentModelFactory.java
@@ -0,0 +1,34 @@
+package dev.vality.fraudbusters.util;
+
+import dev.vality.damsel.proxy_inspector.InspectUserContext;
+import dev.vality.damsel.proxy_inspector.ShopContext;
+import dev.vality.fraudbusters.constant.ClickhouseUtilsValue;
+import dev.vality.fraudbusters.fraud.model.PaymentModel;
+import org.springframework.util.StringUtils;
+
+public class PaymentModelFactory {
+
+ public static PaymentModel buildPaymentModel(InspectUserContext context, ShopContext shopContext) {
+ PaymentModel paymentModel = new PaymentModel();
+ paymentModel.setPartyId(shopContext.getParty().getPartyRef().getId());
+ paymentModel.setShopId(shopContext.getShop().getShopRef().getId());
+ paymentModel.setTimestamp(System.currentTimeMillis());
+ if (context.getUserInfo() != null) {
+ paymentModel.setEmail(
+ context.getUserInfo().isSetEmail() && StringUtils.hasLength(context.getUserInfo().getEmail())
+ ? context.getUserInfo().getEmail().toLowerCase()
+ : ClickhouseUtilsValue.UNKNOWN);
+ paymentModel.setPhone(
+ context.getUserInfo().isSetPhoneNumber()
+ && StringUtils.hasLength(context.getUserInfo().getPhoneNumber())
+ ? context.getUserInfo().getPhoneNumber()
+ : ClickhouseUtilsValue.UNKNOWN
+ );
+ } else {
+ paymentModel.setEmail(ClickhouseUtilsValue.UNKNOWN);
+ paymentModel.setPhone(ClickhouseUtilsValue.UNKNOWN);
+ }
+ return paymentModel;
+ }
+}
+
diff --git a/src/main/java/dev/vality/fraudbusters/util/UserCacheKeyUtil.java b/src/main/java/dev/vality/fraudbusters/util/UserCacheKeyUtil.java
new file mode 100644
index 00000000..6a540529
--- /dev/null
+++ b/src/main/java/dev/vality/fraudbusters/util/UserCacheKeyUtil.java
@@ -0,0 +1,47 @@
+package dev.vality.fraudbusters.util;
+
+import dev.vality.damsel.proxy_inspector.InspectUserContext;
+import dev.vality.damsel.proxy_inspector.ShopContext;
+import dev.vality.fraudbusters.constant.ClickhouseUtilsValue;
+import org.springframework.util.StringUtils;
+
+import java.util.Comparator;
+import java.util.stream.Collectors;
+
+public class UserCacheKeyUtil {
+
+ public static String buildInspectUserCacheKey(InspectUserContext context) {
+ if (context == null) {
+ return "null";
+ }
+ String email = ClickhouseUtilsValue.UNKNOWN;
+ String phone = ClickhouseUtilsValue.UNKNOWN;
+ if (context.getUserInfo() != null) {
+ email = context.getUserInfo().isSetEmail() && StringUtils.hasLength(context.getUserInfo().getEmail())
+ ? context.getUserInfo().getEmail().toLowerCase()
+ : ClickhouseUtilsValue.UNKNOWN;
+ phone = context.getUserInfo().isSetPhoneNumber()
+ && StringUtils.hasLength(context.getUserInfo().getPhoneNumber())
+ ? context.getUserInfo().getPhoneNumber()
+ : ClickhouseUtilsValue.UNKNOWN;
+ }
+ if (context.getShopList() == null || context.getShopList().isEmpty()) {
+ return email + "|" + phone + "|";
+ }
+ String shopsKey = context.getShopList().stream()
+ .map(UserCacheKeyUtil::buildShopKey)
+ .sorted(Comparator.naturalOrder())
+ .collect(Collectors.joining(","));
+ return email + "|" + phone + "|" + shopsKey;
+ }
+
+ private static String buildShopKey(ShopContext shopContext) {
+ if (shopContext == null || shopContext.getParty() == null || shopContext.getShop() == null) {
+ return ClickhouseUtilsValue.UNKNOWN;
+ }
+ String partyId = shopContext.getParty().getPartyRef().getId();
+ String shopId = shopContext.getShop().getShopRef().getId();
+ return partyId + ":" + shopId;
+ }
+}
+
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 6a26aa12..7310363b 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -35,8 +35,8 @@ spring:
reconnect.backoff.max.ms: 3000
retry.backoff.ms: 1000
ssl:
- keystore-location: src/main/resources/cert/kenny-k.struzhkin.p12
- keystore-password: kenny
+ key-store-location: src/main/resources/cert/kenny-k.struzhkin.p12
+ key-store-password: kenny
key-password: kenny
trust-store-password: kenny12
trust-store-location: src/main/resources/cert/truststore.p12
@@ -46,9 +46,12 @@ spring:
ansi:
enabled: always
cache:
- cache-names: resolveCountry
+ cache-names: resolveCountry,isNewShop,inspectUser
caffeine:
spec: maximumSize=500,expireAfterAccess=100s
+cache:
+ expire-after-access-seconds: 100
+ inspect-user-expire-after-access-seconds: 300
kafka:
historical.listener:
diff --git a/src/test/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandlerTest.java b/src/test/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandlerTest.java
index fb45bb04..354be067 100644
--- a/src/test/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandlerTest.java
+++ b/src/test/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandlerTest.java
@@ -1,38 +1,63 @@
package dev.vality.fraudbusters.resource.payment.handler;
+import dev.vality.damsel.domain.Category;
+import dev.vality.damsel.domain.ContactInfo;
+import dev.vality.damsel.domain.PartyConfigRef;
+import dev.vality.damsel.domain.ShopConfigRef;
import dev.vality.damsel.proxy_inspector.BlackListContext;
+import dev.vality.damsel.proxy_inspector.BlockedShops;
+import dev.vality.damsel.proxy_inspector.InspectUserContext;
+import dev.vality.damsel.proxy_inspector.InspectorProxySrv;
+import dev.vality.damsel.proxy_inspector.Party;
+import dev.vality.damsel.proxy_inspector.Shop;
+import dev.vality.damsel.proxy_inspector.ShopContext;
+import dev.vality.damsel.domain.ShopLocation;
import dev.vality.damsel.wb_list.ListNotFound;
import dev.vality.damsel.wb_list.WbListServiceSrv;
import dev.vality.fraudbusters.converter.CheckedResultToRiskScoreConverter;
import dev.vality.fraudbusters.converter.ContextToFraudRequestConverter;
import dev.vality.fraudbusters.domain.CheckedResultModel;
+import dev.vality.fraudbusters.domain.ConcreteResultModel;
import dev.vality.fraudbusters.domain.FraudResult;
import dev.vality.fraudbusters.fraud.model.PaymentModel;
import dev.vality.fraudbusters.stream.TemplateVisitor;
+import dev.vality.fraudo.constant.ResultStatus;
import org.apache.thrift.TException;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.boot.test.mock.mockito.MockBean;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
+import java.util.ArrayList;
+import java.util.List;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, SpringExtension.class})
class FraudInspectorHandlerTest {
- @MockBean
+ @MockitoBean
CheckedResultToRiskScoreConverter checkedResultToRiskScoreConverter;
- @MockBean
+ @MockitoBean
ContextToFraudRequestConverter requestConverter;
- @MockBean
+ @MockitoBean
TemplateVisitor templateVisitor;
- @MockBean
+ @MockitoBean
KafkaTemplate kafkaFraudResultTemplate;
- @MockBean
+ @MockitoBean
WbListServiceSrv.Iface wbListServiceSrv;
@Test
@@ -59,11 +84,75 @@ void isExistInBlackList() throws TException {
assertEquals(false, existInBlackList);
}
- private static BlackListContext createBlackListContext() {
+ @Test
+ void inspectUserShopsBlocked() throws TException {
+ FraudInspectorHandler fraudInspectorHandler = new FraudInspectorHandler(
+ "test",
+ checkedResultToRiskScoreConverter,
+ requestConverter,
+ templateVisitor,
+ kafkaFraudResultTemplate,
+ wbListServiceSrv
+ );
+
+ when(templateVisitor.visit(any())).thenAnswer(invocation -> {
+ PaymentModel model = invocation.getArgument(0);
+ if (model != null && "shop_1".equals(model.getShopId())) {
+ return createCheckedResult(ResultStatus.DECLINE);
+ }
+ return createCheckedResult(ResultStatus.THREE_DS);
+ });
+
+ BlockedShops blockedShops = fraudInspectorHandler.inspectUser(createInspectUserContext());
+
+ assertEquals(1, blockedShops.getShopListSize());
+ assertEquals("shop_1", blockedShops.getShopList().get(0).getShop().getShopRef().getId());
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PaymentModel.class);
+ verify(templateVisitor, times(2)).visit(captor.capture());
+ for (PaymentModel model : captor.getAllValues()) {
+ assertEquals("party_1", model.getPartyId());
+ assertEquals("user@email.com", model.getEmail());
+ assertEquals("79990001122", model.getPhone());
+ }
+ }
+
+ private BlackListContext createBlackListContext() {
return new BlackListContext()
.setValue("test")
.setFieldName("field_test")
.setFirstId("test_id")
.setSecondId("test_sec_id");
}
-}
\ No newline at end of file
+
+ private InspectUserContext createInspectUserContext() {
+ ContactInfo contactInfo = new ContactInfo();
+ contactInfo.setEmail("User@Email.Com");
+ contactInfo.setPhoneNumber("79990001122");
+ return new InspectUserContext()
+ .setUserInfo(contactInfo)
+ .setShopList(List.of(
+ createShopContext("party_1", "shop_1"),
+ createShopContext("party_1", "shop_2")
+ ));
+ }
+
+ private ShopContext createShopContext(String partyId, String shopId) {
+ ShopLocation location = new ShopLocation();
+ location.setUrl("http://example.com");
+ return new ShopContext()
+ .setParty(new Party(new PartyConfigRef(partyId)))
+ .setShop(new Shop(
+ new ShopConfigRef(shopId),
+ new Category("category", "category"),
+ "shop-name",
+ location
+ ));
+ }
+
+ private CheckedResultModel createCheckedResult(ResultStatus status) {
+ CheckedResultModel checkedResultModel = new CheckedResultModel();
+ checkedResultModel.setResultModel(new ConcreteResultModel(status, null, null));
+ return checkedResultModel;
+ }
+}