Skip to content

Commit 2a66bfc

Browse files
authored
Add inspect user blocked shop (#84)
1 parent 4b7c4d1 commit 2a66bfc

8 files changed

Lines changed: 256 additions & 15 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ target
5555
# Target ant folder
5656
build
5757
/tmp/
58+
/.vscode/

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
<dependency>
110110
<groupId>dev.vality</groupId>
111111
<artifactId>damsel</artifactId>
112+
<version>1.681-7a97267</version>
112113
</dependency>
113114
<dependency>
114115
<groupId>io.opentelemetry</groupId>
Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,43 @@
11
package dev.vality.fraudbusters.config;
22

33
import com.github.benmanes.caffeine.cache.Caffeine;
4+
import org.springframework.beans.factory.annotation.Value;
45
import org.springframework.cache.CacheManager;
56
import org.springframework.cache.annotation.EnableCaching;
67
import org.springframework.cache.caffeine.CaffeineCacheManager;
78
import org.springframework.context.annotation.Bean;
89
import org.springframework.context.annotation.Configuration;
10+
import org.springframework.context.annotation.Primary;
911

1012
import java.util.concurrent.TimeUnit;
1113

1214
@Configuration
1315
@EnableCaching
1416
public class CachingConfig {
1517

18+
@Value("${cache.expire-after-access-seconds:100}")
19+
private int expireAfterAccessSeconds;
20+
@Value("${cache.inspect-user-expire-after-access-seconds:300}")
21+
private int inspectUserExpireAfterAccessSeconds;
22+
1623
@Bean
24+
@Primary
1725
public CacheManager cacheManager() {
1826
CaffeineCacheManager cacheManager = new CaffeineCacheManager("resolveCountry", "isNewShop");
1927
cacheManager.setCaffeine(Caffeine.newBuilder()
2028
.initialCapacity(200)
2129
.maximumSize(500)
22-
.expireAfterAccess(100, TimeUnit.SECONDS));
30+
.expireAfterAccess(expireAfterAccessSeconds, TimeUnit.SECONDS));
31+
return cacheManager;
32+
}
33+
34+
@Bean(name = "inspectUserCacheManager")
35+
public CacheManager inspectUserCacheManager() {
36+
CaffeineCacheManager cacheManager = new CaffeineCacheManager("inspectUser");
37+
cacheManager.setCaffeine(Caffeine.newBuilder()
38+
.initialCapacity(100)
39+
.maximumSize(1000)
40+
.expireAfterAccess(inspectUserExpireAfterAccessSeconds, TimeUnit.SECONDS));
2341
return cacheManager;
2442
}
2543
}

src/main/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandler.java

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
import dev.vality.damsel.base.InvalidRequest;
44
import dev.vality.damsel.domain.RiskScore;
5-
import dev.vality.damsel.proxy_inspector.BlackListContext;
6-
import dev.vality.damsel.proxy_inspector.Context;
7-
import dev.vality.damsel.proxy_inspector.InspectorProxySrv;
5+
import dev.vality.damsel.proxy_inspector.*;
86
import dev.vality.damsel.wb_list.*;
97
import dev.vality.fraudbusters.converter.CheckedResultToRiskScoreConverter;
108
import dev.vality.fraudbusters.converter.ContextToFraudRequestConverter;
@@ -13,10 +11,20 @@
1311
import dev.vality.fraudbusters.domain.FraudResult;
1412
import dev.vality.fraudbusters.fraud.model.PaymentModel;
1513
import dev.vality.fraudbusters.stream.TemplateVisitor;
14+
import dev.vality.fraudbusters.util.PaymentModelFactory;
15+
import dev.vality.fraudbusters.util.UserCacheKeyUtil;
16+
import dev.vality.fraudo.constant.ResultStatus;
1617
import lombok.RequiredArgsConstructor;
1718
import lombok.extern.slf4j.Slf4j;
1819
import org.apache.thrift.TException;
20+
import org.springframework.cache.annotation.Cacheable;
1921
import org.springframework.kafka.core.KafkaTemplate;
22+
import org.springframework.util.CollectionUtils;
23+
24+
import java.util.AbstractMap;
25+
import java.util.Collections;
26+
import java.util.List;
27+
import java.util.stream.Collectors;
2028

2129
@Slf4j
2230
@RequiredArgsConstructor
@@ -64,4 +72,44 @@ public boolean isBlacklisted(BlackListContext blackListContext) throws InvalidRe
6472
}
6573
}
6674

