Add flow for work with qr code (#69)

This commit is contained in:
struga 2023-08-03 15:23:52 +03:00 committed by GitHub
parent 23c1665f0e
commit 94418ab4ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 624 additions and 118 deletions

View File

@ -12,7 +12,7 @@
</parent>
<artifactId>adapter-flow-lib</artifactId>
<version>0.1.25</version>
<version>0.2.1</version>
<packaging>jar</packaging>
<name>adapter-flow-lib</name>

View File

@ -4,14 +4,17 @@ import dev.vality.adapter.common.cds.CdsStorageClient;
import dev.vality.adapter.common.cds.model.CardDataProxyModel;
import dev.vality.adapter.common.damsel.ProxyProviderPackageCreators;
import dev.vality.adapter.common.damsel.ProxyProviderPackageExtractors;
import dev.vality.adapter.flow.lib.constant.*;
import dev.vality.adapter.flow.lib.constant.MetaData;
import dev.vality.adapter.flow.lib.constant.OptionFields;
import dev.vality.adapter.flow.lib.constant.Step;
import dev.vality.adapter.flow.lib.constant.TargetStatus;
import dev.vality.adapter.flow.lib.model.*;
import dev.vality.adapter.flow.lib.serde.TemporaryContextDeserializer;
import dev.vality.adapter.flow.lib.service.CallbackUrlExtractor;
import dev.vality.adapter.flow.lib.service.CardDataService;
import dev.vality.adapter.flow.lib.service.IdGenerator;
import dev.vality.adapter.flow.lib.service.TemporaryContextService;
import dev.vality.adapter.flow.lib.utils.AdapterProperties;
import dev.vality.adapter.flow.lib.service.CallbackUrlExtractor;
import dev.vality.adapter.flow.lib.utils.CardDataUtils;
import dev.vality.adapter.flow.lib.utils.TargetStatusResolver;
import dev.vality.cds.storage.Auth3DS;
@ -55,7 +58,8 @@ public class CtxToEntryModelConverter implements Converter<PaymentContext, Entry
MobilePaymentData mobilePaymentData = null;
if (paymentResource.isSetDisposablePaymentResource()
&& currentStep == null
&& targetStatus == TargetStatus.PROCESSED) {
&& targetStatus == TargetStatus.PROCESSED
&& paymentResource.getDisposablePaymentResource().getPaymentTool().isSetBankCard()) {
SessionData sessionData = cdsStorageClient.getSessionData(context);
cardData = initCardData(context, paymentResource, sessionData);
mobilePaymentData = initMobilePaymentData(sessionData);
@ -119,7 +123,7 @@ public class CtxToEntryModelConverter implements Converter<PaymentContext, Entry
return StringUtils.hasText(redirectUrl)
? redirectUrl
: adapterConfigurations.getOrDefault(
OptionFields.FAILED_URL.name(), adapterProperties.getFailedRedirectUrl());
OptionFields.FAILED_URL.name(), adapterProperties.getFailedRedirectUrl());
}
private dev.vality.adapter.flow.lib.model.CardData initCardData(PaymentContext context,
@ -149,7 +153,7 @@ public class CtxToEntryModelConverter implements Converter<PaymentContext, Entry
}
private boolean isMobilePay(SessionData sessionData) {
return sessionData.isSetAuthData() && sessionData.getAuthData().isSetAuth3ds();
return sessionData != null && sessionData.isSetAuthData() && sessionData.getAuthData().isSetAuth3ds();
}
private RefundData initRefundData(PaymentInfo paymentInfo) {

View File

@ -0,0 +1,17 @@
package dev.vality.adapter.flow.lib.flow.simple;
import dev.vality.adapter.flow.lib.flow.RecurrentResultIntentResolver;
import dev.vality.adapter.flow.lib.model.ExitStateModel;
import dev.vality.adapter.flow.lib.service.factory.SimpleRecurrentIntentResultFactory;
import dev.vality.damsel.proxy_provider.RecurrentTokenIntent;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class UnsupportedGenerateTokenResultIntentResolver implements RecurrentResultIntentResolver {
@Override
public RecurrentTokenIntent initIntentByStep(ExitStateModel exitStateModel) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,20 @@
package dev.vality.adapter.flow.lib.flow.simple;
import dev.vality.adapter.flow.lib.constant.Step;
import dev.vality.adapter.flow.lib.flow.AbstractGenerateTokenStepResolver;
import dev.vality.adapter.flow.lib.model.EntryStateModel;
import dev.vality.adapter.flow.lib.model.ExitStateModel;
public class UnsupportedGenerateTokenStepResolverImpl extends
AbstractGenerateTokenStepResolver<EntryStateModel, ExitStateModel> {
@Override
public Step resolveCurrentStep(EntryStateModel stateModel) {
throw new UnsupportedOperationException();
}
@Override
public Step resolveNextStep(ExitStateModel exitStateModel) {
throw new UnsupportedOperationException();
}
}

View File

@ -1,43 +1,11 @@
package dev.vality.adapter.flow.lib.handler;
import dev.vality.adapter.flow.lib.exception.UnknownHandlerForStepException;
import dev.vality.adapter.flow.lib.flow.StepResolver;
import dev.vality.adapter.flow.lib.model.EntryStateModel;
import dev.vality.adapter.flow.lib.model.ExitStateModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.core.convert.converter.Converter;
import java.util.List;
public interface ServerFlowHandler<T, R> {
@Slf4j
@RequiredArgsConstructor
public class ServerFlowHandler<T, R> {
private final List<CommonHandler<ExitStateModel, EntryStateModel>> handlers;
private final StepResolver<EntryStateModel, ExitStateModel> stepResolver;
private final Converter<T, EntryStateModel> entryConverter;
private final Converter<ExitStateModel, R> exitConverter;
public R handle(T context) throws TException {
var entryStateModel = prepareEntryState(entryConverter, context);
log.info("EntryStateModel: {}", entryStateModel);
var exitStateModel = handlers.stream()
.filter(handler -> handler.isHandle(entryStateModel))
.findFirst()
.orElseThrow(() -> new UnknownHandlerForStepException("Can't find handler to data: " + entryStateModel))
.handle(entryStateModel);
log.info("ExitStateModel: {}", exitStateModel);
exitStateModel.setEntryStateModel(entryStateModel);
exitStateModel.setNextStep(stepResolver.resolveNextStep(exitStateModel));
log.info("Step changing: {} -> {}", entryStateModel.getCurrentStep(), exitStateModel.getNextStep());
return exitConverter.convert(exitStateModel);
default R handle(T context) throws TException {
throw new UnsupportedOperationException();
}
private EntryStateModel prepareEntryState(Converter<T, EntryStateModel> entryConverter, T context) {
EntryStateModel entryStateModel = entryConverter.convert(context);
entryStateModel.setCurrentStep(stepResolver.resolveCurrentStep(entryStateModel));
return entryStateModel;
}
}

View File

@ -0,0 +1,44 @@
package dev.vality.adapter.flow.lib.handler;
import dev.vality.adapter.flow.lib.exception.UnknownHandlerForStepException;
import dev.vality.adapter.flow.lib.flow.StepResolver;
import dev.vality.adapter.flow.lib.model.EntryStateModel;
import dev.vality.adapter.flow.lib.model.ExitStateModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.core.convert.converter.Converter;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
public class ServerFlowHandlerImpl<T, R> implements ServerFlowHandler<T, R> {
private final List<CommonHandler<ExitStateModel, EntryStateModel>> handlers;
private final StepResolver<EntryStateModel, ExitStateModel> stepResolver;
private final Converter<T, EntryStateModel> entryConverter;
private final Converter<ExitStateModel, R> exitConverter;
@Override
public R handle(T context) throws TException {
var entryStateModel = prepareEntryState(entryConverter, context);
log.info("EntryStateModel: {}", entryStateModel);
var exitStateModel = handlers.stream()
.filter(handler -> handler.isHandle(entryStateModel))
.findFirst()
.orElseThrow(() -> new UnknownHandlerForStepException("Can't find handler to data: " + entryStateModel))
.handle(entryStateModel);
log.info("ExitStateModel: {}", exitStateModel);
exitStateModel.setEntryStateModel(entryStateModel);
exitStateModel.setNextStep(stepResolver.resolveNextStep(exitStateModel));
log.info("Step changing: {} -> {}", entryStateModel.getCurrentStep(), exitStateModel.getNextStep());
return exitConverter.convert(exitStateModel);
}
private EntryStateModel prepareEntryState(Converter<T, EntryStateModel> entryConverter, T context) {
EntryStateModel entryStateModel = entryConverter.convert(context);
entryStateModel.setCurrentStep(stepResolver.resolveCurrentStep(entryStateModel));
return entryStateModel;
}
}

View File

@ -38,6 +38,10 @@ public class BaseResponseModel {
* Data for choose 3ds flow and parameters for redirects.
*/
private ThreeDsData threeDsData;
/**
* Data for display qr code.
*/
private QrDisplayData qrDisplayData;
/**
* Data for support about transactions.
*/

View File

@ -26,6 +26,7 @@ public class ExitStateModel {
private Map<String, String> trxExtra;
private PollingInfo pollingInfo;
private ThreeDsData threeDsData;
private QrDisplayData qrDisplayData;
private AdditionalTrxInfo additionalTrxInfo;
private String recToken;

View File

@ -0,0 +1,24 @@
package dev.vality.adapter.flow.lib.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class QrDisplayData {
/**
* Url for get qr code.
*/
private String qrUrl;
/**
* Id for generate tag
*/
private String tagId;
}

View File

@ -0,0 +1,41 @@
package dev.vality.adapter.flow.lib.processor;
import dev.vality.adapter.flow.lib.constant.Status;
import dev.vality.adapter.flow.lib.model.BaseResponseModel;
import dev.vality.adapter.flow.lib.model.EntryStateModel;
import dev.vality.adapter.flow.lib.model.ExitStateModel;
import dev.vality.adapter.flow.lib.utils.ErrorUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class QrDisplayProcessor implements Processor<ExitStateModel, BaseResponseModel, EntryStateModel> {
private final Processor<ExitStateModel, BaseResponseModel, EntryStateModel> nextProcessor;
@Override
public ExitStateModel process(BaseResponseModel response, EntryStateModel entryStateModel) {
if (response.getStatus() == Status.NEED_REDIRECT
&& !ErrorUtils.isError(response)
&& response.getQrDisplayData() != null
&& response.getQrDisplayData().getQrUrl() != null) {
log.debug("Start qr display process response: {} entryStateModel: {}", response, entryStateModel);
ExitStateModel exitStateModel = new ExitStateModel();
exitStateModel.setQrDisplayData(response.getQrDisplayData());
exitStateModel.setLastOperationStatus(response.getStatus());
exitStateModel.setProviderTrxId(response.getProviderTrxId());
exitStateModel.setTrxExtra(response.getSaveData());
exitStateModel.setAdditionalTrxInfo(response.getAdditionalTrxInfo());
exitStateModel.setCustomContext(response.getCustomContext());
log.debug("Finish qr display process response: {} entryStateModel: {}", response, entryStateModel);
return exitStateModel;
}
if (nextProcessor != null) {
return nextProcessor.process(response, entryStateModel);
}
throw new IllegalStateException("Processor didn't match for response " + response);
}
}

View File

@ -18,7 +18,12 @@ public class TagManagementService {
Optional<String> first = adapterProperties.getTagGeneratorFieldNames().stream()
.filter(s -> StringUtils.hasText(parameters.get(s)))
.findFirst();
return adapterProperties.getTagPrefix() + parameters.get(first.get());
String tagId = parameters.get(first.get());
return initTag(tagId);
}
public String initTag(String tagId) {
return adapterProperties.getTagPrefix() + tagId;
}
}

View File

@ -0,0 +1,131 @@
package dev.vality.adapter.flow.lib.service.factory;
import dev.vality.adapter.common.mapper.ErrorMapping;
import dev.vality.adapter.flow.lib.constant.HttpMethod;
import dev.vality.adapter.flow.lib.model.*;
import dev.vality.adapter.flow.lib.serde.ParametersSerializer;
import dev.vality.adapter.flow.lib.service.ExponentialBackOffPollingService;
import dev.vality.adapter.flow.lib.service.PollingInfoService;
import dev.vality.adapter.flow.lib.service.TagManagementService;
import dev.vality.adapter.flow.lib.utils.TimeoutUtils;
import dev.vality.adapter.flow.lib.utils.TimerProperties;
import dev.vality.damsel.base.Timer;
import dev.vality.damsel.proxy_provider.*;
import dev.vality.damsel.timeout_behaviour.TimeoutBehaviour;
import dev.vality.damsel.user_interaction.QrCode;
import dev.vality.damsel.user_interaction.QrCodeDisplayRequest;
import dev.vality.damsel.user_interaction.UserInteraction;
import lombok.RequiredArgsConstructor;
import java.nio.ByteBuffer;
import java.util.Map;
import static dev.vality.adapter.common.damsel.OptionsExtractors.extractRedirectTimeout;
import static dev.vality.adapter.common.damsel.ProxyProviderPackageCreators.*;
import static dev.vality.adapter.flow.lib.utils.ThreeDsDataInitializer.TAG;
@RequiredArgsConstructor
public class IntentResultQrPaymentFactory implements IntentResultFactory {
private final TimerProperties timerProperties;
private final TagManagementService tagManagementService;
private final ParametersSerializer parametersSerializer;
private final PollingInfoService pollingInfoService;
private final ErrorMapping errorMapping;
private final ExponentialBackOffPollingService exponentialBackOffPollingService;
@Override
public Intent createFinishIntentSuccessWithCheckToken(ExitStateModel exitStateModel) {
EntryStateModel entryStateModel = exitStateModel.getEntryStateModel();
if (entryStateModel.getBaseRequestModel().getRecurrentPaymentData() != null
&& entryStateModel.getBaseRequestModel().getRecurrentPaymentData().isMakeRecurrent()) {
return createFinishIntentSuccessWithToken(exitStateModel.getRecToken());
}
return createFinishIntentSuccess();
}
@Override
public Intent createSuspendIntentWithFailedAfterTimeout(ExitStateModel exitStateModel) {
EntryStateModel entryStateModel = exitStateModel.getEntryStateModel();
QrDisplayData qrDisplayData = exitStateModel.getQrDisplayData();
Map<String, String> adapterConfigurations = entryStateModel.getBaseRequestModel().getAdapterConfigurations();
int timerRedirectTimeoutMin = extractRedirectTimeout(
adapterConfigurations,
timerProperties.getRedirectTimeoutMin());
return Intent.suspend(
new SuspendIntent(
tagManagementService.initTag(qrDisplayData.getTagId()),
Timer.timeout(TimeoutUtils.toSeconds(timerRedirectTimeoutMin))
).setUserInteraction(
UserInteraction.qr_code_display_request(new QrCodeDisplayRequest()
.setQrCode(new QrCode()
.setPayload(qrDisplayData.getQrUrl().getBytes())))
)
);
}
@Override
public Intent createSuspendIntentWithCallbackAfterTimeout(ExitStateModel exitStateModel) {
QrDisplayData qrDisplayData = exitStateModel.getQrDisplayData();
EntryStateModel entryStateModel = exitStateModel.getEntryStateModel();
PollingInfo pollingInfo = pollingInfoService.initPollingInfo(entryStateModel);
if (pollingInfoService.isDeadline(pollingInfo)) {
return createFinishIntentFailed("Sleep timeout", "Max time pool limit reached");
}
exitStateModel.setPollingInfo(pollingInfo);
Map<String, String> adapterConfigurations = entryStateModel.getBaseRequestModel().getAdapterConfigurations();
int timerRedirectTimeoutMin = extractRedirectTimeout(
adapterConfigurations,
timerProperties.getRedirectTimeoutMin());
String tag = tagManagementService.initTag(qrDisplayData.getTagId());
return Intent.suspend(
new SuspendIntent(
tag,
Timer.timeout(TimeoutUtils.toSeconds(timerRedirectTimeoutMin)))
.setTimeoutBehaviour(TimeoutBehaviour.callback(
ByteBuffer.wrap(
parametersSerializer.writeByte(Map.of(TAG, tag))))
).setUserInteraction(
UserInteraction.qr_code_display_request(new QrCodeDisplayRequest()
.setQrCode(new QrCode()
.setPayload(qrDisplayData.getQrUrl().getBytes()))))
);
}
@Override
public Intent createFinishIntentSuccess() {
return Intent.finish(new FinishIntent(FinishStatus.success(new Success())));
}
@Override
public Intent createSleepIntentWithExponentialPolling(ExitStateModel exitStateModel) {
EntryStateModel entryStateModel = exitStateModel.getEntryStateModel();
PollingInfo pollingInfo = pollingInfoService.initPollingInfo(entryStateModel);
if (pollingInfoService.isDeadline(pollingInfo)) {
return createFinishIntentFailed("Sleep timeout", "Max time pool limit reached");
}
exitStateModel.setPollingInfo(pollingInfo);
Map<String, String> adapterConfigurations = entryStateModel.getBaseRequestModel().getAdapterConfigurations();
int nextTimeoutSec =
exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, adapterConfigurations);
return Intent.sleep(new SleepIntent(Timer.timeout(nextTimeoutSec)));
}
@Override
public Intent createFinishIntentFailed(ExitStateModel exitStateModel) {
return Intent.finish(new FinishIntent(FinishStatus.failure(
errorMapping.mapFailure(exitStateModel.getErrorCode(),
exitStateModel.getErrorMessage()))));
}
@Override
public Intent createFinishIntentFailed(String errorCode, String errorMessage) {
return Intent.finish(new FinishIntent(FinishStatus.failure(
errorMapping.mapFailure(errorCode, errorMessage))));
}
}

View File

@ -22,6 +22,7 @@ import dev.vality.adapter.flow.lib.serde.ParametersSerializer;
import dev.vality.adapter.flow.lib.serde.TemporaryContextDeserializer;
import dev.vality.adapter.flow.lib.serde.TemporaryContextSerializer;
import dev.vality.adapter.flow.lib.service.*;
import dev.vality.adapter.flow.lib.service.factory.IntentResultFactory;
import dev.vality.adapter.flow.lib.service.factory.SimpleIntentResultFactory;
import dev.vality.adapter.flow.lib.service.factory.SimpleRecurrentIntentResultFactory;
import dev.vality.adapter.flow.lib.utils.AdapterProperties;
@ -187,7 +188,7 @@ public class HandlerConfig {
@Bean
public ExitModelToProxyResultConverter exitModelToProxyResultConverter(
SimpleIntentResultFactory intentResultFactory,
IntentResultFactory intentResultFactory,
TemporaryContextSerializer temporaryContextSerializer,
ResultIntentResolver resultIntentResolver,
ExitStateModelToTemporaryContextConverter exitStateModelToTemporaryContextConverter) {

View File

@ -15,6 +15,7 @@ import dev.vality.adapter.flow.lib.flow.full.GenerateTokenResultIntentResolverIm
import dev.vality.adapter.flow.lib.flow.full.ResultIntentResolverImpl;
import dev.vality.adapter.flow.lib.handler.CommonHandler;
import dev.vality.adapter.flow.lib.handler.ServerFlowHandler;
import dev.vality.adapter.flow.lib.handler.ServerFlowHandlerImpl;
import dev.vality.adapter.flow.lib.handler.payment.*;
import dev.vality.adapter.flow.lib.model.BaseResponseModel;
import dev.vality.adapter.flow.lib.model.EntryStateModel;
@ -53,7 +54,7 @@ public class FullThreeDsFlowConfig {
StepResolver<EntryStateModel, ExitStateModel> fullThreeDsAllVersionsStepResolverImpl,
CtxToEntryModelConverter ctxToEntryModelConverter,
ExitModelToProxyResultConverter exitModelToProxyResultConverter) {
return new ServerFlowHandler<>(
return new ServerFlowHandlerImpl<>(
getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor),
fullThreeDsAllVersionsStepResolverImpl,
ctxToEntryModelConverter,
@ -69,7 +70,7 @@ public class FullThreeDsFlowConfig {
RecCtxToEntryModelConverter recCtxToEntryStateModelConverter,
ExitModelToRecTokenProxyResultConverter exitModelToRecTokenProxyResultConverter
) {
return new ServerFlowHandler<>(
return new ServerFlowHandlerImpl<>(
getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor),
generateTokenFullThreeDsAllVersionsStepResolverImpl,
recCtxToEntryStateModelConverter,

View File

@ -0,0 +1,54 @@
package dev.vality.adapter.flow.lib.flow.qr;
import dev.vality.adapter.flow.lib.flow.AbstractPaymentTest;
import dev.vality.adapter.flow.lib.flow.qr.config.QrRedirectWithPollingDsFlowConfig;
import dev.vality.adapter.flow.lib.flow.utils.BeanUtils;
import dev.vality.adapter.flow.lib.flow.utils.MockUtil;
import dev.vality.adapter.flow.lib.model.BaseResponseModel;
import dev.vality.damsel.proxy_provider.PaymentContext;
import dev.vality.damsel.proxy_provider.PaymentProxyResult;
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.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = QrRedirectWithPollingDsFlowConfig.class)
@TestPropertySource(properties = {"server.rest.port=8083",
"error-mapping.file=classpath:fixture/errors.json"})
public class ErrorPaymentQrTest extends AbstractPaymentTest {
@BeforeEach
public void setUp() throws TException {
MockitoAnnotations.openMocks(this);
MockUtil.mockAllWithout3Ds(cdsStorageClient, benderClient);
BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel();
baseResponseModel.setErrorCode("rem_error_21");
baseResponseModel.setErrorMessage("Remote service error!");
Mockito.when(client.auth(any())).thenReturn(baseResponseModel);
Mockito.when(client.pay(any())).thenReturn(baseResponseModel);
}
@Test
public void testErrorPayment() throws TException {
// pay
PaymentContext paymentContext = MockUtil.buildPaymentContext(String.valueOf(new Date().getTime()),
MockUtil.buildOptionsOneStage());
PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext);
assertTrue(paymentProxyResult.getIntent().isSetFinish());
assertTrue(paymentProxyResult.getIntent().getFinish().getStatus().isSetFailure());
}
}

View File

@ -0,0 +1,99 @@
package dev.vality.adapter.flow.lib.flow.qr;
import com.fasterxml.jackson.core.JsonProcessingException;
import dev.vality.adapter.flow.lib.constant.OptionFields;
import dev.vality.adapter.flow.lib.constant.Stage;
import dev.vality.adapter.flow.lib.constant.Status;
import dev.vality.adapter.flow.lib.constant.Step;
import dev.vality.adapter.flow.lib.flow.AbstractPaymentTest;
import dev.vality.adapter.flow.lib.flow.qr.config.QrRedirectWithPollingDsFlowConfig;
import dev.vality.adapter.flow.lib.flow.utils.BeanUtils;
import dev.vality.adapter.flow.lib.flow.utils.MockUtil;
import dev.vality.adapter.flow.lib.model.BaseResponseModel;
import dev.vality.damsel.proxy_provider.PaymentContext;
import dev.vality.damsel.proxy_provider.PaymentProxyResult;
import lombok.extern.slf4j.Slf4j;
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.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.io.IOException;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@Slf4j
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = QrRedirectWithPollingDsFlowConfig.class)
@TestPropertySource(properties = {"error-mapping.file=classpath:fixture/errors.json",
"adapter.callbackUrl=http://localhost:8080/test",
"server.rest.endpoint=adapter",
"server.rest.port=8083"})
public class PaymentSuccessQrTest extends AbstractPaymentTest {
@BeforeEach
public void setUp() throws TException {
MockitoAnnotations.openMocks(this);
MockUtil.mockIdGenerator(benderClient);
BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel();
baseResponseModel.setQrDisplayData(BeanUtils.createQrDisplayData());
baseResponseModel.setStatus(Status.NEED_REDIRECT);
Mockito.when(client.auth(any())).thenReturn(baseResponseModel);
Mockito.when(client.pay(any())).thenReturn(baseResponseModel);
BaseResponseModel successResponseModel = BeanUtils.createBaseResponseModel();
Mockito.when(client.capture(any())).thenReturn(successResponseModel);
Mockito.when(client.refund(any())).thenReturn(successResponseModel);
Mockito.when(client.status(any())).thenReturn(successResponseModel);
}
@Test
public void testOneStage() throws TException, IOException {
// auth
Map<String, String> options = MockUtil.buildOptionsOneStage();
testQr(options);
}
@Test
public void testTwoStage() throws TException, IOException {
// auth
Map<String, String> options = MockUtil.buildOptionsTwoStage();
testQr(options);
}
private void testQr(Map<String, String> options) throws TException, JsonProcessingException {
PaymentContext paymentContext = MockUtil.buildPaymentContextPaymentTerminal("invoice_id", options);
PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext);
assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetQrCodeDisplayRequest());
assertEquals(Step.CHECK_STATUS,
temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep());
//checkStatus
paymentProxyResult = processWithDoNothingSuccessResult(paymentContext, paymentProxyResult);
//capture
if (Stage.ONE.equals(options.get(OptionFields.STAGE.name()))) {
paymentProxyResult = checkSuccessCapture(paymentContext, paymentProxyResult, new byte[] {});
processWithDoNothingSuccessResult(paymentContext, paymentProxyResult);
} else {
paymentProxyResult = processCaptureWithCheckStatusResult(paymentContext, paymentProxyResult, new byte[] {});
processWithCheckStatusResult(paymentContext, paymentProxyResult);
}
//refund
checkSuccessRefund(1100L, paymentContext, paymentProxyResult);
}
}

View File

@ -0,0 +1,116 @@
package dev.vality.adapter.flow.lib.flow.qr.config;
import dev.vality.adapter.common.mapper.ErrorMapping;
import dev.vality.adapter.flow.lib.client.RemoteClient;
import dev.vality.adapter.flow.lib.converter.base.EntryModelToBaseRequestModelConverter;
import dev.vality.adapter.flow.lib.converter.entry.CtxToEntryModelConverter;
import dev.vality.adapter.flow.lib.converter.exit.ExitModelToProxyResultConverter;
import dev.vality.adapter.flow.lib.flow.RecurrentResultIntentResolver;
import dev.vality.adapter.flow.lib.flow.ResultIntentResolver;
import dev.vality.adapter.flow.lib.flow.StepResolver;
import dev.vality.adapter.flow.lib.flow.simple.SimpleRedirectWithPollingResultIntentResolver;
import dev.vality.adapter.flow.lib.flow.simple.SimpleRedirectWithPollingStepResolverImpl;
import dev.vality.adapter.flow.lib.flow.simple.UnsupportedGenerateTokenResultIntentResolver;
import dev.vality.adapter.flow.lib.flow.simple.UnsupportedGenerateTokenStepResolverImpl;
import dev.vality.adapter.flow.lib.handler.CommonHandler;
import dev.vality.adapter.flow.lib.handler.ServerFlowHandler;
import dev.vality.adapter.flow.lib.handler.ServerFlowHandlerImpl;
import dev.vality.adapter.flow.lib.handler.payment.*;
import dev.vality.adapter.flow.lib.model.BaseResponseModel;
import dev.vality.adapter.flow.lib.model.EntryStateModel;
import dev.vality.adapter.flow.lib.model.ExitStateModel;
import dev.vality.adapter.flow.lib.processor.*;
import dev.vality.adapter.flow.lib.serde.ParametersSerializer;
import dev.vality.adapter.flow.lib.service.ExponentialBackOffPollingService;
import dev.vality.adapter.flow.lib.service.PollingInfoService;
import dev.vality.adapter.flow.lib.service.TagManagementService;
import dev.vality.adapter.flow.lib.service.factory.IntentResultQrPaymentFactory;
import dev.vality.adapter.flow.lib.utils.TimerProperties;
import dev.vality.damsel.proxy_provider.PaymentContext;
import dev.vality.damsel.proxy_provider.PaymentProxyResult;
import dev.vality.damsel.proxy_provider.RecurrentTokenContext;
import dev.vality.damsel.proxy_provider.RecurrentTokenProxyResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.List;
@Configuration
public class QrRedirectWithPollingDsFlowConfig {
@Bean
public StepResolver<EntryStateModel, ExitStateModel> stepResolverImpl() {
return new SimpleRedirectWithPollingStepResolverImpl();
}
@Bean
public StepResolver<EntryStateModel, ExitStateModel> generateTokenStepResolverImpl() {
return new UnsupportedGenerateTokenStepResolverImpl();
}
@Bean
public ServerFlowHandler<PaymentContext, PaymentProxyResult> serverFlowHandler(
RemoteClient client,
EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter,
Processor<ExitStateModel, BaseResponseModel, EntryStateModel> baseProcessor,
StepResolver<EntryStateModel, ExitStateModel> stepResolverImpl,
CtxToEntryModelConverter ctxToEntryModelConverter,
ExitModelToProxyResultConverter exitModelToProxyResultConverter) {
return new ServerFlowHandlerImpl<>(
getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor),
stepResolverImpl,
ctxToEntryModelConverter,
exitModelToProxyResultConverter);
}
@Bean
@Primary
public Processor<ExitStateModel, BaseResponseModel, EntryStateModel> baseProcessor() {
ErrorProcessor errorProcessor = new ErrorProcessor();
SuccessFinishProcessor baseProcessor = new SuccessFinishProcessor(errorProcessor);
QrDisplayProcessor qrDisplayProcessor = new QrDisplayProcessor(baseProcessor);
return new RetryProcessor(qrDisplayProcessor);
}
@Bean
public RecurrentResultIntentResolver recurrentResultIntentResolver() {
return new UnsupportedGenerateTokenResultIntentResolver();
}
@Bean
public ResultIntentResolver resultIntentResolver(IntentResultQrPaymentFactory intentResultFactory) {
return new SimpleRedirectWithPollingResultIntentResolver(intentResultFactory);
}
@Bean
public ServerFlowHandler<RecurrentTokenContext, RecurrentTokenProxyResult> generateTokenFlowHandler() {
return new ServerFlowHandler<>() {
};
}
@Bean
public IntentResultQrPaymentFactory intentResultFactory(
TimerProperties timerProperties,
TagManagementService tagManagementService,
ParametersSerializer parametersSerializer,
PollingInfoService pollingInfoService,
ErrorMapping errorMapping,
ExponentialBackOffPollingService exponentialBackOffPollingService) {
return new IntentResultQrPaymentFactory(timerProperties, tagManagementService,
parametersSerializer, pollingInfoService, errorMapping, exponentialBackOffPollingService);
}
private List<CommonHandler<ExitStateModel, EntryStateModel>> getHandlers(
RemoteClient client,
EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter,
Processor<ExitStateModel, BaseResponseModel, EntryStateModel> baseProcessor) {
return List.of(new AuthHandler(client, entryModelToBaseRequestModelConverter, baseProcessor),
new CancelHandler(client, entryModelToBaseRequestModelConverter, baseProcessor),
new CaptureHandler(client, entryModelToBaseRequestModelConverter, baseProcessor),
new StatusHandler(client, entryModelToBaseRequestModelConverter, baseProcessor),
new DoNothingHandler(),
new PaymentHandler(client, entryModelToBaseRequestModelConverter, baseProcessor),
new RefundHandler(client, entryModelToBaseRequestModelConverter, baseProcessor));
}
}

View File

@ -1,71 +0,0 @@
package dev.vality.adapter.flow.lib.flow.simple.redirect;
import com.fasterxml.jackson.core.JsonProcessingException;
import dev.vality.adapter.flow.lib.constant.Status;
import dev.vality.adapter.flow.lib.constant.Step;
import dev.vality.adapter.flow.lib.flow.AbstractGenerateTokenTest;
import dev.vality.adapter.flow.lib.flow.simple.redirect.config.SimpleRedirectWithPollingDsFlowConfig;
import dev.vality.adapter.flow.lib.flow.utils.BeanUtils;
import dev.vality.adapter.flow.lib.flow.utils.MockUtil;
import dev.vality.adapter.flow.lib.model.BaseResponseModel;
import dev.vality.damsel.proxy_provider.RecurrentTokenContext;
import dev.vality.damsel.proxy_provider.RecurrentTokenProxyResult;
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.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Date;
import java.util.Map;
import static dev.vality.adapter.flow.lib.flow.full.three.ds.ForwardRecurrentPaymentNon3dsTest.RECURRENT_TOKEN;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SimpleRedirectWithPollingDsFlowConfig.class)
@TestPropertySource(properties = {"server.rest.port=8083",
"error-mapping.file=classpath:fixture/errors.json",
"service.secret.enabled=true"})
public class GenerateToken3ds1Test extends AbstractGenerateTokenTest {
@BeforeEach
public void setUp() throws TException {
MockitoAnnotations.openMocks(this);
MockUtil.mockAllWithout3Ds(cdsStorageClient, benderClient);
BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel();
baseResponseModel.setThreeDsData(BeanUtils.create3Ds1(baseResponseModel));
baseResponseModel.setStatus(Status.NEED_REDIRECT);
BaseResponseModel baseResponseModelRec = BeanUtils.createBaseResponseModel();
baseResponseModelRec.setRecurrentToken(RECURRENT_TOKEN);
Mockito.when(client.generateToken(any())).thenReturn(baseResponseModel);
Mockito.when(client.status(any())).thenReturn(baseResponseModelRec);
}
@Test
public void testPaymentOneStage() throws TException, JsonProcessingException {
// pay
Map<String, String> options = MockUtil.buildOptionsOneStage();
RecurrentTokenContext paymentContext =
MockUtil.buildRecurrentTokenContext(String.valueOf(new Date().getTime()),
options);
RecurrentTokenProxyResult paymentProxyResult = serverHandlerLogDecorator.generateToken(paymentContext);
assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetRedirect());
assertEquals(Step.CHECK_STATUS,
temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep());
//checkStatus
paymentProxyResult = processWithDoNothingSuccessResult(paymentContext, paymentProxyResult);
}
}

View File

@ -15,6 +15,7 @@ import dev.vality.adapter.flow.lib.flow.simple.SimpleRedirectWithPollingResultIn
import dev.vality.adapter.flow.lib.flow.simple.SimpleRedirectWithPollingStepResolverImpl;
import dev.vality.adapter.flow.lib.handler.CommonHandler;
import dev.vality.adapter.flow.lib.handler.ServerFlowHandler;
import dev.vality.adapter.flow.lib.handler.ServerFlowHandlerImpl;
import dev.vality.adapter.flow.lib.handler.payment.*;
import dev.vality.adapter.flow.lib.model.BaseResponseModel;
import dev.vality.adapter.flow.lib.model.EntryStateModel;
@ -52,7 +53,7 @@ public class SimpleRedirectWithPollingDsFlowConfig {
StepResolver<EntryStateModel, ExitStateModel> stepResolverImpl,
CtxToEntryModelConverter ctxToEntryModelConverter,
ExitModelToProxyResultConverter exitModelToProxyResultConverter) {
return new ServerFlowHandler<>(
return new ServerFlowHandlerImpl<>(
getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor),
stepResolverImpl,
ctxToEntryModelConverter,
@ -78,7 +79,7 @@ public class SimpleRedirectWithPollingDsFlowConfig {
StepResolver<EntryStateModel, ExitStateModel> generateTokenStepResolverImpl,
RecCtxToEntryModelConverter recCtxToEntryStateModelConverter,
ExitModelToRecTokenProxyResultConverter exitModelToRecTokenProxyResultConverter) {
return new ServerFlowHandler<>(
return new ServerFlowHandlerImpl<>(
getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor),
generateTokenStepResolverImpl,
recCtxToEntryStateModelConverter,

View File

@ -5,6 +5,7 @@ import dev.vality.adapter.common.utils.CommonConverter;
import dev.vality.adapter.flow.lib.constant.Status;
import dev.vality.adapter.flow.lib.constant.ThreeDsType;
import dev.vality.adapter.flow.lib.model.BaseResponseModel;
import dev.vality.adapter.flow.lib.model.QrDisplayData;
import dev.vality.adapter.flow.lib.model.ThreeDsData;
import java.nio.ByteBuffer;
@ -41,6 +42,13 @@ public class BeanUtils {
.build();
}
public static QrDisplayData createQrDisplayData() {
return QrDisplayData.builder()
.tagId("testTagId")
.qrUrl("http://localhost/3ds")
.build();
}
public static ThreeDsData create3Ds2FullCheck() {
HashMap<String, String> parameters = new HashMap<>();
parameters.put(THREE_DS_METHOD_DATA, "test_threeDsMethodData");

View File

@ -171,6 +171,38 @@ public class MockUtil {
.setOptions(options);
}
public static PaymentContext buildPaymentContextPaymentTerminal(String invoiceId, Map<String, String> options) {
return new PaymentContext()
.setSession(new Session()
.setTarget(TargetInvoicePaymentStatus.processed(
new InvoicePaymentProcessed())))
.setPaymentInfo(new PaymentInfo()
.setInvoice(new Invoice()
.setId(invoiceId)
.setDetails(new InvoiceDetails()
.setDescription("details")))
.setPayment(new InvoicePayment()
.setId("payment_id")
.setCreatedAt("2016-03-22T06:12:27Z")
.setPaymentResource(PaymentResource.disposable_payment_resource(
new DisposablePaymentResource()
.setClientInfo(new ClientInfo()
.setIpAddress("185.31.132.50"))
.setPaymentTool(buildPaymentToolPaymentTerminal())))
.setCost(new Cash()
.setAmount(1200)
.setCurrency(new Currency()
.setSymbolicCode("RUB")
.setNumericCode((short) 643)))
.setContactInfo(new ContactInfo()
.setEmail("kkkk@kkk.ru")
.setPhoneNumber("89037772299"))
)
)
.setOptions(options);
}
public static Map<String, String> buildOptionsOneStage() {
return Map.of(OptionFields.STAGE.name(), Stage.ONE);
}
@ -190,6 +222,12 @@ public class MockUtil {
);
}
public static PaymentTool buildPaymentToolPaymentTerminal() {
return PaymentTool.payment_terminal(
new PaymentTerminal()
);
}
public static PaymentContext buildRecurrentPaymentContext(String invoiceId, String token) {
PaymentContext paymentContext = MockUtil.buildPaymentContext(invoiceId,
MockUtil.buildOptionsOneStage());