Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ target
# Target ant folder
build
/tmp/
/.vscode/
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
<dependency>
<groupId>dev.vality</groupId>
<artifactId>damsel</artifactId>
<version>1.681-7a97267</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
Expand Down
20 changes: 19 additions & 1 deletion src/main/java/dev/vality/fraudbusters/config/CachingConfig.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
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;

@Configuration
@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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

мб здесь какой-нибудь warn или debug лог отписать, что пустота пришла?

}
try {
List<ShopContext> 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

мб и тут дебаг лог с ответом добавим?

} 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()));
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}

47 changes: 47 additions & 0 deletions src/main/java/dev/vality/fraudbusters/util/UserCacheKeyUtil.java
Original file line number Diff line number Diff line change
@@ -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;
}
}

9 changes: 6 additions & 3 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PaymentModel, CheckedResultModel> templateVisitor;
@MockBean
@MockitoBean
KafkaTemplate<String, FraudResult> kafkaFraudResultTemplate;
@MockBean
@MockitoBean
WbListServiceSrv.Iface wbListServiceSrv;

@Test
Expand All @@ -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<PaymentModel> 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");
}
}

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;
}
}
Loading