75+
@Override
76+
@Cacheable(
77+
cacheManager = "inspectUserCacheManager",
78+
cacheNames = "inspectUser",
79+
key = "#root.target.buildInspectUserCacheKey(#context)"
80+
)
81+
public BlockedShops inspectUser(InspectUserContext context) throws InvalidRequest, TException {
82+
if (CollectionUtils.isEmpty(context.getShopList())) {
83+
log.warn("FraudInspectorHandler inspectUser with empty shopList: {}", context);
84+
return new BlockedShops().setShopList(Collections.emptyList());
85+
}
86+
try {
87+
List<ShopContext> blockedShops = context.getShopList().stream()
88+
.map(shopContext -> {
89+
PaymentModel paymentModel = PaymentModelFactory.buildPaymentModel(context, shopContext);
90+
CheckedResultModel result = templateVisitor.visit(paymentModel);
91+
return new AbstractMap.SimpleEntry<>(shopContext, result);
92+
})
93+
.filter(entry -> isDeclineResult(entry.getValue()))
94+
.map(AbstractMap.SimpleEntry::getKey)
95+
.collect(Collectors.toList());
96+
log.debug("FraudInspectorHandler inspectUser result blockedShops: {}", blockedShops);
97+
return new BlockedShops().setShopList(blockedShops);
98+
} catch (Exception e) {
99+
log.warn("FraudInspectorHandler error when inspectUser e: ", e);
100+
return new BlockedShops().setShopList(Collections.emptyList());
101+
}
102+
}
103+
104+
public String buildInspectUserCacheKey(InspectUserContext context) {
105+
return UserCacheKeyUtil.buildInspectUserCacheKey(context);
106+
}
107+
108+
private static boolean isDeclineResult(CheckedResultModel result) {
109+
return result != null
110+
&& result.getResultModel() != null
111+
&& (ResultStatus.DECLINE.equals(result.getResultModel().getResultStatus())
112+
|| ResultStatus.DECLINE_AND_NOTIFY.equals(result.getResultModel().getResultStatus()));
113+
}
114+
67115
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package dev.vality.fraudbusters.util;
2+
3+
import dev.vality.damsel.proxy_inspector.InspectUserContext;
4+
import dev.vality.damsel.proxy_inspector.ShopContext;
5+
import dev.vality.fraudbusters.constant.ClickhouseUtilsValue;
6+
import dev.vality.fraudbusters.fraud.model.PaymentModel;
7+
import org.springframework.util.StringUtils;
8+
9+
public class PaymentModelFactory {
10+
11+
public static PaymentModel buildPaymentModel(InspectUserContext context, ShopContext shopContext) {
12+
PaymentModel paymentModel = new PaymentModel();
13+
paymentModel.setPartyId(shopContext.getParty().getPartyRef().getId());
14+
paymentModel.setShopId(shopContext.getShop().getShopRef().getId());
15+
paymentModel.setTimestamp(System.currentTimeMillis());
16+
if (context.getUserInfo() != null) {
17+
paymentModel.setEmail(
18+
context.getUserInfo().isSetEmail() && StringUtils.hasLength(context.getUserInfo().getEmail())
19+
? context.getUserInfo().getEmail().toLowerCase()
20+
: ClickhouseUtilsValue.UNKNOWN);
21+
paymentModel.setPhone(
22+
context.getUserInfo().isSetPhoneNumber()
23+
&& StringUtils.hasLength(context.getUserInfo().getPhoneNumber())
24+
? context.getUserInfo().getPhoneNumber()
25+
: ClickhouseUtilsValue.UNKNOWN
26+
);
27+
} else {
28+
paymentModel.setEmail(ClickhouseUtilsValue.UNKNOWN);
29+
paymentModel.setPhone(ClickhouseUtilsValue.UNKNOWN);
30+
}
31+
return paymentModel;
32+
}
33+
}
34+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package dev.vality.fraudbusters.util;
2+
3+
import dev.vality.damsel.proxy_inspector.InspectUserContext;
4+
import dev.vality.damsel.proxy_inspector.ShopContext;
5+
import dev.vality.fraudbusters.constant.ClickhouseUtilsValue;
6+
import org.springframework.util.StringUtils;
7+
8+
import java.util.Comparator;
9+
import java.util.stream.Collectors;
10+
11+
public class UserCacheKeyUtil {
12+
13+
public static String buildInspectUserCacheKey(InspectUserContext context) {
14+
if (context == null) {
15+
return "null";
16+
}
17+
String email = ClickhouseUtilsValue.UNKNOWN;
18+
String phone = ClickhouseUtilsValue.UNKNOWN;
19+
if (context.getUserInfo() != null) {
20+
email = context.getUserInfo().isSetEmail() && StringUtils.hasLength(context.getUserInfo().getEmail())
21+
? context.getUserInfo().getEmail().toLowerCase()
22+
: ClickhouseUtilsValue.UNKNOWN;
23+
phone = context.getUserInfo().isSetPhoneNumber()
24+
&& StringUtils.hasLength(context.getUserInfo().getPhoneNumber())
25+
? context.getUserInfo().getPhoneNumber()
26+
: ClickhouseUtilsValue.UNKNOWN;
27+
}
28+
if (context.getShopList() == null || context.getShopList().isEmpty()) {
29+
return email + "|" + phone + "|";
30+
}
31+
String shopsKey = context.getShopList().stream()
32+
.map(UserCacheKeyUtil::buildShopKey)
33+
.sorted(Comparator.naturalOrder())
34+
.collect(Collectors.joining(","));
35+
return email + "|" + phone + "|" + shopsKey;
36+
}
37+
38+
private static String buildShopKey(ShopContext shopContext) {
39+
if (shopContext == null || shopContext.getParty() == null || shopContext.getShop() == null) {
40+
return ClickhouseUtilsValue.UNKNOWN;
41+
}
42+
String partyId = shopContext.getParty().getPartyRef().getId();
43+
String shopId = shopContext.getShop().getShopRef().getId();
44+
return partyId + ":" + shopId;
45+
}
46+
}
47+

src/main/resources/application.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ spring:
3535
reconnect.backoff.max.ms: 3000
3636
retry.backoff.ms: 1000
3737
ssl:
38-
keystore-location: src/main/resources/cert/kenny-k.struzhkin.p12
39-
keystore-password: kenny
38+
key-store-location: src/main/resources/cert/kenny-k.struzhkin.p12
39+
key-store-password: kenny
4040
key-password: kenny
4141
trust-store-password: kenny12
4242
trust-store-location: src/main/resources/cert/truststore.p12
@@ -46,9 +46,12 @@ spring:
4646
ansi:
4747
enabled: always
4848
cache:
49-
cache-names: resolveCountry
49+
cache-names: resolveCountry,isNewShop,inspectUser
5050
caffeine:
5151
spec: maximumSize=500,expireAfterAccess=100s
52+
cache:
53+
expire-after-access-seconds: 100
54+
inspect-user-expire-after-access-seconds: 300
5255

5356
kafka:
5457
historical.listener:

src/test/java/dev/vality/fraudbusters/resource/payment/handler/FraudInspectorHandlerTest.java

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,63 @@
11
package dev.vality.fraudbusters.resource.payment.handler;
22

3+
import dev.vality.damsel.domain.Category;
4+
import dev.vality.damsel.domain.ContactInfo;
5+
import dev.vality.damsel.domain.PartyConfigRef;
6+
import dev.vality.damsel.domain.ShopConfigRef;
37
import dev.vality.damsel.proxy_inspector.BlackListContext;
8+
import dev.vality.damsel.proxy_inspector.BlockedShops;
9+
import dev.vality.damsel.proxy_inspector.InspectUserContext;
10+
import dev.vality.damsel.proxy_inspector.InspectorProxySrv;
11+
import dev.vality.damsel.proxy_inspector.Party;
12+
import dev.vality.damsel.proxy_inspector.Shop;
13+
import dev.vality.damsel.proxy_inspector.ShopContext;
14+
import dev.vality.damsel.domain.ShopLocation;
415
import dev.vality.damsel.wb_list.ListNotFound;
516
import dev.vality.damsel.wb_list.WbListServiceSrv;
617
import dev.vality.fraudbusters.converter.CheckedResultToRiskScoreConverter;
718
import dev.vality.fraudbusters.converter.ContextToFraudRequestConverter;
819
import dev.vality.fraudbusters.domain.CheckedResultModel;
20+
import dev.vality.fraudbusters.domain.ConcreteResultModel;
921
import dev.vality.fraudbusters.domain.FraudResult;
1022
import dev.vality.fraudbusters.fraud.model.PaymentModel;
1123
import dev.vality.fraudbusters.stream.TemplateVisitor;
24+
import dev.vality.fraudo.constant.ResultStatus;
1225
import org.apache.thrift.TException;
26+
import org.junit.jupiter.api.BeforeEach;
1327
import org.junit.jupiter.api.Test;
1428
import org.junit.jupiter.api.extension.ExtendWith;
29+
import org.mockito.ArgumentCaptor;
1530
import org.mockito.junit.jupiter.MockitoExtension;
16-
import org.springframework.boot.test.mock.mockito.MockBean;
31+
import org.mockito.Mockito;
32+
import org.springframework.beans.factory.annotation.Autowired;
33+
import org.springframework.boot.test.context.TestConfiguration;
34+
import org.springframework.context.annotation.Bean;
1735
import org.springframework.kafka.core.KafkaTemplate;
36+
import org.springframework.test.context.ContextConfiguration;
37+
import org.springframework.test.context.bean.override.mockito.MockitoBean;
1838
import org.springframework.test.context.junit.jupiter.SpringExtension;
1939

40+
import java.util.ArrayList;
41+
import java.util.List;
42+
2043
import static org.junit.jupiter.api.Assertions.assertEquals;
2144
import static org.mockito.ArgumentMatchers.any;
45+
import static org.mockito.Mockito.times;
46+
import static org.mockito.Mockito.verify;
2247
import static org.mockito.Mockito.when;
2348

2449
@ExtendWith({MockitoExtension.class, SpringExtension.class})
2550
class FraudInspectorHandlerTest {
2651

27-
@MockBean
52+
@MockitoBean
2853
CheckedResultToRiskScoreConverter checkedResultToRiskScoreConverter;
29-
@MockBean
54+
@MockitoBean
3055
ContextToFraudRequestConverter requestConverter;
31-
@MockBean
56+
@MockitoBean
3257
TemplateVisitor<PaymentModel, CheckedResultModel> templateVisitor;
33-
@MockBean
58+
@MockitoBean
3459
KafkaTemplate<String, FraudResult> kafkaFraudResultTemplate;
35-
@MockBean
60+
@MockitoBean
3661
WbListServiceSrv.Iface wbListServiceSrv;
3762

3863
@Test
@@ -59,11 +84,75 @@ void isExistInBlackList() throws TException {
5984
assertEquals(false, existInBlackList);
6085
}
6186

62-
private static BlackListContext createBlackListContext() {
87+
@Test
88+
void inspectUserShopsBlocked() throws TException {
89+
FraudInspectorHandler fraudInspectorHandler = new FraudInspectorHandler(
90+
"test",
91+
checkedResultToRiskScoreConverter,
92+
requestConverter,
93+
templateVisitor,
94+
kafkaFraudResultTemplate,
95+
wbListServiceSrv
96+
);
97+
98+
when(templateVisitor.visit(any())).thenAnswer(invocation -> {
99+
PaymentModel model = invocation.getArgument(0);
100+
if (model != null && "shop_1".equals(model.getShopId())) {
101+
return createCheckedResult(ResultStatus.DECLINE);
102+
}
103+
return createCheckedResult(ResultStatus.THREE_DS);
104+
});
105+
106+
BlockedShops blockedShops = fraudInspectorHandler.inspectUser(createInspectUserContext());
107+
108+
assertEquals(1, blockedShops.getShopListSize());
109+
assertEquals("shop_1", blockedShops.getShopList().get(0).getShop().getShopRef().getId());
110+
111+
ArgumentCaptor<PaymentModel> captor = ArgumentCaptor.forClass(PaymentModel.class);
112+
verify(templateVisitor, times(2)).visit(captor.capture());
113+
for (PaymentModel model : captor.getAllValues()) {
114+
assertEquals("party_1", model.getPartyId());
115+
assertEquals("user@email.com", model.getEmail());
116+
assertEquals("79990001122", model.getPhone());
117+
}
118+
}
119+
120+
private BlackListContext createBlackListContext() {
63121
return new BlackListContext()
64122
.setValue("test")
65123
.setFieldName("field_test")
66124
.setFirstId("test_id")
67125
.setSecondId("test_sec_id");
68126
}
69-
}
127+
128+
private InspectUserContext createInspectUserContext() {
129+
ContactInfo contactInfo = new ContactInfo();
130+
contactInfo.setEmail("User@Email.Com");
131+
contactInfo.setPhoneNumber("79990001122");
132+
return new InspectUserContext()
133+
.setUserInfo(contactInfo)
134+
.setShopList(List.of(
135+
createShopContext("party_1", "shop_1"),
136+
createShopContext("party_1", "shop_2")
137+
));
138+
}
139+
140+
private ShopContext createShopContext(String partyId, String shopId) {
141+
ShopLocation location = new ShopLocation();
142+
location.setUrl("http://example.com");
143+
return new ShopContext()
144+
.setParty(new Party(new PartyConfigRef(partyId)))
145+
.setShop(new Shop(
146+
new ShopConfigRef(shopId),
147+
new Category("category", "category"),
148+
"shop-name",
149+
location
150+
));
151+
}
152+
153+
private CheckedResultModel createCheckedResult(ResultStatus status) {
154+
CheckedResultModel checkedResultModel = new CheckedResultModel();
155+
checkedResultModel.setResultModel(new ConcreteResultModel(status, null, null));
156+
return checkedResultModel;
157+
}
158+
}

0 commit comments

Comments
 (0)