diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7381159 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,10 @@ +name: Maven Build Artifact + +on: + pull_request: + branches: + - '*' + +jobs: + build: + uses: valitydev/base-workflow/.github/workflows/maven-library-build.yml@v1 \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..df9c2f4 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,16 @@ +name: Maven Deploy Artifact + +on: + push: + branches: + - 'master' + - 'main' + +jobs: + deploy: + uses: valitydev/base-workflow/.github/workflows/maven-library-deploy.yml@v1 + secrets: + server-username: ${{ secrets.OSSRH_USERNAME }} + server-password: ${{ secrets.OSSRH_TOKEN }} + deploy-secret-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} + deploy-secret-key-password: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb08ff7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# Created by .ignore support plugin (hsz.mobi) +.eunit +deps +*.o +*.beam +*.plt +erl_crash.dump +ebin/*.beam +rel/example_project +.concrete/DEV_MODE +.rebar +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/ +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +*.iws +*.ipr +*.iml + + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +env.list diff --git a/docs/client_implementations_manual.md b/docs/client_implementations_manual.md new file mode 100644 index 0000000..e08bd4e --- /dev/null +++ b/docs/client_implementations_manual.md @@ -0,0 +1,18 @@ +### Имплементация клиента? + +Во все методы клиента приходит полная модель со всеми необходимыми полями для вызова: + +[BaseRequestModel](../src/main/java/dev/vality/adapter/flow/lib/model/BaseRequestModel.java) + +После формирования необходимой внутренней модели и внешнего вызова, вы должны сформировать следующую модель: + +[BaseResponseModel](../src/main/java/dev/vality/adapter/flow/lib/model/BaseResponseModel.java) + +Необходимо учитывать следующие особенности: + +* Можно влиять на выбор следующего шага с помощью + поля [BaseResponseModel.status](../src/main/java/dev/vality/adapter/flow/lib/constant/Status.java). +* Также если был выбран статус NEED_RETRY, есть возможность выбрать конкретную версию процесса, если он + существует в рамках вашего общего взаимодействия. Необходимо сверяться с общим флоу и необходимостью идти в ту или + иную ветку. +* При указании errorCode, в не зависимости от передаваемого статуса, процесс будет завершон с ошибкой. \ No newline at end of file diff --git a/docs/flow_using_lib.md b/docs/flow_using_lib.md new file mode 100644 index 0000000..a582872 --- /dev/null +++ b/docs/flow_using_lib.md @@ -0,0 +1,12 @@ +### Как использовать бибилиотеку? + +1. Необходимо провести аналитику и определить какой процесс вам подходит. Список процессов: + * [Процесс с полным прохождением 3дс версии 1.0 и 2.0](./flows/Full Three Ds V1 V2 Flow Steps.md) + * [Простой процесс с редиректом и поллингом статусов после всех операций](./flows/Full Three Ds V1 V2 Flow Steps.md) +2. Если подходящий вам процесс найден, процесс продолжается по следующему пути: + 1. Подготавливается конфигурация под spring-boot [настройка конфигурации](./spring-boot-configuration.md). + 2. Релизовать, соответствующие выбранному флоу, методы + из [RemoteClient](../src/main/java/dev/vality/adapter/flow/lib/client/RemoteClient.java) [инструкция](./client_implementations_manual.md) + 3. Реализовать валидаторы для ваших входных параметров, уникальных для вашего адаптера и настраевымых командой + поддержки поддержкой(options - на уровне протокола damsel) [интерфейс](../src/main/java/dev/vality/adapter/flow/lib/validator/Validator.java) + 4. Реализация тестов \ No newline at end of file diff --git a/docs/flows/Full Three Ds V1 V2 Flow Steps.md b/docs/flows/Full Three Ds V1 V2 Flow Steps.md new file mode 100644 index 0000000..e2569bf --- /dev/null +++ b/docs/flows/Full Three Ds V1 V2 Flow Steps.md @@ -0,0 +1 @@ +### Процесс с полным прохождением 3дс версии 1.0 и 2.0 \ No newline at end of file diff --git a/docs/flows/Simple Redirect With Polling Flow Steps.md b/docs/flows/Simple Redirect With Polling Flow Steps.md new file mode 100644 index 0000000..e1cc912 --- /dev/null +++ b/docs/flows/Simple Redirect With Polling Flow Steps.md @@ -0,0 +1 @@ +### Простой процесс с редиректом и поллингом статусов после всех операций \ No newline at end of file diff --git a/docs/flows/imgs/Full Three Ds V1 V2 Flow Steps.svg b/docs/flows/imgs/Full Three Ds V1 V2 Flow Steps.svg new file mode 100644 index 0000000..224f917 --- /dev/null +++ b/docs/flows/imgs/Full Three Ds V1 V2 Flow Steps.svg @@ -0,0 +1,470 @@ + + + + + + + + + + + + + + + +
+
+
+ V1 +
+
+
+
+ V1 + +
+
+ + + + + + + + + + +
+
+
+ AUTH +
+
+
+
+ AUTH + +
+
+ + + + + + + + + + +
+
+
+ FINISH_THREE_DS_V1 +
+
+
+
+ FINISH_THREE_DS_V1 + +
+
+ + + + + + + +
+
+
+ V2 +
+
+
+
+ V2 + +
+
+ + + + + + +
+
+
+ 3ds version +
+
+
+
+ 3ds version + +
+
+ + + + + + + + + + +
+
+
+ CHECK_STATUS_3DS_V2 +
+
+
+
+ CHECK_STATUS_3DS_V2 + +
+
+ + + + + + + +
+
+
+ YES +
+
+
+
+ YES + +
+
+ + + + + + + +
+
+
+ NO +
+
+
+
+ NO + +
+
+ + + + + + +
+
+
+ if 3ds +
+
+
+
+ if 3ds + +
+
+ + + + + + +
+
+
+ DO_NOTHING +
+
+
+
+ DO_NOTHING + +
+
+ + + + + + + +
+
+
+ YES +
+
+
+
+ YES + +
+
+ + + + + + + +
+
+
+ NO +
+
+
+
+ NO + +
+
+ + + + + + +
+
+
+ simlple redirect? +
+
+
+
+ simlple redirect? + +
+
+ + + + + + + + + + +
+
+
+ CHECK_NEED_3DS_V2 +
+
+
+
+ CHECK_NEED_3DS_V2 + +
+
+ + + + + + + + + + +
+
+
+ FINISH_3DS_V2 +
+
+
+
+ FINISH_3DS_V2 + +
+
+
+ + + + Text is not SVG - cannot display + + +
\ No newline at end of file diff --git a/docs/flows/imgs/Simple Redirect With Polling Flow Steps.drawio.svg b/docs/flows/imgs/Simple Redirect With Polling Flow Steps.drawio.svg new file mode 100644 index 0000000..313ac72 --- /dev/null +++ b/docs/flows/imgs/Simple Redirect With Polling Flow Steps.drawio.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + +
+
+
+ YES +
+
+
+
+ YES + +
+
+ + + + + + + + + + +
+
+
+ AUTH/PAY +
+
+
+
+ AUTH/PAY + +
+
+ + + + + + + + + + +
+
+
+ CHECK_STATUS +
+
+
+
+ CHECK_STATUS + +
+
+ + + + + + + +
+
+
+ NO +
+
+
+
+ NO + +
+
+ + + + + + +
+
+
+ if 3ds +
+
+
+
+ if 3ds + +
+
+ + + + + + +
+
+
+ DO_NOTHING +
+
+
+
+ DO_NOTHING + +
+
+
+ + + + Text is not SVG - cannot display + + +
\ No newline at end of file diff --git a/docs/spring-boot-configuration.md b/docs/spring-boot-configuration.md new file mode 100644 index 0000000..95d87e6 --- /dev/null +++ b/docs/spring-boot-configuration.md @@ -0,0 +1 @@ +### Конфигурация Spring Boot? diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0066b61 --- /dev/null +++ b/pom.xml @@ -0,0 +1,247 @@ + + + + 4.0.0 + + + dev.vality + library-parent-pom + 1.0.2 + + + + adapter-flow-lib + 0.0.1 + jar + + adapter-flow-lib + Adapter flows libs for reuse + https://github.com/valitydev/adapter-flow-lib.git + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + devs@vality.dev + Vality + https://vality.dev + + + + + scm:git:git://github.com/valitydev/library-parent-pom.git + scm:git:ssh://github.com/valitydev/library-parent-pom.git + https://github.com/valitydev/library-parent-pom/tree/master + + + + ${env.REGISTRY} + UTF-8 + UTF-8 + 15 + 0.3.6 + + 1.20-be9cdeb + + 3.10 + 1.15 + + 1.7.33 + 1.18.22 + 1.2.10 + 7.0.1 + + 2.11.2 + [1.0.4,) + + 1.544-dcd92dd + 1.62-07f2b0f + + 1.0.6 + 1.0.1 + 1.0.0 + 5.8.2 + 0.0.1 + 2.22.2 + 5.3.14 + 4.0.1 + 2.0.1.Final + 1.0.0 + 2.11.2 + + 2.6.3 + + + + + + + javax.validation + validation-api + ${validation-api.version} + + + javax.servlet + javax.servlet-api + ${javax.servlet-api.version} + provided + + + org.springframework.boot + spring-boot-starter-validation + ${spring-boot-starter.version} + provided + + + org.springframework + spring-web + ${spring.version} + provided + + + org.springframework + spring-core + ${spring.version} + provided + + + org.apache.commons + commons-lang3 + ${apache.commons.lang3.version} + + + commons-codec + commons-codec + ${apache.commons.codec.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.projectlombok + lombok + ${lombok.version} + provided + + + ch.qos.logback + logback-core + ${logback.version} + compile + + + ch.qos.logback + logback-classic + ${logback.version} + compile + + + net.logstash.logback + logstash-logback-encoder + ${logstash-logback-encoder.version} + compile + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + + + dev.vality + damsel + ${damsel.version} + provided + + + dev.vality.woody + woody-thrift + ${woody.version} + provided + + + dev.vality.adapter-client-lib + cds-client-storage + ${adapter-client-lib.version} + + + dev.vality.adapter-client-lib + hellgate-adapter-client + ${adapter-client-lib.version} + + + dev.vality + bender-proto + ${bender-proto.version} + provided + + + dev.vality + error-mapping-java + ${error-mapping-java.version} + + + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot-starter.version} + test + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot-starter.version} + test + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot-starter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter-engine.version} + test + + + dev.vality.geck + serializer + 0.0.1 + test + + + dev.vality.logback + nop-rolling + 1.0.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/client/RemoteClient.java b/src/main/java/dev/vality/adapter/flow/lib/client/RemoteClient.java new file mode 100644 index 0000000..6f49a61 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/client/RemoteClient.java @@ -0,0 +1,53 @@ +package dev.vality.adapter.flow.lib.client; + + +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +import dev.vality.adapter.flow.lib.model.BaseResponseModel; + +/** + * This interface is used to call an external system in order to perform some operation in the payment flow. + * Not all methods are required for flows. + */ +public interface RemoteClient { + + default BaseResponseModel auth(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel generateToken(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel pay(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel capture(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel cancel(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel refund(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel finish3ds(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel check3dsV2(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel finish3dsV2(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + + default BaseResponseModel status(BaseRequestModel request) { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/constant/MetaData.java b/src/main/java/dev/vality/adapter/flow/lib/constant/MetaData.java new file mode 100644 index 0000000..4127c5f --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/constant/MetaData.java @@ -0,0 +1,11 @@ +package dev.vality.adapter.flow.lib.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MetaData { + + public static final String META_REC_TOKEN = "meta_rec_token"; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/constant/OptionFields.java b/src/main/java/dev/vality/adapter/flow/lib/constant/OptionFields.java new file mode 100644 index 0000000..968c652 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/constant/OptionFields.java @@ -0,0 +1,7 @@ +package dev.vality.adapter.flow.lib.constant; + +public enum OptionFields { + + STAGE + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/constant/RedirectFields.java b/src/main/java/dev/vality/adapter/flow/lib/constant/RedirectFields.java new file mode 100644 index 0000000..7176d4f --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/constant/RedirectFields.java @@ -0,0 +1,13 @@ +package dev.vality.adapter.flow.lib.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum RedirectFields { + + TERM_URL("TermUrl"); + + private final String value; +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/constant/Stage.java b/src/main/java/dev/vality/adapter/flow/lib/constant/Stage.java new file mode 100644 index 0000000..78a25ee --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/constant/Stage.java @@ -0,0 +1,11 @@ +package dev.vality.adapter.flow.lib.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Stage { + + public static final String ONE = "ONE"; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/constant/Status.java b/src/main/java/dev/vality/adapter/flow/lib/constant/Status.java new file mode 100644 index 0000000..d3f5b2f --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/constant/Status.java @@ -0,0 +1,22 @@ +package dev.vality.adapter.flow.lib.constant; + +public enum Status { + + /** + * The operation will complete successfully or move on to the next step if a step exists. + */ + SUCCESS, + /** + * The operation will fail and exit. + */ + ERROR, + /** + * The operation will redirect if there are options for the redirect. + */ + NEED_REDIRECT, + /** + * This status need for cycle or retry operation (example: polling status). + */ + NEED_RETRY + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/constant/Step.java b/src/main/java/dev/vality/adapter/flow/lib/constant/Step.java new file mode 100644 index 0000000..4b68b01 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/constant/Step.java @@ -0,0 +1,18 @@ +package dev.vality.adapter.flow.lib.constant; + +public enum Step { + + AUTH, + PAY, + GENERATE_TOKEN, + FINISH_THREE_DS_V1, + FINISH_THREE_DS_V2, + CANCEL, + REFUND, + CHECK_STATUS, + CHECK_STATUS_3DS_V2, + CHECK_NEED_3DS_V2, + CAPTURE, + DO_NOTHING + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/constant/TargetStatus.java b/src/main/java/dev/vality/adapter/flow/lib/constant/TargetStatus.java new file mode 100644 index 0000000..fdcb45f --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/constant/TargetStatus.java @@ -0,0 +1,8 @@ +package dev.vality.adapter.flow.lib.constant; + +public enum TargetStatus { + PROCESSED, + CAPTURED, + CANCELLED, + REFUNDED +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/constant/ThreeDsType.java b/src/main/java/dev/vality/adapter/flow/lib/constant/ThreeDsType.java new file mode 100644 index 0000000..b9eee38 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/constant/ThreeDsType.java @@ -0,0 +1,9 @@ +package dev.vality.adapter.flow.lib.constant; + +public enum ThreeDsType { + + V1, + V2_SIMPLE, + V2_FULL + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/controller/ThreeDsCallbackController.java b/src/main/java/dev/vality/adapter/flow/lib/controller/ThreeDsCallbackController.java new file mode 100644 index 0000000..98b8e9a --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/controller/ThreeDsCallbackController.java @@ -0,0 +1,33 @@ +package dev.vality.adapter.flow.lib.controller; + +import dev.vality.adapter.flow.lib.service.ThreeDsAdapterService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Slf4j +@RestController +@RequestMapping({"/${server.rest.endpoint}"}) +@RequiredArgsConstructor +public class ThreeDsCallbackController { + + private final ThreeDsAdapterService threeDsAdapterService; + + @PostMapping({"term-url"}) + public String receivePaymentIncomingParameters(HttpServletRequest servletRequest, + HttpServletResponse servletResponse) { + return this.threeDsAdapterService.receivePaymentIncomingParameters(servletRequest, servletResponse); + } + + @PostMapping({"recurrent-term-url"}) + public String receiveRecurrentIncomingParameters(HttpServletRequest servletRequest, + HttpServletResponse servletResponse) { + return this.threeDsAdapterService.receiveRecurrentIncomingParameters(servletRequest, servletResponse); + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/converter/ExitStateModelToTemporaryContextConverter.java b/src/main/java/dev/vality/adapter/flow/lib/converter/ExitStateModelToTemporaryContextConverter.java new file mode 100644 index 0000000..c4f97f0 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/converter/ExitStateModelToTemporaryContextConverter.java @@ -0,0 +1,18 @@ +package dev.vality.adapter.flow.lib.converter; + +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.model.TemporaryContext; +import org.springframework.core.convert.converter.Converter; + +public class ExitStateModelToTemporaryContextConverter implements Converter { + + @Override + public TemporaryContext convert(ExitStateModel source) { + return TemporaryContext.builder() + .providerTrxId(source.getProviderTrxId()) + .nextStep(source.getNextStep()) + .pollingInfo(source.getPollingInfo()) + .build(); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/converter/base/EntryModelToBaseRequestModelConverter.java b/src/main/java/dev/vality/adapter/flow/lib/converter/base/EntryModelToBaseRequestModelConverter.java new file mode 100644 index 0000000..520a167 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/converter/base/EntryModelToBaseRequestModelConverter.java @@ -0,0 +1,17 @@ +package dev.vality.adapter.flow.lib.converter.base; + +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import lombok.RequiredArgsConstructor; +import org.springframework.core.convert.converter.Converter; + +@RequiredArgsConstructor +public class EntryModelToBaseRequestModelConverter implements Converter { + + @Override + public BaseRequestModel convert(EntryStateModel entryStateModel) { + return entryStateModel.getBaseRequestModel(); + } + +} + diff --git a/src/main/java/dev/vality/adapter/flow/lib/converter/entry/CtxToEntryModelConverter.java b/src/main/java/dev/vality/adapter/flow/lib/converter/entry/CtxToEntryModelConverter.java new file mode 100644 index 0000000..4cc2b57 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/converter/entry/CtxToEntryModelConverter.java @@ -0,0 +1,145 @@ +package dev.vality.adapter.flow.lib.converter.entry; + +import dev.vality.adapter.flow.lib.constant.MetaData; +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.IdGenerator; +import dev.vality.adapter.flow.lib.service.TemporaryContextService; +import dev.vality.adapter.flow.lib.utils.CallbackUrlExtractor; +import dev.vality.adapter.flow.lib.utils.CardDataUtils; +import dev.vality.adapter.flow.lib.utils.TargetStatusResolver; +import dev.vality.cds.client.storage.CdsClientStorage; +import dev.vality.cds.client.storage.utils.BankCardExtractor; +import dev.vality.cds.storage.Auth3DS; +import dev.vality.cds.storage.CardData; +import dev.vality.cds.storage.SessionData; +import dev.vality.damsel.domain.BankCard; +import dev.vality.damsel.domain.TransactionInfo; +import dev.vality.damsel.proxy_provider.*; +import dev.vality.java.cds.utils.model.CardDataProxyModel; +import dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators; +import dev.vality.java.damsel.utils.extractors.ProxyProviderPackageExtractors; +import lombok.RequiredArgsConstructor; +import org.springframework.core.convert.converter.Converter; + +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +public class CtxToEntryModelConverter implements Converter { + + private final CdsClientStorage cdsStorage; + private final TemporaryContextDeserializer temporaryContextDeserializer; + private final IdGenerator idGenerator; + private final TemporaryContextService temporaryContextService; + private final CallbackUrlExtractor callbackUrlExtractor; + + @Override + public EntryStateModel convert(PaymentContext context) { + var paymentInfo = context.getPaymentInfo(); + var payment = paymentInfo.getPayment(); + var temporaryContext = temporaryContextService.getTemporaryContext(context, temporaryContextDeserializer); + var paymentResource = payment.getPaymentResource(); + var mobilePaymentDataBuilder = MobilePaymentData.builder(); + var cardDataBuilder = dev.vality.adapter.flow.lib.model.CardData.builder(); + + Step currentStep = temporaryContext.getNextStep(); + TargetStatus targetStatus = TargetStatusResolver.extractTargetStatus(context.getSession().getTarget()); + initPaymentData(context, paymentResource, mobilePaymentDataBuilder, cardDataBuilder, currentStep, targetStatus); + + TransactionInfo trx = payment.getTrx(); + RecurrentPaymentData recurrentPaymentData = initRecurrentPaymentData(payment, paymentResource, trx); + Map adapterConfigurations = context.getOptions(); + return EntryStateModel.builder() + .baseRequestModel(BaseRequestModel.builder().recurrentPaymentData(recurrentPaymentData) + .mobilePaymentData(mobilePaymentDataBuilder.build()) + .cardData(cardDataBuilder.build()) + .refundData(initRefundData(paymentInfo)) + .paymentId(idGenerator.get(paymentInfo.getInvoice().getId())) + .createdAt(paymentInfo.getPayment().getCreatedAt()) + .currency(payment.getCost().getCurrency().getSymbolicCode()) + .amount(payment.getCost().getAmount()) + .details(paymentInfo.getInvoice().getDetails().getDescription()) + .payerInfo(PayerInfo.builder() + .ip(ProxyProviderPackageCreators.extractIpAddress(context)) + .build()) + .adapterConfigurations(adapterConfigurations) + .providerTrxId(trx != null ? trx.getId() : temporaryContext.getProviderTrxId()) + .savedData(trx != null ? trx.getExtra() : new HashMap<>()) + .successRedirectUrl(getSuccessRedirectUrl(payment, adapterConfigurations)) + .build()) + .targetStatus(targetStatus) + .currentStep(currentStep) + .build(); + } + + private String getSuccessRedirectUrl(InvoicePayment payment, Map adapterConfigurations) { + return callbackUrlExtractor.getSuccessRedirectUrl( + adapterConfigurations, + payment.isSetPayerSessionInfo() + ? payment.getPayerSessionInfo().getRedirectUrl() + : null); + } + + private void initPaymentData(PaymentContext context, PaymentResource paymentResource, + MobilePaymentData.MobilePaymentDataBuilder mobilePaymentDataBuilder, + dev.vality.adapter.flow.lib.model.CardData.CardDataBuilder cardDataBuilder, + Step currentStep, TargetStatus targetStatus) { + if (paymentResource.isSetDisposablePaymentResource() + && currentStep == null + && targetStatus == TargetStatus.PROCESSED) { + SessionData sessionData = cdsStorage.getSessionData(context); + if (sessionData.isSetAuthData() && sessionData.getAuthData().isSetAuth3ds()) { + Auth3DS auth3ds = sessionData.getAuthData().getAuth3ds(); + mobilePaymentDataBuilder.cryptogram(auth3ds.getCryptogram()) + .eci(auth3ds.getEci()); + } else { + CardDataProxyModel cardData = getCardData(context, paymentResource); + cardDataBuilder.cardHolder(cardData.getCardholderName()) + .pan(cardData.getPan()) + .cvv2(CardDataUtils.extractCvv2(sessionData)) + .expYear(cardData.getExpYear()) + .expMonth(cardData.getExpMonth()); + } + } + } + + private RefundData initRefundData(PaymentInfo paymentInfo) { + RefundData refundData = null; + if (paymentInfo.isSetRefund()) { + InvoicePaymentRefund refund = paymentInfo.getRefund(); + refundData = RefundData.builder() + .id(refund.getId()) + .amount(refund.getCash().getAmount()) + .build(); + } + return refundData; + } + + private RecurrentPaymentData initRecurrentPaymentData(InvoicePayment payment, + PaymentResource paymentResource, + TransactionInfo transactionInfo) { + var recurrentPaymentDataBuilder = RecurrentPaymentData.builder() + .makeRecurrent(payment.make_recurrent); + if (paymentResource.isSetRecurrentPaymentResource()) { + recurrentPaymentDataBuilder.recToken(paymentResource.getRecurrentPaymentResource().getRecToken()); + } else if (transactionInfo != null && transactionInfo.getExtra() != null + && transactionInfo.getExtra().containsKey(MetaData.META_REC_TOKEN)) { + recurrentPaymentDataBuilder.recToken(transactionInfo.getExtra().get(MetaData.META_REC_TOKEN)); + } + return recurrentPaymentDataBuilder.build(); + } + + private CardDataProxyModel getCardData(PaymentContext context, PaymentResource paymentResource) { + if (paymentResource.isSetDisposablePaymentResource()) { + String cardToken = ProxyProviderPackageExtractors.extractBankCardToken(paymentResource); + CardData cardData = cdsStorage.getCardData(cardToken); + BankCard bankCard = ProxyProviderPackageExtractors.extractBankCard(context); + return BankCardExtractor.initCardDataProxyModel(bankCard, cardData); + } + return cdsStorage.getCardData(context); + } +} + diff --git a/src/main/java/dev/vality/adapter/flow/lib/converter/entry/RecCtxToEntryModelConverter.java b/src/main/java/dev/vality/adapter/flow/lib/converter/entry/RecCtxToEntryModelConverter.java new file mode 100644 index 0000000..c1d3ad9 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/converter/entry/RecCtxToEntryModelConverter.java @@ -0,0 +1,125 @@ +package dev.vality.adapter.flow.lib.converter.entry; + +import dev.vality.adapter.flow.lib.constant.MetaData; +import dev.vality.adapter.flow.lib.model.*; +import dev.vality.adapter.flow.lib.serde.TemporaryContextDeserializer; +import dev.vality.adapter.flow.lib.service.IdGenerator; +import dev.vality.adapter.flow.lib.service.TemporaryContextService; +import dev.vality.adapter.flow.lib.utils.CardDataUtils; +import dev.vality.cds.client.storage.CdsClientStorage; +import dev.vality.cds.client.storage.utils.BankCardExtractor; +import dev.vality.cds.storage.Auth3DS; +import dev.vality.cds.storage.CardData; +import dev.vality.cds.storage.SessionData; +import dev.vality.damsel.domain.BankCard; +import dev.vality.damsel.domain.DisposablePaymentResource; +import dev.vality.damsel.domain.PaymentTool; +import dev.vality.damsel.domain.TransactionInfo; +import dev.vality.damsel.proxy_provider.RecurrentPaymentTool; +import dev.vality.damsel.proxy_provider.RecurrentTokenContext; +import dev.vality.java.cds.utils.model.CardDataProxyModel; +import dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators; +import dev.vality.java.damsel.utils.extractors.ProxyProviderPackageExtractors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; + +import java.util.HashMap; +import java.util.UUID; + +@Slf4j +@RequiredArgsConstructor +public class RecCtxToEntryModelConverter implements Converter { + + private final TemporaryContextDeserializer temporaryContextDeserializer; + private final CdsClientStorage cdsStorage; + private final IdGenerator idGenerator; + private final TemporaryContextService temporaryContextService; + + @Override + public EntryStateModel convert(RecurrentTokenContext context) { + var generalExitStateModel = temporaryContextService.getTemporaryContext( + context, temporaryContextDeserializer); + var tokenInfo = context.getTokenInfo(); + var recurrentPaymentTool = tokenInfo.getPaymentTool(); + var paymentResource = recurrentPaymentTool.getPaymentResource(); + var paymentTool = paymentResource.getPaymentTool(); + validatePaymentTool(paymentTool); + var entryStateModelBuilder = + EntryStateModel.builder(); + var mobilePaymentDataBuilder = MobilePaymentData.builder(); + var cardDataBuilder = dev.vality.adapter.flow.lib.model.CardData.builder(); + initPaymentData(context, generalExitStateModel, paymentResource, mobilePaymentDataBuilder, cardDataBuilder); + + TransactionInfo transactionInfo = tokenInfo.getTrx(); + Long orderId = idGenerator.get(tokenInfo.getPaymentTool().getId()); + return entryStateModelBuilder + .baseRequestModel(BaseRequestModel.builder() + .recurrentPaymentData(RecurrentPaymentData + .builder() + .makeRecurrent(true) + .recToken(transactionInfo != null && transactionInfo.getExtra() != null + ? transactionInfo.getExtra().get(MetaData.META_REC_TOKEN) + : null) + .build()) + .mobilePaymentData(mobilePaymentDataBuilder.build()) + .cardData(cardDataBuilder.build()) + .refundData(initRefundData(recurrentPaymentTool, orderId)) + .paymentId(orderId) + .currency(recurrentPaymentTool.getMinimalPaymentCost().getCurrency().getSymbolicCode()) + .amount(recurrentPaymentTool.getMinimalPaymentCost().getAmount()) + .details(recurrentPaymentTool.getId()) + .payerInfo(PayerInfo.builder() + .ip(ProxyProviderPackageCreators.extractIpAddress(context)) + .build()) + .adapterConfigurations(context.getOptions()) + .providerTrxId(transactionInfo != null ? transactionInfo.getId() : null) + .savedData(transactionInfo == null || transactionInfo.getExtra() == null + ? new HashMap<>() + : transactionInfo.getExtra()) + .build()) + .currentStep(generalExitStateModel.getNextStep()) + .build(); + } + + private void initPaymentData(RecurrentTokenContext context, TemporaryContext generalExitStateModel, + DisposablePaymentResource paymentResource, + MobilePaymentData.MobilePaymentDataBuilder mobilePaymentDataBuilder, + dev.vality.adapter.flow.lib.model.CardData.CardDataBuilder cardDataBuilder) { + if (generalExitStateModel == null || generalExitStateModel.getNextStep() == null) { + SessionData sessionData = cdsStorage.getSessionData(context); + if (sessionData.getAuthData().isSetAuth3ds()) { + Auth3DS auth3ds = sessionData.getAuthData().getAuth3ds(); + mobilePaymentDataBuilder.cryptogram(auth3ds.getCryptogram()) + .eci(auth3ds.getEci()); + } else { + CardDataProxyModel cardData = getCardData(context, paymentResource); + cardDataBuilder.cardHolder(cardData.getCardholderName()) + .pan(cardData.getPan()) + .cvv2(CardDataUtils.extractCvv2(sessionData)) + .expYear(cardData.getExpYear()) + .expMonth(cardData.getExpMonth()); + } + } + } + + private void validatePaymentTool(PaymentTool paymentTool) { + if (!paymentTool.isSetBankCard()) { + throw new IllegalArgumentException("Wrong recurrentPaymentTool. It should be bank card"); + } + } + + private RefundData initRefundData(RecurrentPaymentTool recurrentPaymentTool, Long orderId) { + return RefundData.builder() + .id(String.valueOf(orderId)) + .amount(recurrentPaymentTool.getMinimalPaymentCost().getAmount()) + .build(); + } + + private CardDataProxyModel getCardData(RecurrentTokenContext context, DisposablePaymentResource paymentResource) { + String cardToken = ProxyProviderPackageExtractors.extractBankCardToken(paymentResource); + CardData cardData = cdsStorage.getCardData(cardToken); + BankCard bankCard = ProxyProviderPackageExtractors.extractBankCard(context); + return BankCardExtractor.initCardDataProxyModel(bankCard, cardData); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/converter/exit/ExitModelToProxyResultConverter.java b/src/main/java/dev/vality/adapter/flow/lib/converter/exit/ExitModelToProxyResultConverter.java new file mode 100644 index 0000000..e88a526 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/converter/exit/ExitModelToProxyResultConverter.java @@ -0,0 +1,42 @@ +package dev.vality.adapter.flow.lib.converter.exit; + +import dev.vality.adapter.flow.lib.converter.ExitStateModelToTemporaryContextConverter; +import dev.vality.adapter.flow.lib.flow.ResultIntentResolver; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.serde.TemporaryContextSerializer; +import dev.vality.adapter.flow.lib.service.IntentResultFactory; +import dev.vality.damsel.proxy_provider.Intent; +import dev.vality.damsel.proxy_provider.PaymentProxyResult; +import dev.vality.java.damsel.utils.creators.DomainPackageCreators; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; + +@Slf4j +@RequiredArgsConstructor +public class ExitModelToProxyResultConverter implements Converter { + + private final IntentResultFactory intentResultFactory; + private final TemporaryContextSerializer serializer; + private final ResultIntentResolver resultIntentResolver; + private final ExitStateModelToTemporaryContextConverter contextConverter; + + @Override + public PaymentProxyResult convert(ExitStateModel exitStateModel) { + if (StringUtils.hasText(exitStateModel.getErrorCode())) { + return new PaymentProxyResult(intentResultFactory.createFinishIntentFailed(exitStateModel)); + } + + Intent intent = resultIntentResolver.initIntentByStep(exitStateModel); + + return new PaymentProxyResult(intent) + .setNextState(serializer.writeByte(contextConverter.convert(exitStateModel))) + .setTrx(DomainPackageCreators.createTransactionInfo( + exitStateModel.getProviderTrxId(), exitStateModel.getTrxExtra()) + ); + } + + +} + diff --git a/src/main/java/dev/vality/adapter/flow/lib/converter/exit/ExitModelToRecTokenProxyResultConverter.java b/src/main/java/dev/vality/adapter/flow/lib/converter/exit/ExitModelToRecTokenProxyResultConverter.java new file mode 100644 index 0000000..5b5c8cb --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/converter/exit/ExitModelToRecTokenProxyResultConverter.java @@ -0,0 +1,43 @@ +package dev.vality.adapter.flow.lib.converter.exit; + +import dev.vality.adapter.flow.lib.converter.ExitStateModelToTemporaryContextConverter; +import dev.vality.adapter.flow.lib.flow.RecurrentResultIntentResolver; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.serde.TemporaryContextSerializer; +import dev.vality.adapter.flow.lib.service.RecurrentIntentResultFactory; +import dev.vality.damsel.proxy_provider.RecurrentTokenIntent; +import dev.vality.damsel.proxy_provider.RecurrentTokenProxyResult; +import lombok.RequiredArgsConstructor; +import org.springframework.core.convert.converter.Converter; + +import static dev.vality.java.damsel.utils.creators.DomainPackageCreators.createTransactionInfo; + +@RequiredArgsConstructor +public class ExitModelToRecTokenProxyResultConverter implements Converter { + + private final RecurrentIntentResultFactory recurrentIntentResultFactory; + private final TemporaryContextSerializer serializer; + private final RecurrentResultIntentResolver recurrentResultIntentResolver; + private final ExitStateModelToTemporaryContextConverter contextConverter; + + @Override + public RecurrentTokenProxyResult convert(ExitStateModel exitStateModel) { + if (exitStateModel.getErrorCode() != null) { + return new RecurrentTokenProxyResult( + recurrentIntentResultFactory.createFinishIntentFailed( + exitStateModel.getErrorCode(), + exitStateModel.getErrorMessage()) + ); + } + + RecurrentTokenIntent intent = recurrentResultIntentResolver.initIntentByStep(exitStateModel); + + return new RecurrentTokenProxyResult(intent) + .setNextState(serializer.writeByte(contextConverter.convert(exitStateModel))) + .setTrx(createTransactionInfo( + exitStateModel.getProviderTrxId(), exitStateModel.getTrxExtra()) + ); + } + +} + diff --git a/src/main/java/dev/vality/adapter/flow/lib/exception/DataNotCorrespondStateException.java b/src/main/java/dev/vality/adapter/flow/lib/exception/DataNotCorrespondStateException.java new file mode 100644 index 0000000..0346838 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/exception/DataNotCorrespondStateException.java @@ -0,0 +1,24 @@ +package dev.vality.adapter.flow.lib.exception; + +public class DataNotCorrespondStateException extends RuntimeException { + + public DataNotCorrespondStateException() { + } + + public DataNotCorrespondStateException(String message) { + super(message); + } + + public DataNotCorrespondStateException(String message, Throwable cause) { + super(message, cause); + } + + public DataNotCorrespondStateException(Throwable cause) { + super(cause); + } + + public DataNotCorrespondStateException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/exception/DeserializationException.java b/src/main/java/dev/vality/adapter/flow/lib/exception/DeserializationException.java new file mode 100644 index 0000000..60a3302 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/exception/DeserializationException.java @@ -0,0 +1,19 @@ +package dev.vality.adapter.flow.lib.exception; + +public class DeserializationException extends RuntimeException { + public DeserializationException() { + super(); + } + + public DeserializationException(String message) { + super(message); + } + + public DeserializationException(Throwable cause) { + super(cause); + } + + public DeserializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/exception/UnknownHandlerForStepException.java b/src/main/java/dev/vality/adapter/flow/lib/exception/UnknownHandlerForStepException.java new file mode 100644 index 0000000..c71f697 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/exception/UnknownHandlerForStepException.java @@ -0,0 +1,23 @@ +package dev.vality.adapter.flow.lib.exception; + +public class UnknownHandlerForStepException extends RuntimeException { + public UnknownHandlerForStepException() { + } + + public UnknownHandlerForStepException(String message) { + super(message); + } + + public UnknownHandlerForStepException(String message, Throwable cause) { + super(message, cause); + } + + public UnknownHandlerForStepException(Throwable cause) { + super(cause); + } + + public UnknownHandlerForStepException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/exception/UnknownTargetStatusException.java b/src/main/java/dev/vality/adapter/flow/lib/exception/UnknownTargetStatusException.java new file mode 100644 index 0000000..b59c41b --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/exception/UnknownTargetStatusException.java @@ -0,0 +1,8 @@ +package dev.vality.adapter.flow.lib.exception; + +public class UnknownTargetStatusException extends RuntimeException { + + public UnknownTargetStatusException() { + super("Unknown target status!"); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/AbstractGenerateTokenStepResolver.java b/src/main/java/dev/vality/adapter/flow/lib/flow/AbstractGenerateTokenStepResolver.java new file mode 100644 index 0000000..85cd8f2 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/AbstractGenerateTokenStepResolver.java @@ -0,0 +1,20 @@ +package dev.vality.adapter.flow.lib.flow; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.utils.StageFlowResolver; + +public abstract class AbstractGenerateTokenStepResolver + implements StepResolver { + + @Override + public Step resolveCurrentStep(EntryStateModel stateModel) { + Step currentStep = stateModel.getCurrentStep(); + if (currentStep != null) { + return currentStep; + } + return StageFlowResolver.isOneStageFlow(stateModel) ? Step.PAY : Step.AUTH; + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/AbstractPaymentStepResolver.java b/src/main/java/dev/vality/adapter/flow/lib/flow/AbstractPaymentStepResolver.java new file mode 100644 index 0000000..52ae5ba --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/AbstractPaymentStepResolver.java @@ -0,0 +1,25 @@ +package dev.vality.adapter.flow.lib.flow; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.utils.StageFlowResolver; + +public abstract class AbstractPaymentStepResolver + implements StepResolver { + + @Override + public Step resolveCurrentStep(T entryStateModel) { + Step currentStep = entryStateModel.getCurrentStep(); + if (currentStep != null) { + return currentStep; + } + return switch (entryStateModel.getTargetStatus()) { + case PROCESSED -> StageFlowResolver.isOneStageFlow(entryStateModel) ? Step.PAY : Step.AUTH; + case CAPTURED -> StageFlowResolver.isOneStageFlow(entryStateModel) ? Step.DO_NOTHING : Step.CAPTURE; + case CANCELLED -> Step.CANCEL; + case REFUNDED -> Step.REFUND; + }; + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/RecurrentResultIntentResolver.java b/src/main/java/dev/vality/adapter/flow/lib/flow/RecurrentResultIntentResolver.java new file mode 100644 index 0000000..7e4c729 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/RecurrentResultIntentResolver.java @@ -0,0 +1,10 @@ +package dev.vality.adapter.flow.lib.flow; + +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.damsel.proxy_provider.RecurrentTokenIntent; + +public interface RecurrentResultIntentResolver { + + RecurrentTokenIntent initIntentByStep(ExitStateModel exitStateModel); + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/ResultIntentResolver.java b/src/main/java/dev/vality/adapter/flow/lib/flow/ResultIntentResolver.java new file mode 100644 index 0000000..82a2793 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/ResultIntentResolver.java @@ -0,0 +1,10 @@ +package dev.vality.adapter.flow.lib.flow; + +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.damsel.proxy_provider.Intent; + +public interface ResultIntentResolver { + + Intent initIntentByStep(ExitStateModel exitStateModel); + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/StepResolver.java b/src/main/java/dev/vality/adapter/flow/lib/flow/StepResolver.java new file mode 100644 index 0000000..c86d8c3 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/StepResolver.java @@ -0,0 +1,13 @@ +package dev.vality.adapter.flow.lib.flow; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; + +public interface StepResolver { + + Step resolveCurrentStep(T entryStateModel); + + Step resolveNextStep(R exitStateModel); + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/full/FullThreeDsAllVersionsStepResolverImpl.java b/src/main/java/dev/vality/adapter/flow/lib/flow/full/FullThreeDsAllVersionsStepResolverImpl.java new file mode 100644 index 0000000..2d9fc7f --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/full/FullThreeDsAllVersionsStepResolverImpl.java @@ -0,0 +1,39 @@ +package dev.vality.adapter.flow.lib.flow.full; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.flow.AbstractPaymentStepResolver; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; + +public class FullThreeDsAllVersionsStepResolverImpl + extends AbstractPaymentStepResolver { + + @Override + public Step resolveNextStep(ExitStateModel exitStateModel) { + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + Step step = entryStateModel.getCurrentStep(); + switch (step) { + case AUTH, PAY: + if (ThreeDsBranchResolver.isRedirectForThreeDsV1(exitStateModel)) { + return Step.FINISH_THREE_DS_V1; + } else if (ThreeDsBranchResolver.isRedirectForThreeDsV2Simple(exitStateModel)) { + return Step.FINISH_THREE_DS_V2; + } else if (ThreeDsBranchResolver.isRedirectForThreeDsV2Full(exitStateModel)) { + return Step.CHECK_NEED_3DS_V2; + } else { + return Step.DO_NOTHING; + } + case CHECK_NEED_3DS_V2: + if (ThreeDsBranchResolver.isRedirectForThreeDsV2Full(exitStateModel)) { + return Step.FINISH_THREE_DS_V2; + } else { + return Step.DO_NOTHING; + } + case FINISH_THREE_DS_V1, FINISH_THREE_DS_V2, CANCEL, REFUND, CAPTURE: + return Step.DO_NOTHING; + default: + return step; + } + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/full/GenerateTokenFullThreeDsAllVersionsStepResolverImpl.java b/src/main/java/dev/vality/adapter/flow/lib/flow/full/GenerateTokenFullThreeDsAllVersionsStepResolverImpl.java new file mode 100644 index 0000000..59115ec --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/full/GenerateTokenFullThreeDsAllVersionsStepResolverImpl.java @@ -0,0 +1,43 @@ +package dev.vality.adapter.flow.lib.flow.full; + +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 GenerateTokenFullThreeDsAllVersionsStepResolverImpl extends + AbstractGenerateTokenStepResolver { + + @Override + public Step resolveNextStep(ExitStateModel exitStateModel) { + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + Step step = entryStateModel.getCurrentStep(); + switch (step) { + case AUTH, PAY: + if (ThreeDsBranchResolver.isRedirectForThreeDsV1(exitStateModel)) { + return Step.FINISH_THREE_DS_V1; + } else if (ThreeDsBranchResolver.isRedirectForThreeDsV2Simple(exitStateModel)) { + return Step.FINISH_THREE_DS_V2; + } else if (ThreeDsBranchResolver.isRedirectForThreeDsV2Full(exitStateModel)) { + return Step.CHECK_NEED_3DS_V2; + } else { + return Step.CAPTURE; + } + case CHECK_NEED_3DS_V2: + if (ThreeDsBranchResolver.isRedirectForThreeDsV2Full(exitStateModel)) { + return Step.FINISH_THREE_DS_V2; + } else { + return Step.CAPTURE; + } + case FINISH_THREE_DS_V1, FINISH_THREE_DS_V2: + return Step.CAPTURE; + case CAPTURE: + return Step.REFUND; + case REFUND: + return Step.DO_NOTHING; + default: + return step; + } + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/full/GenerateTokenResultIntentResolverImpl.java b/src/main/java/dev/vality/adapter/flow/lib/flow/full/GenerateTokenResultIntentResolverImpl.java new file mode 100644 index 0000000..f1f5db7 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/full/GenerateTokenResultIntentResolverImpl.java @@ -0,0 +1,27 @@ +package dev.vality.adapter.flow.lib.flow.full; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.flow.RecurrentResultIntentResolver; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.service.RecurrentIntentResultFactory; +import dev.vality.damsel.proxy_provider.RecurrentTokenIntent; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class GenerateTokenResultIntentResolverImpl implements RecurrentResultIntentResolver { + + private final RecurrentIntentResultFactory recurrentIntentResultFactory; + + @Override + public RecurrentTokenIntent initIntentByStep(ExitStateModel exitStateModel) { + Step nextStep = exitStateModel.getNextStep(); + return switch (nextStep) { + case REFUND, CAPTURE -> recurrentIntentResultFactory.createSleepIntentForReinvocation(); + case FINISH_THREE_DS_V1, FINISH_THREE_DS_V2 -> recurrentIntentResultFactory.createIntentWithSuspension( + exitStateModel); + case DO_NOTHING -> recurrentIntentResultFactory.createFinishIntent(exitStateModel.getRecToken()); + default -> throw new IllegalStateException("Wrong state: " + nextStep); + }; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/full/ResultIntentResolverImpl.java b/src/main/java/dev/vality/adapter/flow/lib/flow/full/ResultIntentResolverImpl.java new file mode 100644 index 0000000..d63c3ef --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/full/ResultIntentResolverImpl.java @@ -0,0 +1,42 @@ +package dev.vality.adapter.flow.lib.flow.full; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.flow.ResultIntentResolver; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.service.IntentResultFactory; +import dev.vality.damsel.proxy_provider.Intent; +import lombok.RequiredArgsConstructor; + +import static dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators.createFinishIntentSuccess; + +@RequiredArgsConstructor +public class ResultIntentResolverImpl implements ResultIntentResolver { + + private final IntentResultFactory intentResultFactory; + + @Override + public Intent initIntentByStep(ExitStateModel exitStateModel) { + Step nextStep = exitStateModel.getNextStep(); + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + Step currentStep = entryStateModel.getCurrentStep(); + return switch (nextStep) { + case FINISH_THREE_DS_V1, CHECK_NEED_3DS_V2, FINISH_THREE_DS_V2 -> intentResultFactory + .createSuspendIntentWithFailedAfterTimeout(exitStateModel); + case DO_NOTHING -> createIntentByCurrentStep(exitStateModel, currentStep); + case CAPTURE, REFUND, CANCEL -> createFinishIntentSuccess(); + default -> throw new IllegalStateException("Wrong nextStep: " + nextStep); + }; + } + + private Intent createIntentByCurrentStep(ExitStateModel exitStateModel, Step currentStep) { + return switch (currentStep) { + case CHECK_NEED_3DS_V2, FINISH_THREE_DS_V1, FINISH_THREE_DS_V2, + DO_NOTHING, PAY, AUTH, CAPTURE -> intentResultFactory + .createFinishIntentSuccessWithCheckToken(exitStateModel); + case REFUND, CANCEL -> createFinishIntentSuccess(); + default -> throw new IllegalStateException("Wrong currentStep: " + currentStep); + }; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/full/ThreeDsBranchResolver.java b/src/main/java/dev/vality/adapter/flow/lib/flow/full/ThreeDsBranchResolver.java new file mode 100644 index 0000000..c89e6f3 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/full/ThreeDsBranchResolver.java @@ -0,0 +1,30 @@ +package dev.vality.adapter.flow.lib.flow.full; + +import dev.vality.adapter.flow.lib.constant.Status; +import dev.vality.adapter.flow.lib.constant.ThreeDsType; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ThreeDsBranchResolver { + + public static boolean isRedirectForThreeDsV2Full(ExitStateModel exitStateModel) { + return isRedirectForThreeDsType(exitStateModel, ThreeDsType.V2_FULL); + } + + public static boolean isRedirectForThreeDsV2Simple(ExitStateModel exitStateModel) { + return isRedirectForThreeDsType(exitStateModel, ThreeDsType.V2_SIMPLE); + } + + public static boolean isRedirectForThreeDsV1(ExitStateModel exitStateModel) { + return isRedirectForThreeDsType(exitStateModel, ThreeDsType.V1); + } + + private static boolean isRedirectForThreeDsType(ExitStateModel exitStateModel, ThreeDsType threeDsType) { + return exitStateModel.getLastOperationStatus() == Status.NEED_REDIRECT + && exitStateModel.getThreeDsData() != null + && exitStateModel.getThreeDsData().getThreeDsType() == threeDsType; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/simple/GenerateTokenSimpleRedirectWithPollingStepResolverImpl.java b/src/main/java/dev/vality/adapter/flow/lib/flow/simple/GenerateTokenSimpleRedirectWithPollingStepResolverImpl.java new file mode 100644 index 0000000..1ac39d0 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/simple/GenerateTokenSimpleRedirectWithPollingStepResolverImpl.java @@ -0,0 +1,33 @@ +package dev.vality.adapter.flow.lib.flow.simple; + +import dev.vality.adapter.flow.lib.constant.Status; +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 GenerateTokenSimpleRedirectWithPollingStepResolverImpl extends + AbstractGenerateTokenStepResolver { + + @Override + public Step resolveCurrentStep(EntryStateModel stateModel) { + Step currentStep = stateModel.getCurrentStep(); + if (currentStep != null) { + return currentStep; + } + return Step.GENERATE_TOKEN; + } + + @Override + public Step resolveNextStep(ExitStateModel exitStateModel) { + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + Step step = entryStateModel.getCurrentStep(); + return switch (step) { + case GENERATE_TOKEN -> Step.CHECK_STATUS; + case CHECK_STATUS -> exitStateModel.getLastOperationStatus() == Status.NEED_RETRY + ? Step.CHECK_STATUS + : Step.DO_NOTHING; + default -> Step.DO_NOTHING; + }; + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectGenerateTokenResultIntentResolver.java b/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectGenerateTokenResultIntentResolver.java new file mode 100644 index 0000000..39528b2 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectGenerateTokenResultIntentResolver.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.flow.simple; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.flow.RecurrentResultIntentResolver; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.service.RecurrentIntentResultFactory; +import dev.vality.damsel.proxy_provider.RecurrentTokenIntent; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class SimpleRedirectGenerateTokenResultIntentResolver implements RecurrentResultIntentResolver { + + private final RecurrentIntentResultFactory recurrentIntentResultFactory; + + @Override + public RecurrentTokenIntent initIntentByStep(ExitStateModel exitStateModel) { + Step nextStep = exitStateModel.getNextStep(); + Step currentStep = exitStateModel.getEntryStateModel().getCurrentStep(); + return switch (nextStep) { + case CHECK_STATUS -> currentStep == Step.GENERATE_TOKEN + ? recurrentIntentResultFactory.createIntentWithSuspension(exitStateModel) + : recurrentIntentResultFactory.createSleepIntentWithExponentialPolling(exitStateModel); + case DO_NOTHING -> recurrentIntentResultFactory.createFinishIntent(exitStateModel.getRecToken()); + default -> throw new IllegalStateException("Wrong state: " + nextStep); + }; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectWIthPollingStepResolverImpl.java b/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectWIthPollingStepResolverImpl.java new file mode 100644 index 0000000..31fa585 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectWIthPollingStepResolverImpl.java @@ -0,0 +1,24 @@ +package dev.vality.adapter.flow.lib.flow.simple; + +import dev.vality.adapter.flow.lib.constant.Status; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.flow.AbstractPaymentStepResolver; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; + +public class SimpleRedirectWIthPollingStepResolverImpl + extends AbstractPaymentStepResolver { + + @Override + public Step resolveNextStep(ExitStateModel exitStateModel) { + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + Step step = entryStateModel.getCurrentStep(); + return switch (step) { + case AUTH, PAY, CAPTURE, REFUND, CANCEL -> Step.CHECK_STATUS; + case CHECK_STATUS -> exitStateModel.getLastOperationStatus() == Status.NEED_RETRY + ? Step.CHECK_STATUS + : Step.DO_NOTHING; + default -> step; + }; + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectWithPollingResultIntentResolver.java b/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectWithPollingResultIntentResolver.java new file mode 100644 index 0000000..eb33635 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/flow/simple/SimpleRedirectWithPollingResultIntentResolver.java @@ -0,0 +1,41 @@ +package dev.vality.adapter.flow.lib.flow.simple; + +import dev.vality.adapter.flow.lib.constant.Status; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.flow.ResultIntentResolver; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.service.IntentResultFactory; +import dev.vality.damsel.proxy_provider.Intent; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class SimpleRedirectWithPollingResultIntentResolver implements ResultIntentResolver { + + private final IntentResultFactory intentResultFactory; + + @Override + public Intent initIntentByStep(ExitStateModel exitStateModel) { + Step nextStep = exitStateModel.getNextStep(); + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + Step currentStep = entryStateModel.getCurrentStep(); + return switch (nextStep) { + case CHECK_STATUS -> exitStateModel.getLastOperationStatus() == Status.NEED_REDIRECT + ? intentResultFactory.createSuspendIntentWithCallbackAfterTimeout(exitStateModel) + : intentResultFactory.createSleepIntentWithExponentialPolling(exitStateModel); + case DO_NOTHING -> createIntentByCurrentStep(exitStateModel, currentStep); + case REFUND, CANCEL -> intentResultFactory.createFinishIntentSuccess(); + default -> throw new IllegalStateException("Wrong nextStep: " + nextStep); + }; + } + + private Intent createIntentByCurrentStep(ExitStateModel exitStateModel, Step currentStep) { + return switch (currentStep) { + case CHECK_STATUS, CHECK_NEED_3DS_V2, FINISH_THREE_DS_V1, FINISH_THREE_DS_V2, DO_NOTHING, + PAY, AUTH -> intentResultFactory.createFinishIntentSuccessWithCheckToken(exitStateModel); + case REFUND, CANCEL -> intentResultFactory.createFinishIntentSuccess(); + default -> throw new IllegalStateException("Wrong currentStep: " + currentStep); + }; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/CommonHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/CommonHandler.java new file mode 100644 index 0000000..69c702f --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/CommonHandler.java @@ -0,0 +1,11 @@ +package dev.vality.adapter.flow.lib.handler; + +import org.apache.thrift.TException; + +public interface CommonHandler { + + boolean isHandle(E model); + + T handle(E context) throws TException; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/CommonHandlerImpl.java b/src/main/java/dev/vality/adapter/flow/lib/handler/CommonHandlerImpl.java new file mode 100644 index 0000000..6b076f0 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/CommonHandlerImpl.java @@ -0,0 +1,25 @@ +package dev.vality.adapter.flow.lib.handler; + +import dev.vality.adapter.flow.lib.processor.Processor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; + +import java.util.function.Function; + +@Slf4j +@RequiredArgsConstructor +public abstract class CommonHandlerImpl implements CommonHandler { + + private final Function requestFunction; + private final Converter converter; + private final Processor processor; + + @Override + public T handle(E entryStateModel) { + P request = converter.convert(entryStateModel); + R response = requestFunction.apply(request); + return processor.process(response, entryStateModel); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/ProxyProviderServiceImpl.java b/src/main/java/dev/vality/adapter/flow/lib/handler/ProxyProviderServiceImpl.java new file mode 100644 index 0000000..52fe9aa --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/ProxyProviderServiceImpl.java @@ -0,0 +1,48 @@ +package dev.vality.adapter.flow.lib.handler; + +import dev.vality.adapter.flow.lib.handler.callback.CallbackHandler; +import dev.vality.adapter.flow.lib.validator.AdapterConfigurationValidator; +import dev.vality.damsel.proxy_provider.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; + +import java.nio.ByteBuffer; + +@Slf4j +@RequiredArgsConstructor +public class ProxyProviderServiceImpl implements ProviderProxySrv.Iface { + + private final CallbackHandler paymentCallbackHandler; + private final CallbackHandler recurrentTokenCallbackHandler; + private final ServerFlowHandler serverFlowHandler; + private final ServerFlowHandler generateTokenFlowHandler; + private final AdapterConfigurationValidator adapterConfigurationValidator; + + @Override + public RecurrentTokenProxyResult generateToken(RecurrentTokenContext context) throws TException { + adapterConfigurationValidator.validate(context.getOptions()); + return generateTokenFlowHandler.handle(context); + } + + @Override + public RecurrentTokenCallbackResult handleRecurrentTokenCallback( + ByteBuffer byteBuffer, + RecurrentTokenContext context + ) throws TException { + return recurrentTokenCallbackHandler.handleCallback(byteBuffer, context); + } + + @Override + public PaymentProxyResult processPayment(PaymentContext context) throws TException { + adapterConfigurationValidator.validate(context.getOptions()); + return serverFlowHandler.handle(context); + } + + @Override + public PaymentCallbackResult handlePaymentCallback(ByteBuffer byteBuffer, PaymentContext context) + throws TException { + return paymentCallbackHandler.handleCallback(byteBuffer, context); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/ServerFlowHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/ServerFlowHandler.java new file mode 100644 index 0000000..827234d --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/ServerFlowHandler.java @@ -0,0 +1,43 @@ +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 ServerFlowHandler { + + private final List> handlers; + private final StepResolver stepResolver; + private final Converter entryConverter; + private final Converter 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); + } + + private EntryStateModel prepareEntryState(Converter entryConverter, T context) { + EntryStateModel entryStateModel = entryConverter.convert(context); + entryStateModel.setCurrentStep(stepResolver.resolveCurrentStep(entryStateModel)); + return entryStateModel; + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/ServerHandlerLogDecorator.java b/src/main/java/dev/vality/adapter/flow/lib/handler/ServerHandlerLogDecorator.java new file mode 100644 index 0000000..4391715 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/ServerHandlerLogDecorator.java @@ -0,0 +1,83 @@ +package dev.vality.adapter.flow.lib.handler; + +import dev.vality.adapter.flow.lib.utils.PaymentResourceTypeResolver; +import dev.vality.damsel.proxy_provider.*; +import dev.vality.java.damsel.utils.extractors.ProxyProviderPackageExtractors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; + +import java.nio.ByteBuffer; + +import static dev.vality.java.damsel.utils.verification.ProxyProviderVerification.isUndefinedResultOrUnavailable; + +@Slf4j +@RequiredArgsConstructor +public class ServerHandlerLogDecorator implements ProviderProxySrv.Iface { + + private final ProviderProxySrv.Iface handler; + + @Override + public RecurrentTokenProxyResult generateToken(RecurrentTokenContext context) throws TException { + String recurrentId = ProxyProviderPackageExtractors.extractRecurrentId(context); + log.info("Generate token started with recurrentId {}", recurrentId); + try { + RecurrentTokenProxyResult proxyResult = handler.generateToken(context); + log.info("Generate token finished {} with recurrentId {}", proxyResult, recurrentId); + return proxyResult; + } catch (Exception ex) { + String message = "Failed handle generate token with recurrentId " + recurrentId; + logMessage(ex, message); + throw ex; + } + } + + @Override + public RecurrentTokenCallbackResult handleRecurrentTokenCallback(ByteBuffer byteBuffer, + RecurrentTokenContext context) throws TException { + String recurrentId = ProxyProviderPackageExtractors.extractRecurrentId(context); + log.info("handleRecurrentTokenCallback: start with recurrentId {}", recurrentId); + RecurrentTokenCallbackResult result = handler.handleRecurrentTokenCallback(byteBuffer, context); + log.info("handleRecurrentTokenCallback end {} with recurrentId {}", result, recurrentId); + return result; + } + + @Override + public PaymentProxyResult processPayment(PaymentContext context) throws TException { + String invoiceId = ProxyProviderPackageExtractors.extractInvoiceId(context); + String invoicePaymentStatus = ProxyProviderPackageExtractors.extractTargetInvoicePaymentStatus(context); + String paymentResourceType = PaymentResourceTypeResolver.extractPaymentResourceType(context); + log.info("Process payment handle resource='{}', status='{}' start with invoiceId {}", paymentResourceType, + invoicePaymentStatus, invoiceId); + try { + PaymentProxyResult proxyResult = handler.processPayment(context); + log.info("Process payment handle resource='{}', status='{}' finished with invoiceId {} and proxyResult {}", + paymentResourceType, invoicePaymentStatus, invoiceId, proxyResult); + return proxyResult; + } catch (Exception e) { + String message = String.format( + "Failed handle resource=%s, status=%s process payment for operation with invoiceId %s", + paymentResourceType, invoicePaymentStatus, invoiceId); + logMessage(e, message); + throw e; + } + } + + @Override + public PaymentCallbackResult handlePaymentCallback(ByteBuffer byteBuffer, + PaymentContext context) throws TException { + String invoiceId = ProxyProviderPackageExtractors.extractInvoiceId(context); + log.info("handlePaymentCallback start with invoiceId {}", invoiceId); + PaymentCallbackResult result = handler.handlePaymentCallback(byteBuffer, context); + log.info("handlePaymentCallback finish {} with invoiceId {}", result, invoiceId); + return result; + } + + private static void logMessage(Exception ex, String message) { + if (isUndefinedResultOrUnavailable(ex)) { + log.warn(message, ex); + } else { + log.error(message, ex); + } + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/ServerHandlerMdcDecorator.java b/src/main/java/dev/vality/adapter/flow/lib/handler/ServerHandlerMdcDecorator.java new file mode 100644 index 0000000..cb25faf --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/ServerHandlerMdcDecorator.java @@ -0,0 +1,58 @@ +package dev.vality.adapter.flow.lib.handler; + +import dev.vality.adapter.flow.lib.logback.mdc.MdcContext; +import dev.vality.damsel.proxy_provider.*; +import lombok.RequiredArgsConstructor; +import org.apache.thrift.TException; +import org.slf4j.MDC; + +import java.nio.ByteBuffer; + +@RequiredArgsConstructor +public class ServerHandlerMdcDecorator implements ProviderProxySrv.Iface { + + private final ProviderProxySrv.Iface serverHandlerLogDecorator; + + public RecurrentTokenProxyResult generateToken(RecurrentTokenContext recurrentTokenContext) throws TException { + MdcContext.mdcPutContext(recurrentTokenContext); + try { + return serverHandlerLogDecorator.generateToken(recurrentTokenContext); + } finally { + MDC.clear(); + } + } + + @Override + public RecurrentTokenCallbackResult handleRecurrentTokenCallback(ByteBuffer byteBuffer, + RecurrentTokenContext recurrentTokenContext) + throws TException { + MdcContext.mdcPutContext(recurrentTokenContext); + try { + return serverHandlerLogDecorator.handleRecurrentTokenCallback(byteBuffer, recurrentTokenContext); + } finally { + MDC.clear(); + } + } + + @Override + public PaymentProxyResult processPayment(PaymentContext paymentContext) throws TException { + MdcContext.mdcPutContext(paymentContext); + try { + return serverHandlerLogDecorator.processPayment(paymentContext); + } finally { + MDC.clear(); + } + } + + @Override + public PaymentCallbackResult handlePaymentCallback(ByteBuffer byteBuffer, PaymentContext paymentContext) + throws TException { + MdcContext.mdcPutContext(paymentContext); + try { + return serverHandlerLogDecorator.handlePaymentCallback(byteBuffer, paymentContext); + } finally { + MDC.clear(); + } + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/callback/CallbackHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/callback/CallbackHandler.java new file mode 100644 index 0000000..3e85900 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/callback/CallbackHandler.java @@ -0,0 +1,9 @@ +package dev.vality.adapter.flow.lib.handler.callback; + +import java.nio.ByteBuffer; + +public interface CallbackHandler { + + T handleCallback(ByteBuffer byteBuffer, R context); + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/callback/PaymentCallbackHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/callback/PaymentCallbackHandler.java new file mode 100644 index 0000000..bb147b2 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/callback/PaymentCallbackHandler.java @@ -0,0 +1,37 @@ +package dev.vality.adapter.flow.lib.handler.callback; + +import dev.vality.adapter.flow.lib.model.TemporaryContext; +import dev.vality.adapter.flow.lib.serde.Deserializer; +import dev.vality.adapter.flow.lib.serde.StateSerializer; +import dev.vality.adapter.flow.lib.service.TemporaryContextService; +import dev.vality.damsel.proxy_provider.PaymentCallbackProxyResult; +import dev.vality.damsel.proxy_provider.PaymentCallbackResult; +import dev.vality.damsel.proxy_provider.PaymentContext; +import dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; + +@Slf4j +@RequiredArgsConstructor +public class PaymentCallbackHandler implements CallbackHandler { + + private final Deserializer temporaryContextDeserializer; + private final StateSerializer adapterSerializer; + private final TemporaryContextService temporaryContextService; + + public PaymentCallbackResult handleCallback(ByteBuffer callback, PaymentContext context) { + var generalExitStateModel = initTemporaryContext(callback, context); + byte[] callbackResponse = new byte[0]; + return ProxyProviderPackageCreators.createCallbackResult(callbackResponse, (new PaymentCallbackProxyResult()) + .setIntent(ProxyProviderPackageCreators.createIntentWithSleepIntent(0)) + .setNextState(this.adapterSerializer.writeByte(generalExitStateModel))); + } + + private TemporaryContext initTemporaryContext(ByteBuffer callback, PaymentContext context) { + var temporaryContext = temporaryContextService.getTemporaryContext(context, this.temporaryContextDeserializer); + return temporaryContextService.appendThreeDsParametersToContext(callback, temporaryContext); + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/callback/RecurrentTokenCallbackHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/callback/RecurrentTokenCallbackHandler.java new file mode 100644 index 0000000..81e156d --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/callback/RecurrentTokenCallbackHandler.java @@ -0,0 +1,43 @@ +package dev.vality.adapter.flow.lib.handler.callback; + +import dev.vality.adapter.flow.lib.model.TemporaryContext; +import dev.vality.adapter.flow.lib.serde.Deserializer; +import dev.vality.adapter.flow.lib.serde.StateSerializer; +import dev.vality.adapter.flow.lib.service.TemporaryContextService; +import dev.vality.damsel.proxy_provider.RecurrentTokenCallbackResult; +import dev.vality.damsel.proxy_provider.RecurrentTokenContext; +import dev.vality.damsel.proxy_provider.RecurrentTokenIntent; +import dev.vality.damsel.proxy_provider.RecurrentTokenProxyResult; +import dev.vality.java.damsel.utils.creators.BasePackageCreators; +import dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; + +@Slf4j +@RequiredArgsConstructor +public class RecurrentTokenCallbackHandler + implements CallbackHandler { + + private final Deserializer adapterDeserializer; + private final StateSerializer adapterSerializer; + private final TemporaryContextService temporaryContextService; + + public RecurrentTokenCallbackResult handleCallback(ByteBuffer callback, RecurrentTokenContext context) { + var generalExitStateModel = initTemporaryContext(callback, context); + byte[] callbackResponse = new byte[0]; + return ProxyProviderPackageCreators.createRecurrentTokenCallbackResult(callbackResponse, + (new RecurrentTokenProxyResult()).setIntent(RecurrentTokenIntent + .sleep(ProxyProviderPackageCreators.createSleepIntent( + BasePackageCreators.createTimerTimeout(0)))) + .setNextState(this.adapterSerializer.writeByte(generalExitStateModel)) + ); + } + + private TemporaryContext initTemporaryContext(ByteBuffer callback, RecurrentTokenContext context) { + var temporaryContext = temporaryContextService.getTemporaryContext(context, this.adapterDeserializer); + return temporaryContextService.appendThreeDsParametersToContext(callback, temporaryContext); + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/AuthHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/AuthHandler.java new file mode 100644 index 0000000..a3b4a6c --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/AuthHandler.java @@ -0,0 +1,29 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + + +public class AuthHandler + extends CommonHandlerImpl { + + public AuthHandler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::auth, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.AUTH == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/CancelHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/CancelHandler.java new file mode 100644 index 0000000..7ff3765 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/CancelHandler.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + +public class CancelHandler + extends CommonHandlerImpl { + + public CancelHandler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::cancel, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.CANCEL == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/CaptureHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/CaptureHandler.java new file mode 100644 index 0000000..b03c620 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/CaptureHandler.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + +public class CaptureHandler + extends CommonHandlerImpl { + + public CaptureHandler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::capture, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.CAPTURE == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Check3dsV2Handler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Check3dsV2Handler.java new file mode 100644 index 0000000..4e78830 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Check3dsV2Handler.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + +public class Check3dsV2Handler + extends CommonHandlerImpl { + + public Check3dsV2Handler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::check3dsV2, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.CHECK_NEED_3DS_V2 == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/DoNothingHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/DoNothingHandler.java new file mode 100644 index 0000000..b5ecc39 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/DoNothingHandler.java @@ -0,0 +1,30 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandler; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import org.apache.thrift.TException; + + +public class DoNothingHandler implements CommonHandler { + + @Override + public ExitStateModel handle(EntryStateModel entryStateModel) throws TException { + var exitStateModel = new ExitStateModel(); + exitStateModel.setEntryStateModel(entryStateModel); + exitStateModel.setProviderTrxId(entryStateModel.getBaseRequestModel().getProviderTrxId()); + exitStateModel.setNextStep(Step.DO_NOTHING); + exitStateModel.setTrxExtra(entryStateModel.getBaseRequestModel().getSavedData()); + if (entryStateModel.getBaseRequestModel().getRecurrentPaymentData().isMakeRecurrent()) { + exitStateModel.setRecToken(entryStateModel.getBaseRequestModel().getRecurrentPaymentData().getRecToken()); + } + return exitStateModel; + } + + @Override + public boolean isHandle(EntryStateModel model) { + return model.getCurrentStep() == Step.DO_NOTHING; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Finish3dsHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Finish3dsHandler.java new file mode 100644 index 0000000..4946934 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Finish3dsHandler.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + +public class Finish3dsHandler + extends CommonHandlerImpl { + + public Finish3dsHandler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::finish3ds, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.FINISH_THREE_DS_V1 == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Finish3dsV2Handler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Finish3dsV2Handler.java new file mode 100644 index 0000000..0d2a670 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/Finish3dsV2Handler.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + +public class Finish3dsV2Handler + extends CommonHandlerImpl { + + public Finish3dsV2Handler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::finish3dsV2, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.FINISH_THREE_DS_V2 == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/GenerateTokenHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/GenerateTokenHandler.java new file mode 100644 index 0000000..d8c7236 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/GenerateTokenHandler.java @@ -0,0 +1,29 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + + +public class GenerateTokenHandler + extends CommonHandlerImpl { + + public GenerateTokenHandler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::generateToken, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.GENERATE_TOKEN == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/PaymentHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/PaymentHandler.java new file mode 100644 index 0000000..df77e17 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/PaymentHandler.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + +public class PaymentHandler + extends CommonHandlerImpl { + + public PaymentHandler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::pay, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.PAY == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/RefundHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/RefundHandler.java new file mode 100644 index 0000000..6c98b12 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/RefundHandler.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + +public class RefundHandler + extends CommonHandlerImpl { + + public RefundHandler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::refund, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.REFUND == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/handler/payment/StatusHandler.java b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/StatusHandler.java new file mode 100644 index 0000000..12a99cc --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/handler/payment/StatusHandler.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.handler.payment; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.handler.CommonHandlerImpl; +import dev.vality.adapter.flow.lib.model.BaseRequestModel; +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.Processor; +import org.springframework.core.convert.converter.Converter; + +public class StatusHandler + extends CommonHandlerImpl { + + public StatusHandler( + RemoteClient client, + Converter converter, + Processor responseProcessorChain + ) { + super(client::status, converter, responseProcessorChain); + } + + @Override + public boolean isHandle(EntryStateModel entryStateModel) { + return Step.CHECK_STATUS == entryStateModel.getCurrentStep(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/logback/mask/MaskedEvent.java b/src/main/java/dev/vality/adapter/flow/lib/logback/mask/MaskedEvent.java new file mode 100644 index 0000000..aa6f520 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/logback/mask/MaskedEvent.java @@ -0,0 +1,92 @@ +package dev.vality.adapter.flow.lib.logback.mask; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.LoggerContextVO; +import lombok.RequiredArgsConstructor; +import org.slf4j.Marker; + +import java.util.Map; + +@RequiredArgsConstructor +public class MaskedEvent implements ILoggingEvent { + + private final ILoggingEvent event; + private final String message; + + @Override + public String getThreadName() { + return event.getThreadName(); + } + + @Override + public Level getLevel() { + return event.getLevel(); + } + + @Override + public String getMessage() { + return event.getMessage(); + } + + @Override + public Object[] getArgumentArray() { + return new Object[0]; + } + + @Override + public String getFormattedMessage() { + return message; + } + + @Override + public String getLoggerName() { + return event.getLoggerName(); + } + + @Override + public LoggerContextVO getLoggerContextVO() { + return event.getLoggerContextVO(); + } + + @Override + public IThrowableProxy getThrowableProxy() { + return event.getThrowableProxy(); + } + + @Override + public StackTraceElement[] getCallerData() { + return new StackTraceElement[0]; + } + + @Override + public boolean hasCallerData() { + return true; + } + + @Override + public Marker getMarker() { + return event.getMarker(); + } + + @Override + public Map getMDCPropertyMap() { + return event.getMDCPropertyMap(); + } + + @Override + public Map getMdc() { + return event.getMDCPropertyMap(); + } + + @Override + public long getTimeStamp() { + return event.getTimeStamp(); + } + + @Override + public void prepareForDeferredProcessing() { + // Do nothing + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/logback/mask/MaskingMessageWithPattern.java b/src/main/java/dev/vality/adapter/flow/lib/logback/mask/MaskingMessageWithPattern.java new file mode 100644 index 0000000..225aeca --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/logback/mask/MaskingMessageWithPattern.java @@ -0,0 +1,29 @@ +package dev.vality.adapter.flow.lib.logback.mask; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MaskingMessageWithPattern { + + public static String maskMessage(String message, Pattern multilinePattern) { + if (multilinePattern == null) { + return message; + } + StringBuilder sb = new StringBuilder(message); + Matcher matcher = multilinePattern.matcher(sb); + while (matcher.find()) { + IntStream.rangeClosed(1, matcher.groupCount()) + .filter(group -> matcher.group(group) != null) + .forEach( + group -> IntStream.range(matcher.start(group), matcher.end(group)) + .forEach(i -> sb.setCharAt(i, '*')) + ); + } + return sb.toString(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/logback/mask/PatternMaskingLayout.java b/src/main/java/dev/vality/adapter/flow/lib/logback/mask/PatternMaskingLayout.java new file mode 100644 index 0000000..f121483 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/logback/mask/PatternMaskingLayout.java @@ -0,0 +1,26 @@ +package dev.vality.adapter.flow.lib.logback.mask; + +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.spi.ILoggingEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class PatternMaskingLayout extends PatternLayout { + + private Pattern multilinePattern; + private List maskPatterns = new ArrayList<>(); + private static final String DELIMITER = "|"; + + public void addMaskPattern(String maskPattern) { + maskPatterns.add(maskPattern); + multilinePattern = Pattern.compile(String.join(DELIMITER, maskPatterns), Pattern.MULTILINE); + } + + @Override + public String doLayout(ILoggingEvent event) { + return super.doLayout(new MaskedEvent(event, + MaskingMessageWithPattern.maskMessage(event.getFormattedMessage(), multilinePattern))); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/logback/mask/PatternMaskingMessageJsonProvider.java b/src/main/java/dev/vality/adapter/flow/lib/logback/mask/PatternMaskingMessageJsonProvider.java new file mode 100644 index 0000000..5ef8f96 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/logback/mask/PatternMaskingMessageJsonProvider.java @@ -0,0 +1,42 @@ +package dev.vality.adapter.flow.lib.logback.mask; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.fasterxml.jackson.core.JsonGenerator; +import net.logstash.logback.composite.AbstractFieldJsonProvider; +import net.logstash.logback.composite.FieldNamesAware; +import net.logstash.logback.composite.JsonWritingUtils; +import net.logstash.logback.fieldnames.LogstashFieldNames; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class PatternMaskingMessageJsonProvider extends AbstractFieldJsonProvider + implements FieldNamesAware { + private Pattern multilinePattern; + private List maskPatterns = new ArrayList<>(); + private static final String FIELD_MESSAGE = "message"; + private static final String DELIMITER = "|"; + + public void addMaskPattern(String maskPattern) { + maskPatterns.add(maskPattern); + multilinePattern = Pattern.compile(String.join(DELIMITER, maskPatterns), Pattern.MULTILINE); + } + + public PatternMaskingMessageJsonProvider() { + setFieldName(FIELD_MESSAGE); + } + + @Override + public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException { + JsonWritingUtils.writeStringField(generator, getFieldName(), + MaskingMessageWithPattern.maskMessage(event.getFormattedMessage(), multilinePattern) + ); + } + + @Override + public void setFieldNames(LogstashFieldNames fieldNames) { + setFieldName(fieldNames.getMessage()); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/logback/mdc/MdcContext.java b/src/main/java/dev/vality/adapter/flow/lib/logback/mdc/MdcContext.java new file mode 100644 index 0000000..e121807 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/logback/mdc/MdcContext.java @@ -0,0 +1,63 @@ +package dev.vality.adapter.flow.lib.logback.mdc; + +import dev.vality.damsel.domain.TransactionInfo; +import dev.vality.damsel.proxy_provider.PaymentContext; +import dev.vality.damsel.proxy_provider.RecurrentTokenContext; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.slf4j.MDC; + +import java.util.Map; +import java.util.regex.Pattern; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MdcContext { + + public static void mdcPutContext(RecurrentTokenContext context, String[] fieldsToPutInMdc) { + TransactionInfo transactionInfo = context.getTokenInfo().getTrx(); + mdcPutContextTransactionInfo(transactionInfo, fieldsToPutInMdc); + } + + public static void mdcPutContext(PaymentContext context, String[] fieldsToPutInMdc) { + TransactionInfo transactionInfo = context.getPaymentInfo().getPayment().getTrx(); + mdcPutContextTransactionInfo(transactionInfo, fieldsToPutInMdc); + } + + public static void mdcPutContext(PaymentContext context) { + TransactionInfo transactionInfo = context.getPaymentInfo().getPayment().getTrx(); + mdcPutContextTransactionInfo(transactionInfo); + } + + public static void mdcPutContext(RecurrentTokenContext context) { + TransactionInfo transactionInfo = context.getTokenInfo().getTrx(); + mdcPutContextTransactionInfo(transactionInfo); + } + + public static void mdcPutContextTransactionInfo(TransactionInfo transactionInfo, String[] fieldsToPutInMdc) { + if (transactionInfo != null) { + Map trxextra = transactionInfo.getExtra(); + for (String field : fieldsToPutInMdc) { + MDC.put(field, trxextra.get(field)); + } + } + } + + public static void mdcPutContextTransactionInfo(TransactionInfo transactionInfo) { + if (transactionInfo != null) { + Map trxextra = transactionInfo.getExtra(); + String maskPattern = "\\b\\d{6}([\\d\\s]{2,9})\\d{4}\\b"; + Pattern pattern = Pattern.compile(maskPattern); + for (Map.Entry extra : trxextra.entrySet()) { + if (pattern.matcher(extra.getValue()).find()) { + MDC.put(extra.getKey(), extra.getValue()); + } + } + } + } + + public static void mdcRemoveContext(String[] fieldsToPutInMdc) { + for (String field : fieldsToPutInMdc) { + MDC.remove(field); + } + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/BaseRequestModel.java b/src/main/java/dev/vality/adapter/flow/lib/model/BaseRequestModel.java new file mode 100644 index 0000000..1384857 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/BaseRequestModel.java @@ -0,0 +1,89 @@ +package dev.vality.adapter.flow.lib.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Map; + +/** + * Base class with all needed fields for pay and other operations + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class BaseRequestModel { + + /** + * Uniq Long identifier for payment. + */ + private Long paymentId; + /** + * Uniq identifier from provider (if exists), not come in first methods. + */ + private String providerTrxId; + /** + * Amount in Long format and in minimum payment units (example: cents). + */ + private Long amount; + /** + * Currency in symbolic formats (example: "USD"). + */ + private String currency; + /** + * Timestamp RFC 3339. + * + * The string must contain the date and time in UTC in the following format: + * `2016-03-22T06:12:27Z` + */ + private String createdAt; + /** + * Description payment from merchant. + */ + private String details; + /** + * Card data, can be empty on some steps and recurrent or mobile operations. + */ + private CardData cardData; + /** + * Info about payer, send from merchant and can be empty. + */ + private PayerInfo payerInfo; + /** + * Data for mobile pay methods. + */ + private MobilePaymentData mobilePaymentData; + /** + * Data for recurrent payments. + */ + private RecurrentPaymentData recurrentPaymentData; + /** + * Full needed for refund data. + */ + private RefundData refundData; + /** + * The URL to which to redirect if the 3ds stream completed successfully. + */ + private String successRedirectUrl; + /** + * The URL to which to redirect if the 3ds stream completed fails. + */ + private String failedRedirectUrl; + /** + * All static parameters from configuration by support team. + * (options - in old naming) + */ + private Map adapterConfigurations; + /** + * Data that you send to save on previous steps. + */ + private Map savedData; + /** + * Data that comes in a callback from mpi after the 3ds step. + * Parameter names can be unique to your implementation, only you can know. + */ + private Map threeDsDataFromMpiCallback; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/BaseResponseModel.java b/src/main/java/dev/vality/adapter/flow/lib/model/BaseResponseModel.java new file mode 100644 index 0000000..14ff6c9 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/BaseResponseModel.java @@ -0,0 +1,42 @@ +package dev.vality.adapter.flow.lib.model; + +import dev.vality.adapter.flow.lib.constant.Status; +import lombok.Data; +import lombok.experimental.SuperBuilder; + +import java.util.Map; + +@Data +@SuperBuilder +public class BaseResponseModel { + + /** + * Result status operation, by this status flow choose stop, retry or redirect branch. + */ + private Status status; + /** + * Error code from external system or your implementation + */ + private String errorCode; + /** + * Error description + */ + private String errorMessage; + /** + * Uniq identifier from provider (if exists). + */ + private String providerTrxId; + /** + * Token for pay in recurrent operations. + */ + private String recurrentToken; + /** + * Data that you want to use in next steps. + */ + private Map saveData; + /** + * Data for choose 3ds flow and parameters for redirects. + */ + private ThreeDsData threeDsData; + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/CardData.java b/src/main/java/dev/vality/adapter/flow/lib/model/CardData.java new file mode 100644 index 0000000..2dc548e --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/CardData.java @@ -0,0 +1,26 @@ +package dev.vality.adapter.flow.lib.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class CardData { + + @ToString.Exclude + private String pan; + @ToString.Exclude + private Byte expMonth; + @ToString.Exclude + private Short expYear; + @ToString.Exclude + private String cvv2; + @ToString.Exclude + private String cardHolder; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/EntryStateModel.java b/src/main/java/dev/vality/adapter/flow/lib/model/EntryStateModel.java new file mode 100644 index 0000000..3e79c3e --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/EntryStateModel.java @@ -0,0 +1,22 @@ +package dev.vality.adapter.flow.lib.model; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.constant.TargetStatus; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class EntryStateModel { + + private BaseRequestModel baseRequestModel; + + private Step currentStep; + private PollingInfo startedPollingInfo; + private TargetStatus targetStatus; + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/ExitStateModel.java b/src/main/java/dev/vality/adapter/flow/lib/model/ExitStateModel.java new file mode 100644 index 0000000..d0990d9 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/ExitStateModel.java @@ -0,0 +1,33 @@ +package dev.vality.adapter.flow.lib.model; + +import dev.vality.adapter.flow.lib.constant.Status; +import dev.vality.adapter.flow.lib.constant.Step; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Map; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class ExitStateModel { + + private Status lastOperationStatus; + + private String errorCode; + private String errorMessage; + private Step nextStep; + private EntryStateModel entryStateModel; + + private String providerTrxId; + private Map trxExtra; + private PollingInfo pollingInfo; + private ThreeDsData threeDsData; + + private String recToken; + + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/MobilePaymentData.java b/src/main/java/dev/vality/adapter/flow/lib/model/MobilePaymentData.java new file mode 100644 index 0000000..799b754 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/MobilePaymentData.java @@ -0,0 +1,18 @@ +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 MobilePaymentData { + + private String tokenProvider; + private String cryptogram; + private String eci; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/PayerInfo.java b/src/main/java/dev/vality/adapter/flow/lib/model/PayerInfo.java new file mode 100644 index 0000000..cfb67d9 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/PayerInfo.java @@ -0,0 +1,18 @@ +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 PayerInfo { + + private String ip; + private String email; + private String phone; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/PollingInfo.java b/src/main/java/dev/vality/adapter/flow/lib/model/PollingInfo.java new file mode 100644 index 0000000..837692b --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/PollingInfo.java @@ -0,0 +1,21 @@ +package dev.vality.adapter.flow.lib.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.time.Instant; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class PollingInfo { + + @JsonProperty(value = "start_date_time_polling") + private Instant startDateTimePolling; + + @JsonProperty(value = "max_date_time_polling") + private Instant maxDateTimePolling; + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/RecurrentPaymentData.java b/src/main/java/dev/vality/adapter/flow/lib/model/RecurrentPaymentData.java new file mode 100644 index 0000000..7dd17c8 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/RecurrentPaymentData.java @@ -0,0 +1,17 @@ +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 RecurrentPaymentData { + + private String recToken; + private boolean makeRecurrent; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/RefundData.java b/src/main/java/dev/vality/adapter/flow/lib/model/RefundData.java new file mode 100644 index 0000000..1a76077 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/RefundData.java @@ -0,0 +1,17 @@ +package dev.vality.adapter.flow.lib.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefundData { + + private Long amount; + private String id; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/TemporaryContext.java b/src/main/java/dev/vality/adapter/flow/lib/model/TemporaryContext.java new file mode 100644 index 0000000..2da377c --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/TemporaryContext.java @@ -0,0 +1,22 @@ +package dev.vality.adapter.flow.lib.model; + +import dev.vality.adapter.flow.lib.constant.Step; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Map; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class TemporaryContext { + + private Step nextStep; + private String providerTrxId; + private PollingInfo pollingInfo; + private Map threeDsData; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/model/ThreeDsData.java b/src/main/java/dev/vality/adapter/flow/lib/model/ThreeDsData.java new file mode 100644 index 0000000..17817fb --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/model/ThreeDsData.java @@ -0,0 +1,31 @@ +package dev.vality.adapter.flow.lib.model; + +import dev.vality.adapter.flow.lib.constant.ThreeDsType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Map; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class ThreeDsData { + + /** + * Use type to select desired 3ds flow. + */ + private ThreeDsType threeDsType; + /** + * Url for redirect user. + */ + private String acsUrl; + /** + * Parameters that would send in redirect request. + * Name and value of parameters don't change and forward send in your format. + */ + private Map parameters; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/processor/ErrorProcessor.java b/src/main/java/dev/vality/adapter/flow/lib/processor/ErrorProcessor.java new file mode 100644 index 0000000..7788c9a --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/processor/ErrorProcessor.java @@ -0,0 +1,34 @@ +package dev.vality.adapter.flow.lib.processor; + +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 ErrorProcessor implements Processor { + + @Override + public ExitStateModel process(BaseResponseModel response, EntryStateModel entryStateModel) { + log.debug("Start error process response: {} entryStateModel: {}", response, entryStateModel); + + if (response != null && ErrorUtils.isError(response)) { + ExitStateModel exitStateModel = new ExitStateModel(); + exitStateModel.setErrorCode(String.valueOf(response.getErrorCode())); + exitStateModel.setErrorMessage(response.getErrorMessage()); + log.debug("Finish error process response: {} entryStateModel: {}", response, entryStateModel); + return exitStateModel; + } + + ExitStateModel exitStateModel = new ExitStateModel(); + exitStateModel.setErrorCode("unknown_error"); + exitStateModel.setErrorMessage("Unknown error reason!"); + exitStateModel.setEntryStateModel(entryStateModel); + + log.debug("Finish error process response: {} entryStateModel: {}", response, entryStateModel); + return exitStateModel; + } +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/processor/Processor.java b/src/main/java/dev/vality/adapter/flow/lib/processor/Processor.java new file mode 100644 index 0000000..b2e1c01 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/processor/Processor.java @@ -0,0 +1,7 @@ +package dev.vality.adapter.flow.lib.processor; + +public interface Processor { + + R process(T response, E context); + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/processor/RedirectProcessor.java b/src/main/java/dev/vality/adapter/flow/lib/processor/RedirectProcessor.java new file mode 100644 index 0000000..c662cd5 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/processor/RedirectProcessor.java @@ -0,0 +1,38 @@ +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 RedirectProcessor implements Processor { + + private final Processor nextProcessor; + + @Override + public ExitStateModel process(BaseResponseModel response, EntryStateModel entryStateModel) { + if (response.getStatus() == Status.NEED_REDIRECT + && !ErrorUtils.isError(response) + && response.getThreeDsData() != null + && response.getThreeDsData().getThreeDsType() != null) { + log.debug("Start redirect process response: {} entryStateModel: {}", response, entryStateModel); + ExitStateModel exitStateModel = new ExitStateModel(); + exitStateModel.setThreeDsData(response.getThreeDsData()); + exitStateModel.setLastOperationStatus(response.getStatus()); + exitStateModel.setProviderTrxId(response.getProviderTrxId()); + log.debug("Finish redirect 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); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/processor/RetryProcessor.java b/src/main/java/dev/vality/adapter/flow/lib/processor/RetryProcessor.java new file mode 100644 index 0000000..3109844 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/processor/RetryProcessor.java @@ -0,0 +1,34 @@ +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 RetryProcessor implements Processor { + + private final Processor nextProcessor; + + @Override + public ExitStateModel process(BaseResponseModel response, EntryStateModel entryStateModel) { + if (response.getStatus() == Status.NEED_RETRY + && !ErrorUtils.isError(response)) { + log.debug("Start redirect process response: {} entryStateModel: {}", response, entryStateModel); + ExitStateModel exitStateModel = new ExitStateModel(); + exitStateModel.setLastOperationStatus(response.getStatus()); + log.debug("Finish redirect 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); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/processor/SuccessFinishProcessor.java b/src/main/java/dev/vality/adapter/flow/lib/processor/SuccessFinishProcessor.java new file mode 100644 index 0000000..dff3506 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/processor/SuccessFinishProcessor.java @@ -0,0 +1,63 @@ +package dev.vality.adapter.flow.lib.processor; + +import dev.vality.adapter.flow.lib.constant.MetaData; +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; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +public class SuccessFinishProcessor + implements Processor { + + private final Processor nextProcessor; + + @Override + public ExitStateModel process(BaseResponseModel response, EntryStateModel entryStateModel) { + if (response.getStatus() == Status.SUCCESS + && !ErrorUtils.isError(response)) { + log.debug("Start success process response: {} entryStateModel: {}", response, entryStateModel); + ExitStateModel exitStateModel = new ExitStateModel(); + exitStateModel.setProviderTrxId(response.getProviderTrxId()); + exitStateModel.setLastOperationStatus(response.getStatus()); + Map saveData = response.getSaveData(); + if (entryStateModel.getBaseRequestModel().getRecurrentPaymentData() != null + && entryStateModel.getBaseRequestModel().getRecurrentPaymentData().isMakeRecurrent()) { + if (saveData == null) { + saveData = new HashMap<>(); + } + String recToken = initRecurrentToken(response, entryStateModel); + saveData.put(MetaData.META_REC_TOKEN, recToken); + exitStateModel.setRecToken(recToken); + } + exitStateModel.setTrxExtra(saveData); + log.debug("Finish success 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); + } + + private String initRecurrentToken(BaseResponseModel response, EntryStateModel entryStateModel) { + if (StringUtils.hasText(response.getRecurrentToken())) { + return response.getRecurrentToken(); + } else if (entryStateModel.getBaseRequestModel().getRecurrentPaymentData() != null + && StringUtils.hasText(entryStateModel.getBaseRequestModel().getRecurrentPaymentData() + .getRecToken())) { + return entryStateModel.getBaseRequestModel().getRecurrentPaymentData().getRecToken(); + } + return null; + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/serde/Deserializer.java b/src/main/java/dev/vality/adapter/flow/lib/serde/Deserializer.java new file mode 100644 index 0000000..296541b --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/serde/Deserializer.java @@ -0,0 +1,9 @@ +package dev.vality.adapter.flow.lib.serde; + +public interface Deserializer { + + T read(byte[] data); + + T read(String data); + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/serde/ParametersDeserializer.java b/src/main/java/dev/vality/adapter/flow/lib/serde/ParametersDeserializer.java new file mode 100644 index 0000000..f780870 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/serde/ParametersDeserializer.java @@ -0,0 +1,45 @@ +package dev.vality.adapter.flow.lib.serde; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.adapter.flow.lib.exception.DeserializationException; +import lombok.RequiredArgsConstructor; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +public class ParametersDeserializer implements Deserializer> { + + private final ObjectMapper mapper; + + public Map read(byte[] data) { + if (data == null) { + return new HashMap<>(); + } else { + try { + return mapper.readValue(data, new TypeReference>() { + }); + } catch (IOException var3) { + throw new IllegalArgumentException(var3); + } + } + } + + public Map read(String data) { + throw new DeserializationException("Deserialization not supported"); + } + + public Map read(HttpServletRequest request) { + return Optional.ofNullable(request.getParameterMap()) + .orElseGet(HashMap::new) + .entrySet().stream() + .collect(Collectors.toMap(k -> k.getKey().trim(), + v -> v.getValue()[0])); + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/serde/ParametersSerializer.java b/src/main/java/dev/vality/adapter/flow/lib/serde/ParametersSerializer.java new file mode 100644 index 0000000..9e036de --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/serde/ParametersSerializer.java @@ -0,0 +1,11 @@ +package dev.vality.adapter.flow.lib.serde; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Map; + +public class ParametersSerializer extends StateSerializer> { + public ParametersSerializer(ObjectMapper mapper) { + super(mapper); + } +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/serde/Serializer.java b/src/main/java/dev/vality/adapter/flow/lib/serde/Serializer.java new file mode 100644 index 0000000..32572ed --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/serde/Serializer.java @@ -0,0 +1,9 @@ +package dev.vality.adapter.flow.lib.serde; + +public interface Serializer { + + byte[] writeByte(T obj); + + String writeString(T obj); + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/serde/StateSerializer.java b/src/main/java/dev/vality/adapter/flow/lib/serde/StateSerializer.java new file mode 100644 index 0000000..f7a9c01 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/serde/StateSerializer.java @@ -0,0 +1,37 @@ +package dev.vality.adapter.flow.lib.serde; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.SerializationException; + +import java.io.IOException; +import java.util.Base64; + +@Getter +@Setter +@AllArgsConstructor +public abstract class StateSerializer implements Serializer { + + protected final ObjectMapper mapper; + + @Override + public byte[] writeByte(Object obj) { + try { + return mapper.writeValueAsBytes(obj); + } catch (IOException e) { + throw new SerializationException(e); + } + } + + @Override + public String writeString(Object obj) { + try { + return Base64.getEncoder().encodeToString(getMapper().writeValueAsBytes(obj)); + } catch (IOException e) { + throw new SerializationException(e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/serde/TemporaryContextDeserializer.java b/src/main/java/dev/vality/adapter/flow/lib/serde/TemporaryContextDeserializer.java new file mode 100644 index 0000000..667f944 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/serde/TemporaryContextDeserializer.java @@ -0,0 +1,37 @@ +package dev.vality.adapter.flow.lib.serde; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.adapter.flow.lib.exception.DeserializationException; +import dev.vality.adapter.flow.lib.model.TemporaryContext; + +import java.io.IOException; + +public class TemporaryContextDeserializer implements Deserializer { + + private final ObjectMapper mapper; + + public TemporaryContext read(byte[] data) { + if (data == null) { + return new TemporaryContext(); + } else { + try { + return this.getMapper().readValue(data, TemporaryContext.class); + } catch (IOException var3) { + throw new IllegalArgumentException(var3); + } + } + } + + public TemporaryContext read(String data) { + throw new DeserializationException("Deserialization not supported"); + } + + public ObjectMapper getMapper() { + return this.mapper; + } + + public TemporaryContextDeserializer(ObjectMapper mapper) { + this.mapper = mapper; + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/serde/TemporaryContextSerializer.java b/src/main/java/dev/vality/adapter/flow/lib/serde/TemporaryContextSerializer.java new file mode 100644 index 0000000..c3d5b96 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/serde/TemporaryContextSerializer.java @@ -0,0 +1,10 @@ +package dev.vality.adapter.flow.lib.serde; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.adapter.flow.lib.model.TemporaryContext; + +public class TemporaryContextSerializer extends StateSerializer { + public TemporaryContextSerializer(ObjectMapper mapper) { + super(mapper); + } +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/service/ExponentialBackOffPollingService.java b/src/main/java/dev/vality/adapter/flow/lib/service/ExponentialBackOffPollingService.java new file mode 100644 index 0000000..78fb848 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/service/ExponentialBackOffPollingService.java @@ -0,0 +1,46 @@ +package dev.vality.adapter.flow.lib.service; + +import dev.vality.adapter.flow.lib.model.PollingInfo; +import dev.vality.adapter.flow.lib.utils.backoff.BackOffExecution; +import dev.vality.adapter.flow.lib.utils.backoff.ExponentialBackOff; +import dev.vality.adapter.flow.lib.utils.backoff.TimeOptionsExtractors; + +import java.time.Instant; +import java.util.Map; + +import static dev.vality.adapter.flow.lib.utils.backoff.ExponentialBackOff.*; + + +public class ExponentialBackOffPollingService { + + public BackOffExecution prepareBackOffExecution(PollingInfo pollingInfo, Map options) { + return exponentialBackOff(pollingInfo, options) + .start(); + } + + public int prepareNextPollingInterval(PollingInfo pollingInfo, Map options) { + return exponentialBackOff(pollingInfo, options) + .start() + .nextBackOff() + .intValue(); + } + + private ExponentialBackOff exponentialBackOff(PollingInfo pollingInfo, Map options) { + final Long currentLocalTime = Instant.now().toEpochMilli(); + + Long startTime = pollingInfo.getStartDateTimePolling() != null + ? pollingInfo.getStartDateTimePolling().toEpochMilli() + : currentLocalTime; + Integer exponential = TimeOptionsExtractors.extractExponent(options, DEFAULT_MUTIPLIER); + Integer defaultInitialExponential = + TimeOptionsExtractors.extractDefaultInitialExponential(options, DEFAULT_INITIAL_INTERVAL_SEC); + Integer maxTimeBackOff = TimeOptionsExtractors.extractMaxTimeBackOff(options, DEFAULT_MAX_INTERVAL_SEC); + + return new ExponentialBackOff( + startTime, + currentLocalTime, + exponential, + defaultInitialExponential, + maxTimeBackOff); + } +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/service/IdGenerator.java b/src/main/java/dev/vality/adapter/flow/lib/service/IdGenerator.java new file mode 100644 index 0000000..b9f2ff7 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/service/IdGenerator.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.service; + +import dev.vality.bender.BenderSrv; +import dev.vality.bender.GenerationResult; +import dev.vality.bender.GenerationSchema; +import dev.vality.bender.SequenceSchema; +import dev.vality.msgpack.Nil; +import dev.vality.msgpack.Value; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@RequiredArgsConstructor +public class IdGenerator { + + private static final String SEQ_ID = "orderId"; + + private final BenderSrv.Iface benderClient; + + @org.springframework.beans.factory.annotation.Value("adapter.prefix") + private String adapterPrefix; + + @SneakyThrows + public Long get(String invoiceId) { + GenerationSchema schema = GenerationSchema.sequence(new SequenceSchema().setSequenceId(SEQ_ID)); + GenerationResult result = benderClient.generateID(adapterPrefix + invoiceId, schema, Value.nl(new Nil())); + return Long.parseLong(result.getInternalId()); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/service/IntentResultFactory.java b/src/main/java/dev/vality/adapter/flow/lib/service/IntentResultFactory.java new file mode 100644 index 0000000..efb80ee --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/service/IntentResultFactory.java @@ -0,0 +1,123 @@ +package dev.vality.adapter.flow.lib.service; + +import dev.vality.adapter.flow.lib.constant.RedirectFields; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.model.PollingInfo; +import dev.vality.adapter.flow.lib.model.ThreeDsData; +import dev.vality.adapter.flow.lib.serde.ParametersSerializer; +import dev.vality.adapter.flow.lib.utils.CallbackUrlExtractor; +import dev.vality.adapter.flow.lib.utils.ThreeDsDataInitializer; +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.error.mapping.ErrorMapping; +import lombok.RequiredArgsConstructor; + +import java.nio.ByteBuffer; +import java.util.Map; + +import static dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators.createFinishIntentSuccessWithToken; +import static dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators.createPostUserInteraction; +import static dev.vality.java.damsel.utils.extractors.OptionsExtractors.extractRedirectTimeout; + +@RequiredArgsConstructor +public class IntentResultFactory { + + private final TimerProperties timerProperties; + private final CallbackUrlExtractor callbackUrlExtractor; + private final TagManagementService tagManagementService; + private final ParametersSerializer parametersSerializer; + private final PollingInfoService pollingInfoService; + private final ErrorMapping errorMapping; + private final ExponentialBackOffPollingService exponentialBackOffPollingService; + + public Intent createFinishIntentSuccessWithCheckToken(ExitStateModel exitStateModel) { + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + if (entryStateModel.getBaseRequestModel().getRecurrentPaymentData() != null + && entryStateModel.getBaseRequestModel().getRecurrentPaymentData().isMakeRecurrent()) { + return createFinishIntentSuccessWithToken(exitStateModel.getRecToken()); + } + return createFinishIntentSuccess(); + } + + public Intent createSuspendIntentWithFailedAfterTimeout(ExitStateModel exitStateModel) { + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + ThreeDsData threeDsData = exitStateModel.getThreeDsData(); + Map params = ThreeDsDataInitializer.initThreeDsParameters(exitStateModel); + String redirectUrl = entryStateModel.getBaseRequestModel().getSuccessRedirectUrl(); + params.put(RedirectFields.TERM_URL.getValue(), callbackUrlExtractor.extractCallbackUrl(redirectUrl)); + int timerRedirectTimeout = extractRedirectTimeout( + entryStateModel.getBaseRequestModel().getAdapterConfigurations(), + timerProperties.getRedirectTimeoutMin()); + return Intent.suspend( + new SuspendIntent( + tagManagementService.findTag(params), + Timer.timeout(timerRedirectTimeout) + ).setUserInteraction(createPostUserInteraction(threeDsData.getAcsUrl(), params)) + ); + } + + public Intent createSuspendIntentWithCallbackAfterTimeout(ExitStateModel exitStateModel) { + Map params = ThreeDsDataInitializer.initThreeDsParameters(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); + + String redirectUrl = entryStateModel.getBaseRequestModel().getSuccessRedirectUrl(); + params.put(RedirectFields.TERM_URL.getValue(), callbackUrlExtractor.extractCallbackUrl(redirectUrl)); + ThreeDsData threeDsData = exitStateModel.getThreeDsData(); + int timerRedirectTimeout = extractRedirectTimeout( + entryStateModel.getBaseRequestModel().getAdapterConfigurations(), + timerProperties.getRedirectTimeoutMin()); + return Intent.suspend( + new SuspendIntent( + tagManagementService.findTag(params), + Timer.timeout(timerRedirectTimeout)) + .setTimeoutBehaviour(TimeoutBehaviour.callback( + ByteBuffer.wrap(parametersSerializer.writeByte(params))) + ).setUserInteraction(createPostUserInteraction(threeDsData.getAcsUrl(), params)) + ); + } + + public Intent createFinishIntentSuccess() { + return Intent.finish(new FinishIntent(FinishStatus.success(new Success()))); + } + + public Intent createSleepIntentForReinvocation() { + return Intent.sleep(new SleepIntent(Timer.timeout(0))); + } + + 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 adapterConfigurations = entryStateModel.getBaseRequestModel().getAdapterConfigurations(); + int nextTimeout = + exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, adapterConfigurations); + return Intent.sleep( + new SleepIntent(Timer.timeout(nextTimeout)) + ); + } + + public Intent createFinishIntentFailed(ExitStateModel exitStateModel) { + return Intent.finish(new FinishIntent(FinishStatus.failure( + errorMapping.mapFailure(exitStateModel.getErrorCode(), + exitStateModel.getErrorMessage())))); + } + + public Intent createFinishIntentFailed(String errorCode, String errorMessage) { + return Intent.finish(new FinishIntent(FinishStatus.failure( + errorMapping.mapFailure(errorCode, errorMessage)))); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/service/PollingInfoService.java b/src/main/java/dev/vality/adapter/flow/lib/service/PollingInfoService.java new file mode 100644 index 0000000..6696df1 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/service/PollingInfoService.java @@ -0,0 +1,47 @@ +package dev.vality.adapter.flow.lib.service; + +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.PollingInfo; +import dev.vality.adapter.flow.lib.utils.TimerProperties; +import dev.vality.java.damsel.utils.extractors.OptionsExtractors; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +@RequiredArgsConstructor +public class PollingInfoService { + + private final TimerProperties timerProperties; + + public PollingInfo initPollingInfo(EntryStateModel entryStateModel) { + PollingInfo pollingInfo = entryStateModel.getStartedPollingInfo(); + if (pollingInfo == null) { + pollingInfo = new PollingInfo(); + } + if (pollingInfo.getStartDateTimePolling() == null) { + pollingInfo.setStartDateTimePolling(Instant.now()); + } + Instant maxDateTimePolling = calcDeadline(entryStateModel, pollingInfo); + pollingInfo.setMaxDateTimePolling(maxDateTimePolling); + return pollingInfo; + } + + public boolean isDeadline(PollingInfo pollingInfo) { + Instant now = Instant.now(); + return now.isAfter(pollingInfo.getMaxDateTimePolling()); + } + + private Instant calcDeadline(EntryStateModel entryStateModel, @NonNull PollingInfo pollingInfo) { + if (pollingInfo.getMaxDateTimePolling() == null) { + Integer maxTimePolling = OptionsExtractors.extractMaxTimePolling( + entryStateModel.getBaseRequestModel().getAdapterConfigurations(), + timerProperties.getMaxTimePollingMin()); + return pollingInfo.getStartDateTimePolling().plus(maxTimePolling, ChronoUnit.MINUTES); + } + return pollingInfo.getMaxDateTimePolling(); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/service/RecurrentIntentResultFactory.java b/src/main/java/dev/vality/adapter/flow/lib/service/RecurrentIntentResultFactory.java new file mode 100644 index 0000000..ab4ccfc --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/service/RecurrentIntentResultFactory.java @@ -0,0 +1,80 @@ +package dev.vality.adapter.flow.lib.service; + +import dev.vality.adapter.flow.lib.constant.RedirectFields; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.model.PollingInfo; +import dev.vality.adapter.flow.lib.model.ThreeDsData; +import dev.vality.adapter.flow.lib.utils.CallbackUrlExtractor; +import dev.vality.adapter.flow.lib.utils.ThreeDsDataInitializer; +import dev.vality.adapter.flow.lib.utils.TimerProperties; +import dev.vality.damsel.base.Timer; +import dev.vality.damsel.proxy_provider.*; +import dev.vality.error.mapping.ErrorMapping; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +import static dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators.createPostUserInteraction; +import static dev.vality.java.damsel.utils.creators.ProxyProviderPackageCreators.createRecurrentTokenStatusSuccess; +import static dev.vality.java.damsel.utils.extractors.OptionsExtractors.extractRedirectTimeout; + +@RequiredArgsConstructor +public class RecurrentIntentResultFactory { + + private final TimerProperties timerProperties; + private final CallbackUrlExtractor callbackUrlExtractor; + private final TagManagementService tagManagementService; + private final PollingInfoService pollingInfoService; + private final ErrorMapping errorMapping; + private final ExponentialBackOffPollingService exponentialBackOffPollingService; + + public RecurrentTokenIntent createIntentWithSuspension(ExitStateModel exitStateModel) { + EntryStateModel entryStateModel = exitStateModel.getEntryStateModel(); + ThreeDsData threeDsData = exitStateModel.getThreeDsData(); + Map params = ThreeDsDataInitializer.initThreeDsParameters(exitStateModel); + String redirectUrl = entryStateModel.getBaseRequestModel().getSuccessRedirectUrl(); + params.put(RedirectFields.TERM_URL.getValue(), callbackUrlExtractor.extractCallbackUrl(redirectUrl)); + int timerRedirectTimeout = extractRedirectTimeout( + entryStateModel.getBaseRequestModel().getAdapterConfigurations(), + timerProperties.getRedirectTimeoutMin()); + return RecurrentTokenIntent.suspend( + new SuspendIntent( + tagManagementService.findTag(params), + Timer.timeout(timerRedirectTimeout) + ).setUserInteraction(createPostUserInteraction(threeDsData.getAcsUrl(), params)) + ); + } + + public RecurrentTokenIntent createSleepIntentForReinvocation() { + return RecurrentTokenIntent.sleep(new SleepIntent(Timer.timeout(0))); + } + + public RecurrentTokenIntent 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 adapterConfigurations = entryStateModel.getBaseRequestModel().getAdapterConfigurations(); + int nextTimeout = + exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, adapterConfigurations); + return RecurrentTokenIntent.sleep( + new SleepIntent(Timer.timeout(nextTimeout)) + ); + } + + public RecurrentTokenIntent createFinishIntent(String recToken) { + return RecurrentTokenIntent.finish( + createRecurrentTokenStatusSuccess(recToken) + ); + } + + public RecurrentTokenIntent createFinishIntentFailed(String errorCode, String errorMessage) { + return RecurrentTokenIntent.finish(new RecurrentTokenFinishIntent(RecurrentTokenFinishStatus.failure( + errorMapping.mapFailure(errorCode, errorMessage))) + ); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/service/TagManagementService.java b/src/main/java/dev/vality/adapter/flow/lib/service/TagManagementService.java new file mode 100644 index 0000000..7198f93 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/service/TagManagementService.java @@ -0,0 +1,33 @@ +package dev.vality.adapter.flow.lib.service; + +import dev.vality.adapter.flow.lib.utils.AdapterProperties; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.util.StringUtils; + +import java.util.Map; +import java.util.Optional; + +@RequiredArgsConstructor +public class TagManagementService { + + private final AdapterProperties adapterProperties; + + @SneakyThrows + public String findTag(Map parameters) { + Optional first = adapterProperties.getTagGeneratorFieldNames().stream() + .filter(s -> StringUtils.hasText(parameters.get(s))) + .findFirst(); + return first.get(); + } + + @SneakyThrows + public String get(Map parameters) { + Optional first = adapterProperties.getTagGeneratorFieldNames().stream() + .filter(s -> StringUtils.hasText(parameters.get(s))) + .findFirst(); + return first.get(); + } + + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/service/TemporaryContextService.java b/src/main/java/dev/vality/adapter/flow/lib/service/TemporaryContextService.java new file mode 100644 index 0000000..d491ad3 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/service/TemporaryContextService.java @@ -0,0 +1,54 @@ +package dev.vality.adapter.flow.lib.service; + +import dev.vality.adapter.flow.lib.model.TemporaryContext; +import dev.vality.adapter.flow.lib.serde.Deserializer; +import dev.vality.adapter.flow.lib.serde.ParametersDeserializer; +import dev.vality.damsel.proxy_provider.PaymentContext; +import dev.vality.damsel.proxy_provider.RecurrentTokenContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +public class TemporaryContextService { + + private final ParametersDeserializer parametersDeserializer; + + public TemporaryContext getTemporaryContext(Object context, Deserializer deserializer) { + byte[] state = getState(context); + if (state != null && state.length > 0) { + return deserializer.read(state); + } + return new TemporaryContext(); + } + + public TemporaryContext appendThreeDsParametersToContext(ByteBuffer callback, TemporaryContext temporaryContext) { + if (callback != null && callback.hasArray() && callback.array().length > 0) { + Map parameters = parametersDeserializer.read(callback.array()); + if (temporaryContext != null) { + temporaryContext.setThreeDsData(parameters); + } else { + if (parameters == null || parameters.isEmpty()) { + throw new RuntimeException("Unknown parameters or baseModel!"); + } + } + } + log.info("TemporaryContext: {} after callback.", temporaryContext); + return temporaryContext; + + } + + private static byte[] getState(Object context) { + if (context instanceof RecurrentTokenContext) { + if (((RecurrentTokenContext) context).getSession() == null) { + return new byte[0]; + } + return ((RecurrentTokenContext) context).getSession().getState(); + } + return ((PaymentContext) context).getSession().getState(); + } + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/service/ThreeDsAdapterService.java b/src/main/java/dev/vality/adapter/flow/lib/service/ThreeDsAdapterService.java new file mode 100644 index 0000000..61fa44c --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/service/ThreeDsAdapterService.java @@ -0,0 +1,62 @@ +package dev.vality.adapter.flow.lib.service; + +import dev.vality.adapter.flow.lib.serde.ParametersSerializer; +import dev.vality.adapter.flow.lib.serde.ParametersDeserializer; +import dev.vality.adapter.flow.lib.utils.CallbackUrlExtractor; +import dev.vality.adapter.helpers.hellgate.HellgateAdapterClient; +import dev.vality.adapter.helpers.hellgate.exception.HellgateException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.function.BiFunction; + +@Slf4j +@RequiredArgsConstructor +public class ThreeDsAdapterService { + + private final HellgateAdapterClient hgClient; + private final ParametersSerializer parametersSerializer; + private final ParametersDeserializer parametersDeserializer; + private final TagManagementService tagManagementService; + + public String receivePaymentIncomingParameters(HttpServletRequest servletRequest, + HttpServletResponse servletResponse) { + return this.processCallback(servletRequest, servletResponse, hgClient::processPaymentCallback); + } + + public String receiveRecurrentIncomingParameters(HttpServletRequest servletRequest, + HttpServletResponse servletResponse) { + return this.processCallback(servletRequest, servletResponse, hgClient::processRecurrentTokenCallback); + } + + private String processCallback(HttpServletRequest servletRequest, + HttpServletResponse servletResponse, + BiFunction hgFunction) { + String resp = ""; + Map parameters = this.parametersDeserializer.read(servletRequest); + log.info("-> callback 3ds {}", parameters); + + try { + ByteBuffer callback = ByteBuffer.wrap(this.parametersSerializer.writeByte(parameters)); + ByteBuffer response = hgFunction.apply(tagManagementService.findTag(parameters), callback); + resp = new String(response.array(), StandardCharsets.UTF_8); + if (StringUtils.hasText(parameters.get(CallbackUrlExtractor.TERMINATION_URI))) { + servletResponse.sendRedirect(parameters.get(CallbackUrlExtractor.TERMINATION_URI)); + } + } catch (HellgateException var9) { + log.warn("Failed handle callback for recurrent", var9); + } catch (Exception var10) { + log.error("Failed handle callback for recurrent", var10); + } + + log.info("<- callback 3ds {}", resp); + return resp; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/servlet/AdapterServlet.java b/src/main/java/dev/vality/adapter/flow/lib/servlet/AdapterServlet.java new file mode 100644 index 0000000..2ff481b --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/servlet/AdapterServlet.java @@ -0,0 +1,31 @@ +package dev.vality.adapter.flow.lib.servlet; + +import dev.vality.damsel.proxy_provider.ProviderProxySrv; +import dev.vality.woody.thrift.impl.http.THServiceBuilder; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.servlet.*; +import javax.servlet.annotation.WebServlet; +import java.io.IOException; + +@WebServlet("/adapter/${service.name}") +public class AdapterServlet extends GenericServlet { + + @Autowired + private ProviderProxySrv.Iface serverHandlerLogDecorator; + + private Servlet servlet; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + servlet = new THServiceBuilder().build(ProviderProxySrv.Iface.class, serverHandlerLogDecorator); + } + + @Override + public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { + servlet.service(request, response); + } + +} + diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/AdapterProperties.java b/src/main/java/dev/vality/adapter/flow/lib/utils/AdapterProperties.java new file mode 100644 index 0000000..d553419 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/AdapterProperties.java @@ -0,0 +1,37 @@ +package dev.vality.adapter.flow.lib.utils; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Data +@Validated +@ConfigurationProperties("adapter") +public class AdapterProperties { + + @NotEmpty + private String url; + private String callbackUrl; + private String pathCallbackUrl; + private String pathRecurrentCallbackUrl; + + private String successRedirectUrl; + private String failedRedirectUrl; + + private String tagPrefix; + private List tagGeneratorFieldNames = List.of( + "MD", + "threeDSMethodData", + "threeDSSessionData", + "ThreeDsMethodData", + "threeDsMethodData", + "md", + "ThreeDSMethodData", + "ThreeDSSessionData", + "tag" + ); + +} \ No newline at end of file diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/CallbackUrlExtractor.java b/src/main/java/dev/vality/adapter/flow/lib/utils/CallbackUrlExtractor.java new file mode 100644 index 0000000..7e402e7 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/CallbackUrlExtractor.java @@ -0,0 +1,31 @@ +package dev.vality.adapter.flow.lib.utils; + +import dev.vality.adapter.flow.lib.constant.RedirectFields; +import lombok.RequiredArgsConstructor; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Map; + +@RequiredArgsConstructor +public class CallbackUrlExtractor { + + public static final String TERMINATION_URI = "termination_uri"; + + private final AdapterProperties adapterProperties; + + public String extractCallbackUrl(String redirectUrl) { + return UriComponentsBuilder.fromUriString(adapterProperties.getCallbackUrl()) + .path(adapterProperties.getPathCallbackUrl()) + .queryParam(TERMINATION_URI, redirectUrl).build().toUriString(); + } + + public String getSuccessRedirectUrl(Map adapterConfigurations, String redirectUrl) { + if (StringUtils.hasText(redirectUrl)) { + return redirectUrl; + } + return adapterConfigurations.getOrDefault(RedirectFields.TERM_URL.getValue(), + adapterProperties.getSuccessRedirectUrl()); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/CardDataUtils.java b/src/main/java/dev/vality/adapter/flow/lib/utils/CardDataUtils.java new file mode 100644 index 0000000..e3c842f --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/CardDataUtils.java @@ -0,0 +1,19 @@ +package dev.vality.adapter.flow.lib.utils; + +import dev.vality.cds.storage.SessionData; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CardDataUtils { + + public static String extractCvv2(SessionData sessionData) { + if (sessionData == null + || sessionData.getAuthData() == null + || !sessionData.getAuthData().isSetCardSecurityCode()) { + return null; + } + return sessionData.getAuthData().getCardSecurityCode().getValue(); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/ErrorUtils.java b/src/main/java/dev/vality/adapter/flow/lib/utils/ErrorUtils.java new file mode 100644 index 0000000..ea10aa1 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/ErrorUtils.java @@ -0,0 +1,12 @@ +package dev.vality.adapter.flow.lib.utils; + +import dev.vality.adapter.flow.lib.model.BaseResponseModel; +import org.springframework.util.StringUtils; + +public class ErrorUtils { + + public static boolean isError(BaseResponseModel baseResponse) { + return baseResponse == null || StringUtils.hasText(baseResponse.getErrorCode()); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/PaymentResourceTypeResolver.java b/src/main/java/dev/vality/adapter/flow/lib/utils/PaymentResourceTypeResolver.java new file mode 100644 index 0000000..c9bb320 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/PaymentResourceTypeResolver.java @@ -0,0 +1,27 @@ +package dev.vality.adapter.flow.lib.utils; + +import dev.vality.damsel.proxy_provider.PaymentContext; +import dev.vality.damsel.proxy_provider.PaymentResource; +import dev.vality.java.damsel.constant.PaymentResourceType; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PaymentResourceTypeResolver { + + public static String extractPaymentResourceType(PaymentContext paymentContext) { + if (paymentContext == null) { + throw new IllegalArgumentException("PaymentContext cannot be empty"); + } else if (paymentContext.getSession() == null) { + throw new IllegalArgumentException("Payment context session cannot be empty"); + } + return extractPaymentResourceType(paymentContext.getPaymentInfo().getPayment().getPaymentResource()); + } + + public static String extractPaymentResourceType(PaymentResource paymentResource) { + return (paymentResource.isSetRecurrentPaymentResource()) + ? PaymentResourceType.RECURRENT.name() + : PaymentResourceType.PAYMENT.name(); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/SimpleErrorMapping.java b/src/main/java/dev/vality/adapter/flow/lib/utils/SimpleErrorMapping.java new file mode 100644 index 0000000..1a2f90c --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/SimpleErrorMapping.java @@ -0,0 +1,26 @@ +package dev.vality.adapter.flow.lib.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.error.mapping.ErrorMapping; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.Resource; + +import java.io.IOException; + +@RequiredArgsConstructor +public class SimpleErrorMapping { + + private final Resource filePath; + private final String patternReason; + + public ErrorMapping createErrorMapping() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + + ErrorMapping errorMapping = new ErrorMapping(filePath.getInputStream(), patternReason, mapper); + errorMapping.validateMapping(); + return errorMapping; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/StageFlowResolver.java b/src/main/java/dev/vality/adapter/flow/lib/utils/StageFlowResolver.java new file mode 100644 index 0000000..dd30a4f --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/StageFlowResolver.java @@ -0,0 +1,17 @@ +package dev.vality.adapter.flow.lib.utils; + +import dev.vality.adapter.flow.lib.constant.OptionFields; +import dev.vality.adapter.flow.lib.constant.Stage; +import dev.vality.adapter.flow.lib.model.EntryStateModel; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StageFlowResolver { + + public static boolean isOneStageFlow(EntryStateModel stateModel) { + return Stage.ONE.equals(stateModel.getBaseRequestModel().getAdapterConfigurations() + .get(OptionFields.STAGE.name())); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/TargetStatusResolver.java b/src/main/java/dev/vality/adapter/flow/lib/utils/TargetStatusResolver.java new file mode 100644 index 0000000..9919ff9 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/TargetStatusResolver.java @@ -0,0 +1,37 @@ +package dev.vality.adapter.flow.lib.utils; + +import dev.vality.adapter.flow.lib.constant.TargetStatus; +import dev.vality.adapter.flow.lib.exception.UnknownTargetStatusException; +import dev.vality.damsel.domain.TargetInvoicePaymentStatus; +import dev.vality.damsel.proxy_provider.PaymentContext; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TargetStatusResolver { + + public static TargetStatus extractTargetStatus(PaymentContext paymentContext) { + if (paymentContext == null) { + throw new IllegalArgumentException("PaymentContext cannot be empty"); + } else if (paymentContext.getSession() == null) { + throw new IllegalArgumentException("Payment context session cannot be empty"); + } else { + return extractTargetStatus(paymentContext.getSession().getTarget()); + } + } + + public static TargetStatus extractTargetStatus(TargetInvoicePaymentStatus targetInvoicePaymentStatus) { + if (targetInvoicePaymentStatus != null) { + if (targetInvoicePaymentStatus.isSetProcessed()) { + return TargetStatus.PROCESSED; + } else if (targetInvoicePaymentStatus.isSetCancelled()) { + return TargetStatus.CANCELLED; + } else if (targetInvoicePaymentStatus.isSetCaptured()) { + return TargetStatus.CAPTURED; + } else if (targetInvoicePaymentStatus.isSetRefunded()) { + return TargetStatus.REFUNDED; + } + } + throw new UnknownTargetStatusException(); + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/ThreeDsDataInitializer.java b/src/main/java/dev/vality/adapter/flow/lib/utils/ThreeDsDataInitializer.java new file mode 100644 index 0000000..024009d --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/ThreeDsDataInitializer.java @@ -0,0 +1,24 @@ +package dev.vality.adapter.flow.lib.utils; + +import dev.vality.adapter.flow.lib.model.ExitStateModel; +import dev.vality.adapter.flow.lib.model.ThreeDsData; + +import java.util.HashMap; +import java.util.Map; + +public class ThreeDsDataInitializer { + + public static final String TAG = "tag"; + + public static Map initThreeDsParameters(ExitStateModel exitStateModel) { + Map params = new HashMap<>(); + ThreeDsData threeDsData = exitStateModel.getThreeDsData(); + if (threeDsData.getParameters() != null) { + params.putAll(threeDsData.getParameters()); + } else { + params.put(TAG, exitStateModel.getProviderTrxId()); + } + return params; + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/TimerProperties.java b/src/main/java/dev/vality/adapter/flow/lib/utils/TimerProperties.java new file mode 100644 index 0000000..bce7748 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/TimerProperties.java @@ -0,0 +1,37 @@ +package dev.vality.adapter.flow.lib.utils; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; + +@Validated +@Getter +@Setter +@ConfigurationProperties("time.config") +public class TimerProperties { + + @NotNull + private int redirectTimeoutMin; + + @NotNull + private int maxTimePollingMin; + + @NotNull + private int pollingDelayMs; + + @NotNull + private int exponential; + + @NotNull + private int defaultInitialExponential; + + @NotNull + private int maxTimeBackOff; + + @NotNull + private int maxTimeCoefficient; + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/BackOff.java b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/BackOff.java new file mode 100644 index 0000000..4e48d8e --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/BackOff.java @@ -0,0 +1,5 @@ +package dev.vality.adapter.flow.lib.utils.backoff; + +public interface BackOff { + BackOffExecution start(); +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/BackOffExecution.java b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/BackOffExecution.java new file mode 100644 index 0000000..8c38866 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/BackOffExecution.java @@ -0,0 +1,6 @@ +package dev.vality.adapter.flow.lib.utils.backoff; + +@FunctionalInterface +public interface BackOffExecution { + Long nextBackOff(); +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/ExponentialBackOff.java b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/ExponentialBackOff.java new file mode 100644 index 0000000..2ae44bd --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/ExponentialBackOff.java @@ -0,0 +1,72 @@ +package dev.vality.adapter.flow.lib.utils.backoff; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ExponentialBackOff implements BackOff { + + public static final Integer DEFAULT_MUTIPLIER = 2; + public static final Integer DEFAULT_INITIAL_INTERVAL_SEC = 2; + public static final Integer DEFAULT_MAX_INTERVAL_SEC = 300; + + private Integer multiplier = DEFAULT_MUTIPLIER; + private Integer initialInterval = DEFAULT_INITIAL_INTERVAL_SEC; + private Integer maxInterval = DEFAULT_MAX_INTERVAL_SEC; + + private Long startTime; + private Long currentTime; + + public ExponentialBackOff( + Long startTime, + Long currentTime, + Integer multiplier, + Integer initialInterval, + Integer maxInterval) { + this.startTime = startTime; + this.currentTime = currentTime; + this.multiplier = multiplier; + this.initialInterval = initialInterval; + this.maxInterval = maxInterval; + } + + @Override + public BackOffExecution start() { + return new ExponentialBackOffExecution(); + } + + private class ExponentialBackOffExecution implements BackOffExecution { + @Override + public Long nextBackOff() { + if (ExponentialBackOff.this.currentTime.equals(ExponentialBackOff.this.startTime)) { + return Long.valueOf(ExponentialBackOff.this.initialInterval); + } + + long nextBackOff = computeNextInterval( + ExponentialBackOff.this.multiplier, + ExponentialBackOff.this.startTime, + ExponentialBackOff.this.currentTime); + + if (nextBackOff > ExponentialBackOff.this.maxInterval) { + nextBackOff = (long) ExponentialBackOff.this.maxInterval; + } + + return nextBackOff; + } + + private long computeNextInterval(int multiplier, Long startTime, Long currentTime) { + long diff = (currentTime - startTime) / 1000; + if (diff < 1 || multiplier == 1) { + return initialInterval; + } + long result = initialInterval; + int step = 0; + while (diff >= result) { + long pow = (long) Math.pow(multiplier, step++); + result = initialInterval * pow; + } + return result; + } + } +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/README.md b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/README.md new file mode 100644 index 0000000..07a8075 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/README.md @@ -0,0 +1,29 @@ +# Поллинг для адаптеров + +## Настройки + +Название параметра | Описание | Пример +------------ | ------------- | ------------- +**exponential** | экспонента | 2 +**max_time_backoff** | максимальное время для ограничения роста экспоненты | 600 (10 минут) +**default_initial_exponential** | экспонента по умолчанию | 2 + +## Пример использования: + +``` +int nextPollingInterval = BackOffUtils.prepareNextPollingInterval(adapterState, options); +``` + +или + +``` +BackOffExecution backOffExecution = BackOffUtils.prepareBackOffExecution(adapterState, options); +int nextPollingInterval = backOffExecution.nextBackOff().intValue(); +``` + +или + +``` +ExponentialBackOff exponentialBackOff = new ExponentialBackOff(adapterState, options); +int nextPollingInterval = exponentialBackOff.start().nextBackOff().intValue(); +``` diff --git a/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/TimeOptionsExtractors.java b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/TimeOptionsExtractors.java new file mode 100644 index 0000000..58a4c48 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/utils/backoff/TimeOptionsExtractors.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.utils.backoff; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TimeOptionsExtractors { + + public static final String TIMER_EXPONENTIAL = "exponential"; + public static final String MAX_TIME_BACKOFF_SEC = "max_time_backoff_sec"; + public static final String DEFAULT_INITIAL_EXPONENTIAL_SEC = "default_initial_exponential_sec"; + + public static Integer extractExponent(Map options, int maxTimePolling) { + return Integer.parseInt(options.getOrDefault(TIMER_EXPONENTIAL, String.valueOf(maxTimePolling))); + } + + public static Integer extractMaxTimeBackOff(Map options, int maxTimeBackOff) { + return Integer.parseInt(options.getOrDefault(MAX_TIME_BACKOFF_SEC, String.valueOf(maxTimeBackOff))); + } + + public static Integer extractDefaultInitialExponential(Map options, int defaultInitialExponential) { + return Integer.parseInt( + options.getOrDefault(DEFAULT_INITIAL_EXPONENTIAL_SEC, String.valueOf(defaultInitialExponential))); + } + +} diff --git a/src/main/java/dev/vality/adapter/flow/lib/validator/AdapterConfigurationValidator.java b/src/main/java/dev/vality/adapter/flow/lib/validator/AdapterConfigurationValidator.java new file mode 100644 index 0000000..9796447 --- /dev/null +++ b/src/main/java/dev/vality/adapter/flow/lib/validator/AdapterConfigurationValidator.java @@ -0,0 +1,16 @@ +package dev.vality.adapter.flow.lib.validator; + +import java.util.Map; + +/** + * Used to check adapter configuration settings. + */ +public interface AdapterConfigurationValidator { + + /** + * @param adapterConfigurationsParameters map of parameters that are configured by supports, + * uniq for adapter implementation + */ + void validate(Map adapterConfigurationsParameters); + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/AbstractGenerateTokenTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/AbstractGenerateTokenTest.java new file mode 100644 index 0000000..0d128f2 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/AbstractGenerateTokenTest.java @@ -0,0 +1,130 @@ +package dev.vality.adapter.flow.lib.flow; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.controller.ThreeDsCallbackController; +import dev.vality.adapter.flow.lib.flow.config.*; +import dev.vality.adapter.flow.lib.flow.full.FullThreeDsAllVersionsStepResolverImpl; +import dev.vality.adapter.flow.lib.flow.full.GenerateTokenFullThreeDsAllVersionsStepResolverImpl; +import dev.vality.adapter.flow.lib.serde.TemporaryContextDeserializer; +import dev.vality.adapter.flow.lib.service.TagManagementService; +import dev.vality.adapter.flow.lib.utils.CallbackUrlExtractor; +import dev.vality.adapter.flow.lib.validator.AdapterConfigurationValidator; +import dev.vality.adapter.helpers.hellgate.HellgateAdapterClient; +import dev.vality.bender.BenderSrv; +import dev.vality.cds.client.storage.CdsClientStorage; +import dev.vality.damsel.proxy_provider.*; +import dev.vality.java.damsel.utils.verification.ProxyProviderVerification; +import org.apache.thrift.TException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.PropertySource; +import org.springframework.test.context.ContextConfiguration; + +import static dev.vality.adapter.flow.lib.flow.full.three.ds.ForwardRecurrentPaymentNon3dsTest.RECURRENT_TOKEN; +import static dev.vality.java.damsel.utils.creators.DomainPackageCreators.createTargetProcessed; +import static org.junit.jupiter.api.Assertions.*; + + +@PropertySource("classpath:application.yaml") +@ContextConfiguration(classes = {HandlerConfig.class, AppConfig.class, ProcessorConfig.class, SerdeConfig.class, + ThreeDsCallbackController.class, TomcatEmbeddedConfiguration.class, TagManagementService.class, + CallbackUrlExtractor.class, FullThreeDsAllVersionsStepResolverImpl.class, + GenerateTokenFullThreeDsAllVersionsStepResolverImpl.class}) +public class AbstractGenerateTokenTest { + + @MockBean + protected CdsClientStorage cdsClientStorage; + @MockBean + protected AdapterConfigurationValidator paymentContextValidator; + @MockBean + protected BenderSrv.Iface benderClient; + @MockBean + protected RemoteClient client; + @MockBean + protected HellgateAdapterClient hellgateAdapterClient; + @Autowired + public TemporaryContextDeserializer temporaryContextDeserializer; + + @Autowired + protected ProviderProxySrv.Iface serverHandlerLogDecorator; + + protected RecurrentTokenProxyResult checkSuccessAuthOrPay(RecurrentTokenContext paymentContext) throws TException { + RecurrentTokenProxyResult recurrentTokenProxyResult = serverHandlerLogDecorator.generateToken(paymentContext); + assertNotNull(recurrentTokenProxyResult.getTrx().getId()); + assertTrue(recurrentTokenProxyResult.getIntent().isSetSleep()); + assertEquals(Step.CAPTURE, + temporaryContextDeserializer.read(recurrentTokenProxyResult.getNextState()).getNextStep()); + return recurrentTokenProxyResult; + } + + protected RecurrentTokenProxyResult checkSleepWithStatus(Step step, + RecurrentTokenContext recurrentTokenContext, + RecurrentTokenProxyResult recurrentTokenProxyResult, + byte[] state) + throws TException { + recurrentTokenContext.getSession() + .setState(state); + recurrentTokenContext.getTokenInfo().setTrx(recurrentTokenProxyResult.getTrx()); + RecurrentTokenProxyResult paymentProxyResultDeposit = + serverHandlerLogDecorator.generateToken(recurrentTokenContext); + assertTrue(paymentProxyResultDeposit.getIntent().isSetSleep()); + assertEquals(step, temporaryContextDeserializer.read(paymentProxyResultDeposit.getNextState()).getNextStep()); + return paymentProxyResultDeposit; + } + + protected RecurrentTokenProxyResult checkSuccessRefund(RecurrentTokenContext recurrentTokenContext, + RecurrentTokenProxyResult recurrentTokenProxyResult, + byte[] state) + throws TException { + recurrentTokenContext.getSession().setState(state); + recurrentTokenContext.getTokenInfo().setTrx(recurrentTokenProxyResult.getTrx()); + RecurrentTokenProxyResult paymentProxyResultRefunded = + serverHandlerLogDecorator.generateToken(recurrentTokenContext); + assertEquals(paymentProxyResultRefunded.getIntent().getFinish().getStatus().getSuccess(), + new RecurrentTokenSuccess(RECURRENT_TOKEN)); + assertNotNull(paymentProxyResultRefunded.getIntent().getFinish().getStatus().getSuccess().getToken()); + assertEquals(Step.DO_NOTHING, + temporaryContextDeserializer.read(paymentProxyResultRefunded.getNextState()).getNextStep()); + return paymentProxyResultRefunded; + } + + protected RecurrentTokenProxyResult checkSuccessFinishThreeDs(RecurrentTokenContext context, + RecurrentTokenProxyResult proxyResult, + RecurrentTokenCallbackResult paymentCallbackResult) + throws TException { + context.getTokenInfo().setTrx(proxyResult.getTrx()); + context.getSession().setState(paymentCallbackResult.getResult().getNextState()); + RecurrentTokenProxyResult paymentProxyResult = serverHandlerLogDecorator.generateToken(context); + String trxId = paymentProxyResult.getTrx().getId(); + assertTrue(ProxyProviderVerification.isSleep(paymentProxyResult)); + assertEquals(trxId, paymentProxyResult.getTrx().getId()); + return paymentProxyResult; + } + + protected PaymentProxyResult checkSuspend(Step step, + PaymentContext context, + PaymentProxyResult proxyResult, + PaymentCallbackResult paymentCallbackResult) + throws TException { + context.getPaymentInfo().getPayment().setTrx(proxyResult.getTrx()); + context.getSession().setState(paymentCallbackResult.getResult().getNextState()); + context.getSession().setTarget(createTargetProcessed()); + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(context); + assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetRedirect()); + assertEquals(step, temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + return paymentProxyResult; + } + + protected RecurrentTokenProxyResult processWithDoNothingSuccessResult(RecurrentTokenContext paymentContext, + RecurrentTokenProxyResult paymentProxyResult) + throws TException { + paymentContext.getSession().setState(paymentProxyResult.getNextState()); + RecurrentTokenProxyResult recurrentTokenProxyResult = serverHandlerLogDecorator.generateToken(paymentContext); + assertTrue(recurrentTokenProxyResult.getIntent().isSetFinish()); + assertTrue(recurrentTokenProxyResult.getIntent().getFinish().getStatus().isSetSuccess()); + assertEquals(Step.DO_NOTHING, + temporaryContextDeserializer.read(recurrentTokenProxyResult.getNextState()).getNextStep()); + return recurrentTokenProxyResult; + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/AbstractPaymentTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/AbstractPaymentTest.java new file mode 100644 index 0000000..d5a7d32 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/AbstractPaymentTest.java @@ -0,0 +1,168 @@ +package dev.vality.adapter.flow.lib.flow; + +import dev.vality.adapter.flow.lib.client.RemoteClient; +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.controller.ThreeDsCallbackController; +import dev.vality.adapter.flow.lib.flow.config.*; +import dev.vality.adapter.flow.lib.flow.full.FullThreeDsAllVersionsStepResolverImpl; +import dev.vality.adapter.flow.lib.flow.full.GenerateTokenFullThreeDsAllVersionsStepResolverImpl; +import dev.vality.adapter.flow.lib.service.TagManagementService; +import dev.vality.adapter.flow.lib.utils.CallbackUrlExtractor; +import dev.vality.adapter.flow.lib.serde.TemporaryContextDeserializer; +import dev.vality.adapter.flow.lib.validator.AdapterConfigurationValidator; +import dev.vality.adapter.helpers.hellgate.HellgateAdapterClient; +import dev.vality.bender.BenderSrv; +import dev.vality.cds.client.storage.CdsClientStorage; +import dev.vality.damsel.domain.InvoicePaymentCaptured; +import dev.vality.damsel.domain.InvoicePaymentRefunded; +import dev.vality.damsel.domain.TargetInvoicePaymentStatus; +import dev.vality.damsel.proxy_provider.*; +import dev.vality.java.damsel.utils.verification.ProxyProviderVerification; +import org.apache.thrift.TException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.PropertySource; +import org.springframework.test.context.ContextConfiguration; + +import static dev.vality.java.damsel.utils.creators.DomainPackageCreators.createTargetProcessed; +import static org.junit.jupiter.api.Assertions.*; + + +@PropertySource("classpath:application.yaml") +@ContextConfiguration(classes = {HandlerConfig.class, AppConfig.class, ProcessorConfig.class, SerdeConfig.class, + ThreeDsCallbackController.class, TomcatEmbeddedConfiguration.class, TagManagementService.class, + CallbackUrlExtractor.class, FullThreeDsAllVersionsStepResolverImpl.class, + GenerateTokenFullThreeDsAllVersionsStepResolverImpl.class}) +public class AbstractPaymentTest { + + @MockBean + protected CdsClientStorage cdsClientStorage; + @MockBean + protected BenderSrv.Iface benderClient; + @MockBean + protected RemoteClient client; + @MockBean + protected HellgateAdapterClient hellgateAdapterClient; + @MockBean + protected AdapterConfigurationValidator paymentContextValidator; + + @Autowired + protected ProviderProxySrv.Iface serverHandlerLogDecorator; + @Autowired + public TemporaryContextDeserializer temporaryContextDeserializer; + + protected PaymentProxyResult processWithDoNothingSuccessResult(PaymentContext paymentContext) throws TException { + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext); + assertNotNull(paymentProxyResult.getTrx().getId()); + assertTrue(paymentProxyResult.getIntent().isSetFinish()); + assertEquals(Step.DO_NOTHING, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + return paymentProxyResult; + } + + protected PaymentProxyResult processWithDoNothingSuccessResult(PaymentContext paymentContext, + PaymentProxyResult paymentProxyResult) + throws TException { + paymentContext.getSession() + .setState(paymentProxyResult.getNextState()); + PaymentProxyResult paymentProxyResultDeposit = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResultDeposit.getIntent().isSetFinish()); + assertTrue(paymentProxyResultDeposit.getIntent().getFinish().getStatus().isSetSuccess()); + assertEquals(Step.DO_NOTHING, + temporaryContextDeserializer.read(paymentProxyResultDeposit.getNextState()).getNextStep()); + return paymentProxyResultDeposit; + } + + protected PaymentProxyResult processWithCheckStatusResult(PaymentContext paymentContext) throws TException { + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResult.getIntent().isSetSleep()); + assertEquals(Step.CHECK_STATUS, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + return paymentProxyResult; + } + + + protected PaymentProxyResult processWithCheckStatusResult(PaymentContext paymentContext, + PaymentProxyResult paymentProxyResult) + throws TException { + paymentContext.getSession() + .setState(paymentProxyResult.getNextState()); + PaymentProxyResult paymentProxyResultDeposit = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResult.getIntent().isSetSleep()); + assertEquals(Step.CHECK_STATUS, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + return paymentProxyResultDeposit; + } + + protected PaymentProxyResult checkSuccessCapture(PaymentContext paymentContext, + PaymentProxyResult paymentProxyResult, + byte[] state) + throws TException { + paymentContext.getSession() + .setTarget(TargetInvoicePaymentStatus.captured(new InvoicePaymentCaptured())) + .setState(state); + paymentContext.getPaymentInfo().getPayment().setTrx(paymentProxyResult.getTrx()); + PaymentProxyResult paymentProxyResultDeposit = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResultDeposit.getIntent().getFinish().getStatus().isSetSuccess()); + assertEquals(Step.DO_NOTHING, + temporaryContextDeserializer.read(paymentProxyResultDeposit.getNextState()).getNextStep()); + return paymentProxyResultDeposit; + } + + protected PaymentProxyResult processCaptureWithCheckStatusResult(PaymentContext paymentContext, + PaymentProxyResult paymentProxyResult, + byte[] state) + throws TException { + paymentContext.getSession() + .setTarget(TargetInvoicePaymentStatus.captured(new InvoicePaymentCaptured())) + .setState(state); + paymentContext.getPaymentInfo().getPayment().setTrx(paymentProxyResult.getTrx()); + PaymentProxyResult paymentProxyResultDeposit = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResultDeposit.getIntent().isSetSleep()); + assertEquals(Step.CHECK_STATUS, + temporaryContextDeserializer.read(paymentProxyResultDeposit.getNextState()).getNextStep()); + return paymentProxyResultDeposit; + } + + protected void checkSuccessRefund(Long refundAmount, + PaymentContext paymentContext, + PaymentProxyResult paymentProxyResultDeposit) + throws TException { + paymentContext.getSession() + .setTarget(TargetInvoicePaymentStatus.refunded(new InvoicePaymentRefunded())) + .setState(paymentProxyResultDeposit.getNextState()); + paymentContext.getPaymentInfo() + .setRefund(new InvoicePaymentRefund() + .setCash(new Cash(refundAmount, null))); + PaymentProxyResult paymentProxyResultRefunded = serverHandlerLogDecorator.processPayment(paymentContext); + assertEquals(paymentProxyResultRefunded.getIntent().getFinish().getStatus().getSuccess(), new Success()); + } + + protected PaymentProxyResult checkSuccessFinishThreeDs(PaymentContext context, + PaymentProxyResult proxyResult, + PaymentCallbackResult paymentCallbackResult) + throws TException { + context.getPaymentInfo().getPayment().setTrx(proxyResult.getTrx()); + context.getSession().setState(paymentCallbackResult.getResult().getNextState()); + context.getSession().setTarget(createTargetProcessed()); + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(context); + String trxId = paymentProxyResult.getTrx().getId(); + assertTrue(ProxyProviderVerification.isSuccess(paymentProxyResult)); + assertEquals(trxId, paymentProxyResult.getTrx().getId()); + return paymentProxyResult; + } + + protected PaymentProxyResult checkSuspend(Step step, + PaymentContext context, + PaymentProxyResult proxyResult, + PaymentCallbackResult paymentCallbackResult) + throws TException { + context.getPaymentInfo().getPayment().setTrx(proxyResult.getTrx()); + context.getSession().setState(paymentCallbackResult.getResult().getNextState()); + context.getSession().setTarget(createTargetProcessed()); + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(context); + assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetRedirect()); + assertEquals(step, temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + return paymentProxyResult; + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/config/AppConfig.java b/src/test/java/dev/vality/adapter/flow/lib/flow/config/AppConfig.java new file mode 100644 index 0000000..85e73a9 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/config/AppConfig.java @@ -0,0 +1,46 @@ +package dev.vality.adapter.flow.lib.flow.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import dev.vality.adapter.flow.lib.utils.SimpleErrorMapping; +import dev.vality.error.mapping.ErrorMapping; +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.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import java.io.IOException; + +@Configuration +public class AppConfig { + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + PropertySourcesPlaceholderConfigurer pspc + = new PropertySourcesPlaceholderConfigurer(); + Resource[] resources = new ClassPathResource[] + {new ClassPathResource("application.yaml")}; + pspc.setLocations(resources); + pspc.setIgnoreUnresolvablePlaceholders(true); + return pspc; + } + + @Bean + public ErrorMapping errorMapping(@Value("${error-mapping.file}") Resource errorMappingFilePath, + @Value("${error-mapping.patternReason:\"'%s' - '%s'\"}") + String errorMappingPattern) throws IOException { + return new SimpleErrorMapping(errorMappingFilePath, errorMappingPattern).createErrorMapping(); + } + + @Bean + @Primary + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new JavaTimeModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/config/HandlerConfig.java b/src/test/java/dev/vality/adapter/flow/lib/flow/config/HandlerConfig.java new file mode 100644 index 0000000..97f7fab --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/config/HandlerConfig.java @@ -0,0 +1,228 @@ +package dev.vality.adapter.flow.lib.flow.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.adapter.flow.lib.converter.ExitStateModelToTemporaryContextConverter; +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.entry.RecCtxToEntryModelConverter; +import dev.vality.adapter.flow.lib.converter.exit.ExitModelToProxyResultConverter; +import dev.vality.adapter.flow.lib.converter.exit.ExitModelToRecTokenProxyResultConverter; +import dev.vality.adapter.flow.lib.flow.RecurrentResultIntentResolver; +import dev.vality.adapter.flow.lib.flow.ResultIntentResolver; +import dev.vality.adapter.flow.lib.handler.ProxyProviderServiceImpl; +import dev.vality.adapter.flow.lib.handler.ServerFlowHandler; +import dev.vality.adapter.flow.lib.handler.ServerHandlerLogDecorator; +import dev.vality.adapter.flow.lib.handler.callback.PaymentCallbackHandler; +import dev.vality.adapter.flow.lib.handler.callback.RecurrentTokenCallbackHandler; +import dev.vality.adapter.flow.lib.serde.ParametersDeserializer; +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.utils.AdapterProperties; +import dev.vality.adapter.flow.lib.utils.CallbackUrlExtractor; +import dev.vality.adapter.flow.lib.utils.TimerProperties; +import dev.vality.adapter.flow.lib.validator.AdapterConfigurationValidator; +import dev.vality.adapter.helpers.hellgate.HellgateAdapterClient; +import dev.vality.bender.BenderSrv; +import dev.vality.cds.client.storage.CdsClientStorage; +import dev.vality.damsel.proxy_provider.ProviderProxySrv; +import dev.vality.error.mapping.ErrorMapping; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class HandlerConfig { + + @Bean + public IdGenerator idGenerator(BenderSrv.Iface iface) { + return new IdGenerator(iface); + } + + @Bean + public TimerProperties timerProperties() { + TimerProperties timerProperties = new TimerProperties(); + timerProperties.setMaxTimePollingMin(60); + timerProperties.setPollingDelayMs(1000); + timerProperties.setRedirectTimeoutMin(15); + return timerProperties; + } + + @Bean + public PollingInfoService pollingInfoService(TimerProperties timerProperties) { + return new PollingInfoService(timerProperties); + } + + @Bean + public TemporaryContextService temporaryContextService(ParametersDeserializer parametersDeserializer) { + return new TemporaryContextService(parametersDeserializer); + } + + @Bean + public PaymentCallbackHandler paymentCallbackHandler(TemporaryContextDeserializer adapterDeserializer, + TemporaryContextSerializer temporaryContextSerializer, + TemporaryContextService temporaryContextService) { + return new PaymentCallbackHandler(adapterDeserializer, + temporaryContextSerializer, + temporaryContextService + ); + } + + @Bean + public RecurrentTokenCallbackHandler recurrentTokenCallbackHandler( + TemporaryContextDeserializer adapterDeserializer, + TemporaryContextSerializer temporaryContextSerializer, + TemporaryContextService temporaryContextService) { + return new RecurrentTokenCallbackHandler(adapterDeserializer, + temporaryContextSerializer, + temporaryContextService); + } + + @Bean + public CtxToEntryModelConverter ctxToEntryModelConverter(CdsClientStorage cdsClientStorage, + TemporaryContextDeserializer adapterDeserializer, + IdGenerator idGenerator, + TemporaryContextService temporaryContextService, + CallbackUrlExtractor callbackUrlExtractor) { + return new CtxToEntryModelConverter(cdsClientStorage, + adapterDeserializer, + idGenerator, + temporaryContextService, + callbackUrlExtractor); + } + + @Bean + public AdapterProperties adapterProperties() { + AdapterProperties adapterProperties = new AdapterProperties(); + adapterProperties.setCallbackUrl("http://localhost:8080/adapter/term_url"); + adapterProperties.setSuccessRedirectUrl("http://localhost:8080/adapter/term_url"); + return adapterProperties; + } + + @Bean + public RecCtxToEntryModelConverter recCtxToEntryModelConverter(CdsClientStorage cdsClientStorage, + TemporaryContextDeserializer adapterDeserializer, + IdGenerator idGenerator, + TemporaryContextService temporaryContextService) { + return new RecCtxToEntryModelConverter(adapterDeserializer, + cdsClientStorage, + idGenerator, + temporaryContextService); + } + + @Bean + public ExitStateModelToTemporaryContextConverter exitStateModelToTemporaryContextConverter() { + return new ExitStateModelToTemporaryContextConverter(); + } + + @Bean + public ExitModelToRecTokenProxyResultConverter exitModelToRecTokenProxyResultConverter( + RecurrentIntentResultFactory recurrentIntentResultFactory, + TemporaryContextSerializer temporaryContextSerializer, + RecurrentResultIntentResolver recurrentResultIntentResolver, + ExitStateModelToTemporaryContextConverter exitStateModelToTemporaryContextConverter) { + return new ExitModelToRecTokenProxyResultConverter(recurrentIntentResultFactory, + temporaryContextSerializer, + recurrentResultIntentResolver, + exitStateModelToTemporaryContextConverter + ); + } + + @Bean + public ErrorMapping errorMapping() { + return new ErrorMapping("", List.of()); + } + + + @Bean + public TagManagementService tagManagementService(AdapterProperties adapterProperties) { + return new TagManagementService(adapterProperties); + } + + @Bean + public ParametersDeserializer parametersDeserializer(ObjectMapper objectMapper) { + return new ParametersDeserializer(objectMapper); + } + + @Bean + public ParametersSerializer parameterSerializer(ObjectMapper objectMapper) { + return new ParametersSerializer(objectMapper); + } + + @Bean + public ThreeDsAdapterService threeDsAdapterService(HellgateAdapterClient hgClient, + ParametersSerializer parametersSerializer, + ParametersDeserializer parametersDeserializer, + TagManagementService tagManagementService + ) { + return new ThreeDsAdapterService(hgClient, parametersSerializer, parametersDeserializer, tagManagementService); + } + + @Bean + public ExitModelToProxyResultConverter exitModelToProxyResultConverter( + IntentResultFactory intentResultFactory, + TemporaryContextSerializer temporaryContextSerializer, + ResultIntentResolver resultIntentResolver, + ExitStateModelToTemporaryContextConverter exitStateModelToTemporaryContextConverter) { + return new ExitModelToProxyResultConverter(intentResultFactory, + temporaryContextSerializer, + resultIntentResolver, + exitStateModelToTemporaryContextConverter); + } + + @Bean + public EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter() { + return new EntryModelToBaseRequestModelConverter(); + } + + @Bean + public ProviderProxySrv.Iface serverHandlerLogDecorator( + PaymentCallbackHandler paymentCallbackHandler, + RecurrentTokenCallbackHandler recurrentTokenCallbackHandler, + ServerFlowHandler serverFlowHandler, + ServerFlowHandler generateTokenFlowHandler, + AdapterConfigurationValidator paymentContextValidator) { + return new ServerHandlerLogDecorator(new ProxyProviderServiceImpl( + paymentCallbackHandler, + recurrentTokenCallbackHandler, + serverFlowHandler, + generateTokenFlowHandler, + paymentContextValidator + )); + } + + @Bean + public ExponentialBackOffPollingService exponentialBackOffPollingService() { + return new ExponentialBackOffPollingService(); + } + + @Bean + public IntentResultFactory intentResultFactory( + TimerProperties timerProperties, + CallbackUrlExtractor callbackUrlExtractor, + TagManagementService tagManagementService, + ParametersSerializer parametersSerializer, + PollingInfoService pollingInfoService, + ErrorMapping errorMapping, + ExponentialBackOffPollingService exponentialBackOffPollingService) { + return new IntentResultFactory(timerProperties, callbackUrlExtractor, tagManagementService, + parametersSerializer, pollingInfoService, errorMapping, exponentialBackOffPollingService); + } + + @Bean + public RecurrentIntentResultFactory recurrentIntentResultFactory( + TimerProperties timerProperties, + CallbackUrlExtractor callbackUrlExtractor, + TagManagementService tagManagementService, + PollingInfoService pollingInfoService, + ErrorMapping errorMapping, + ExponentialBackOffPollingService exponentialBackOffPollingService) { + return new RecurrentIntentResultFactory(timerProperties, callbackUrlExtractor, tagManagementService, + pollingInfoService, errorMapping, exponentialBackOffPollingService); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/config/ProcessorConfig.java b/src/test/java/dev/vality/adapter/flow/lib/flow/config/ProcessorConfig.java new file mode 100644 index 0000000..23017ee --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/config/ProcessorConfig.java @@ -0,0 +1,23 @@ +package dev.vality.adapter.flow.lib.flow.config; + +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 lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class ProcessorConfig { + + @Bean + public Processor baseProcessor() { + ErrorProcessor errorProcessor = new ErrorProcessor(); + SuccessFinishProcessor baseProcessor = new SuccessFinishProcessor(errorProcessor); + RedirectProcessor redirectProcessor = new RedirectProcessor(baseProcessor); + return new RetryProcessor(redirectProcessor); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/config/SerdeConfig.java b/src/test/java/dev/vality/adapter/flow/lib/flow/config/SerdeConfig.java new file mode 100644 index 0000000..ffe4986 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/config/SerdeConfig.java @@ -0,0 +1,28 @@ +package dev.vality.adapter.flow.lib.flow.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.adapter.flow.lib.serde.ParametersDeserializer; +import dev.vality.adapter.flow.lib.serde.TemporaryContextDeserializer; +import dev.vality.adapter.flow.lib.serde.TemporaryContextSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SerdeConfig { + + @Bean + public TemporaryContextDeserializer adapterDeserializer(ObjectMapper objectMapper) { + return new TemporaryContextDeserializer(objectMapper); + } + + @Bean + public TemporaryContextSerializer adapterSerializer(ObjectMapper objectMapper) { + return new TemporaryContextSerializer(objectMapper); + } + + @Bean + public ParametersDeserializer parametersDeserializer(ObjectMapper objectMapper) { + return new ParametersDeserializer(objectMapper); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/config/TomcatEmbeddedConfiguration.java b/src/test/java/dev/vality/adapter/flow/lib/flow/config/TomcatEmbeddedConfiguration.java new file mode 100644 index 0000000..ef311d0 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/config/TomcatEmbeddedConfiguration.java @@ -0,0 +1,99 @@ +package dev.vality.adapter.flow.lib.flow.config; + +import dev.vality.woody.api.flow.WFlow; +import org.apache.catalina.connector.Connector; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Configuration +public class TomcatEmbeddedConfiguration { + + public static final String HEALTH = "/actuator/health"; + + @Value("${server.rest.port}") + private int restPort; + + @Value("/${server.rest.endpoint}/") + private String restEndpoint; + + @Bean + public ServletWebServerFactory servletContainer() { + TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(); + Connector connector = new Connector(); + connector.setPort(restPort); + tomcat.addAdditionalTomcatConnectors(connector); + return tomcat; + } + + @Bean + public FilterRegistrationBean externalPortRestrictingFilter() { + Filter filter = new OncePerRequestFilter() { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String servletPath = request.getServletPath(); + if ((request.getLocalPort() == restPort) + && !(servletPath.startsWith(restEndpoint) || servletPath.startsWith(HEALTH))) { + response.sendError(404, "Unknown address"); + return; + } + filterChain.doFilter(request, response); + } + }; + + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); + filterRegistrationBean.setFilter(filter); + filterRegistrationBean.setOrder(-100); + filterRegistrationBean.setName("httpPortFilter"); + filterRegistrationBean.addUrlPatterns("/*"); + return filterRegistrationBean; + } + + @Bean + public FilterRegistrationBean woodyFilter() { + WFlow woodyFlow = new WFlow(); + Filter filter = new OncePerRequestFilter() { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if ((request.getLocalPort() == restPort) + && request.getServletPath().startsWith(restEndpoint)) { + woodyFlow.createServiceFork(() -> { + try { + filterChain.doFilter(request, response); + } catch (IOException | ServletException e) { + sneakyThrow(e); + } + }).run(); + return; + } + filterChain.doFilter(request, response); + } + + private T sneakyThrow(Throwable t) throws E { + throw (E) t; + } + }; + + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); + filterRegistrationBean.setFilter(filter); + filterRegistrationBean.setOrder(-50); + filterRegistrationBean.setName("woodyFilter"); + filterRegistrationBean.addUrlPatterns(restEndpoint + "*"); + return filterRegistrationBean; + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/ErrorPaymentNon3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/ErrorPaymentNon3dsTest.java new file mode 100644 index 0000000..a707d3f --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/ErrorPaymentNon3dsTest.java @@ -0,0 +1,54 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +import dev.vality.adapter.flow.lib.flow.AbstractPaymentTest; +import dev.vality.adapter.flow.lib.flow.full.three.ds.config.FullThreeDsFlowConfig; +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 = FullThreeDsFlowConfig.class) +@TestPropertySource(properties = {"server.rest.port=8083", + "error-mapping.file=classpath:fixture/errors.json"}) +public class ErrorPaymentNon3dsTest extends AbstractPaymentTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + MockUtil.mockAllWithout3Ds(cdsClientStorage, 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()); + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/ForwardRecurrentPaymentNon3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/ForwardRecurrentPaymentNon3dsTest.java new file mode 100644 index 0000000..2da8aaa --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/ForwardRecurrentPaymentNon3dsTest.java @@ -0,0 +1,88 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.flow.AbstractPaymentTest; +import dev.vality.adapter.flow.lib.flow.full.three.ds.config.FullThreeDsFlowConfig; +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 java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = FullThreeDsFlowConfig.class) +@TestPropertySource(properties = {"server.rest.port=8083", + "error-mapping.file=classpath:fixture/errors.json", + "service.secret.enabled=true"}) +public class ForwardRecurrentPaymentNon3dsTest extends AbstractPaymentTest { + + public static final String RECURRENT_TOKEN = "recurrentToken"; + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + MockUtil.mockAllWithout3Ds(cdsClientStorage, benderClient); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + baseResponseModel.setRecurrentToken(RECURRENT_TOKEN); + + Mockito.when(client.auth(any())).thenReturn(baseResponseModel); + Mockito.when(client.pay(any())).thenReturn(baseResponseModel); + Mockito.when(client.capture(any())).thenReturn(baseResponseModel); + } + + @Test + public void testOneStage() throws TException { + // pay + Map options = MockUtil.buildOptionsOneStage(); + testRecurrentForward(options); + } + + @Test + public void testTwoStage() throws TException { + // pay + Map options = MockUtil.buildOptionsTwoStage(); + testRecurrentForward(options); + } + + private void testRecurrentForward(Map options) throws TException { + PaymentContext paymentContext = MockUtil.buildPaymentContext(String.valueOf(new Date().getTime()), options); + paymentContext.getPaymentInfo().getPayment().setMakeRecurrent(true); + + PaymentProxyResult paymentProxyResult = processWithDoNothingSuccessResult(paymentContext); + String token = paymentProxyResult.getIntent().getFinish().getStatus().getSuccess().getToken(); + + //capture + paymentProxyResult = checkSuccessCapture(paymentContext, paymentProxyResult, new byte[] {}); + String tokenCapture = paymentProxyResult.getIntent().getFinish().getStatus().getSuccess().getToken(); + assertEquals(token, tokenCapture); + + //recurrent payment + paymentContext = MockUtil.buildRecurrentPaymentContext(String.valueOf(new Date().getTime()), token); + + paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext); + assertNotNull(paymentProxyResult.getTrx().getId()); + assertEquals(Step.DO_NOTHING, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + + // captured + checkSuccessCapture(paymentContext, paymentProxyResult, paymentProxyResult.getNextState()); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/GenerateToken3ds1Test.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/GenerateToken3ds1Test.java new file mode 100644 index 0000000..395b055 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/GenerateToken3ds1Test.java @@ -0,0 +1,101 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +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.full.three.ds.config.FullThreeDsFlowConfig; +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.RecurrentTokenCallbackResult; +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.nio.ByteBuffer; +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 = FullThreeDsFlowConfig.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(cdsClientStorage, 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.auth(any())).thenReturn(baseResponseModel); + Mockito.when(client.pay(any())).thenReturn(baseResponseModel); + + Mockito.when(client.finish3ds(any())).thenReturn(baseResponseModelRec); + Mockito.when(client.refund(any())).thenReturn(baseResponseModelRec); + Mockito.when(client.capture(any())).thenReturn(baseResponseModelRec); + } + + @Test + public void testPaymentOneStage() throws TException, JsonProcessingException { + // pay + Map options = MockUtil.buildOptionsOneStage(); + testPayment(options); + } + + @Test + public void testPaymentTwoStage() throws TException, JsonProcessingException { + // auth + Map options = MockUtil.buildOptionsTwoStage(); + testPayment(options); + } + + private void testPayment(Map options) throws TException, JsonProcessingException { + RecurrentTokenContext paymentContext = MockUtil.buildRecurrentTokenContext(String.valueOf(new Date().getTime()), + options); + RecurrentTokenProxyResult paymentProxyResult = serverHandlerLogDecorator.generateToken(paymentContext); + assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetRedirect()); + assertEquals(Step.FINISH_THREE_DS_V1, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + + ByteBuffer byteBuffer = BeanUtils.createParesBuffer("pares", "md"); + paymentContext.getTokenInfo().setTrx(paymentProxyResult.getTrx()); + paymentContext.getSession().setState(paymentProxyResult.getNextState()); + RecurrentTokenCallbackResult paymentCallbackResult = serverHandlerLogDecorator.handleRecurrentTokenCallback( + byteBuffer, + paymentContext); + + //finish three ds + paymentProxyResult = checkSuccessFinishThreeDs(paymentContext, paymentProxyResult, paymentCallbackResult); + + + //capture + paymentProxyResult = checkSleepWithStatus(Step.REFUND, paymentContext, paymentProxyResult, + paymentProxyResult.getNextState()); + + //refund + checkSuccessRefund(paymentContext, paymentProxyResult, paymentProxyResult.getNextState()); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/GenerateTokenNon3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/GenerateTokenNon3dsTest.java new file mode 100644 index 0000000..2fa6e56 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/GenerateTokenNon3dsTest.java @@ -0,0 +1,74 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +import dev.vality.adapter.flow.lib.constant.Step; +import dev.vality.adapter.flow.lib.flow.AbstractGenerateTokenTest; +import dev.vality.adapter.flow.lib.flow.full.three.ds.config.FullThreeDsFlowConfig; +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.mockito.ArgumentMatchers.any; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = FullThreeDsFlowConfig.class) +@TestPropertySource(properties = {"server.rest.port=8083", + "error-mapping.file=classpath:fixture/errors.json", + "service.secret.enabled=true"}) +public class GenerateTokenNon3dsTest extends AbstractGenerateTokenTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + MockUtil.mockAllWithout3Ds(cdsClientStorage, benderClient); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + baseResponseModel.setRecurrentToken(RECURRENT_TOKEN); + + Mockito.when(client.auth(any())).thenReturn(baseResponseModel); + Mockito.when(client.pay(any())).thenReturn(baseResponseModel); + Mockito.when(client.capture(any())).thenReturn(baseResponseModel); + Mockito.when(client.refund(any())).thenReturn(baseResponseModel); + } + + @Test + public void testPaymentOneStage() throws TException { + // pay + Map options = MockUtil.buildOptionsOneStage(); + testPayment(options); + } + + @Test + public void testPaymentTwoStage() throws TException { + // auth + Map options = MockUtil.buildOptionsTwoStage(); + testPayment(options); + } + + private void testPayment(Map options) throws TException { + RecurrentTokenContext paymentContext = MockUtil.buildRecurrentTokenContext(String.valueOf(new Date().getTime()), + options); + RecurrentTokenProxyResult paymentProxyResult = checkSuccessAuthOrPay(paymentContext); + + //capture + paymentProxyResult = checkSleepWithStatus(Step.REFUND, paymentContext, paymentProxyResult, + paymentProxyResult.getNextState()); + + //refund + checkSuccessRefund(paymentContext, paymentProxyResult, paymentProxyResult.getNextState()); + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentNon3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentNon3dsTest.java new file mode 100644 index 0000000..5eb0520 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentNon3dsTest.java @@ -0,0 +1,65 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +import dev.vality.adapter.flow.lib.flow.AbstractPaymentTest; +import dev.vality.adapter.flow.lib.flow.full.three.ds.config.FullThreeDsFlowConfig; +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 java.util.Map; + +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = FullThreeDsFlowConfig.class) +@TestPropertySource(properties = {"server.rest.port=8083", + "error-mapping.file=classpath:fixture/errors.json", + "service.secret.enabled=true"}) +public class PaymentNon3dsTest extends AbstractPaymentTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + MockUtil.mockAllWithout3Ds(cdsClientStorage, benderClient); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + Mockito.when(client.auth(any())).thenReturn(baseResponseModel); + Mockito.when(client.pay(any())).thenReturn(baseResponseModel); + Mockito.when(client.capture(any())).thenReturn(baseResponseModel); + } + + @Test + public void testPaymentOneStage() throws TException { + // pay + Map options = MockUtil.buildOptionsOneStage(); + testPayment(options); + } + + @Test + public void testPaymentTwoStage() throws TException { + // auth + Map options = MockUtil.buildOptionsTwoStage(); + testPayment(options); + } + + private void testPayment(Map options) throws TException { + PaymentContext paymentContext = MockUtil.buildPaymentContext(String.valueOf(new Date().getTime()), + options); + PaymentProxyResult paymentProxyResult = processWithDoNothingSuccessResult(paymentContext); + + //capture + checkSuccessCapture(paymentContext, paymentProxyResult, new byte[] {}); + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds1Test.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds1Test.java new file mode 100644 index 0000000..1522ff6 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds1Test.java @@ -0,0 +1,103 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +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.AbstractPaymentTest; +import dev.vality.adapter.flow.lib.flow.full.three.ds.config.FullThreeDsFlowConfig; +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.PaymentCallbackResult; +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.nio.ByteBuffer; +import java.util.Map; + +import static dev.vality.adapter.flow.lib.flow.utils.BeanUtils.createParesBuffer; +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 = FullThreeDsFlowConfig.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 PaymentSuccess3ds1Test extends AbstractPaymentTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + + MockUtil.mock3ds1CardData(cdsClientStorage); + MockUtil.mock3ds1SessionData(cdsClientStorage); + MockUtil.mockIdGenerator(benderClient); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + baseResponseModel.setThreeDsData(BeanUtils.create3Ds1(baseResponseModel)); + 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.finish3ds(any())).thenReturn(successResponseModel); + Mockito.when(client.refund(any())).thenReturn(successResponseModel); + } + + @Test + public void testOneStage() throws TException, IOException { + // auth + Map options = MockUtil.buildOptionsOneStage(); + test3ds1(options); + } + + @Test + public void testTwoStage() throws TException, IOException { + // auth + Map options = MockUtil.buildOptionsTwoStage(); + test3ds1(options); + } + + private void test3ds1(Map options) throws TException, JsonProcessingException { + PaymentContext paymentContext = MockUtil.buildPaymentContext("invoice_id", options); + + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetRedirect()); + assertEquals(Step.FINISH_THREE_DS_V1, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + + ByteBuffer byteBuffer = createParesBuffer("pares", "md"); + paymentContext.getPaymentInfo().getPayment().setTrx(paymentProxyResult.getTrx()); + paymentContext.getSession().setState(paymentProxyResult.getNextState()); + PaymentCallbackResult paymentCallbackResult = serverHandlerLogDecorator.handlePaymentCallback(byteBuffer, + paymentContext); + + //finish three ds + paymentProxyResult = checkSuccessFinishThreeDs(paymentContext, paymentProxyResult, paymentCallbackResult); + + //capture + PaymentProxyResult paymentProxyResultDeposit = + checkSuccessCapture(paymentContext, paymentProxyResult, new byte[] {}); + + //refund + checkSuccessRefund(1100L, paymentContext, paymentProxyResultDeposit); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds2FullTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds2FullTest.java new file mode 100644 index 0000000..12a9b60 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds2FullTest.java @@ -0,0 +1,115 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +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.AbstractPaymentTest; +import dev.vality.adapter.flow.lib.flow.full.three.ds.config.FullThreeDsFlowConfig; +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.PaymentCallbackResult; +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.nio.ByteBuffer; +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 = FullThreeDsFlowConfig.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 PaymentSuccess3ds2FullTest extends AbstractPaymentTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + + MockUtil.mock3ds1CardData(cdsClientStorage); + MockUtil.mock3ds1SessionData(cdsClientStorage); + MockUtil.mockIdGenerator(benderClient); + + BaseResponseModel baseResponseModelCheck = BeanUtils.createBaseResponseModel(); + baseResponseModelCheck.setThreeDsData(BeanUtils.create3Ds2FullCheck()); + baseResponseModelCheck.setStatus(Status.NEED_REDIRECT); + + BaseResponseModel baseResponseModelFinish = BeanUtils.createBaseResponseModel(); + baseResponseModelFinish.setThreeDsData(BeanUtils.create3Ds2FullFinish()); + baseResponseModelFinish.setStatus(Status.NEED_REDIRECT); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + + Mockito.when(client.auth(any())).thenReturn(baseResponseModelCheck); + Mockito.when(client.pay(any())).thenReturn(baseResponseModelCheck); + Mockito.when(client.capture(any())).thenReturn(baseResponseModel); + Mockito.when(client.finish3ds(any())).thenReturn(baseResponseModel); + Mockito.when(client.check3dsV2(any())).thenReturn(baseResponseModelFinish); + Mockito.when(client.finish3dsV2(any())).thenReturn(baseResponseModel); + Mockito.when(client.refund(any())).thenReturn(baseResponseModel); + } + + @Test + public void testOneStage() throws TException, IOException { + Map options = MockUtil.buildOptionsOneStage(); + test3ds2Full(options); + } + + @Test + public void testTwoStage() throws TException, IOException { + Map options = MockUtil.buildOptionsTwoStage(); + test3ds2Full(options); + } + + private void test3ds2Full(Map options) throws TException, JsonProcessingException { + // auth + PaymentContext paymentContext = MockUtil.buildPaymentContext("invoice_id", options); + + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetRedirect()); + assertEquals(Step.CHECK_NEED_3DS_V2, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + + ByteBuffer byteBuffer = BeanUtils.createSessionBuffer("methodData", "threeDSSessionData"); + paymentContext.getPaymentInfo().getPayment().setTrx(paymentProxyResult.getTrx()); + paymentContext.getSession().setState(paymentProxyResult.getNextState()); + PaymentCallbackResult paymentCallbackResult = serverHandlerLogDecorator.handlePaymentCallback(byteBuffer, + paymentContext); + + paymentProxyResult = + checkSuspend(Step.FINISH_THREE_DS_V2, paymentContext, paymentProxyResult, paymentCallbackResult); + + byteBuffer = BeanUtils.createCresBuffer("cres", "threeDSSessionData"); + paymentContext.getPaymentInfo().getPayment().setTrx(paymentProxyResult.getTrx()); + paymentContext.getSession().setState(paymentProxyResult.getNextState()); + paymentCallbackResult = serverHandlerLogDecorator.handlePaymentCallback(byteBuffer, paymentContext); + + //finish three ds + paymentProxyResult = checkSuccessFinishThreeDs(paymentContext, paymentProxyResult, paymentCallbackResult); + + //capture + PaymentProxyResult paymentProxyResultDeposit = + checkSuccessCapture(paymentContext, paymentProxyResult, new byte[] {}); + + //refund + checkSuccessRefund(1100L, paymentContext, paymentProxyResultDeposit); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds2SimpleTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds2SimpleTest.java new file mode 100644 index 0000000..f356d05 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/PaymentSuccess3ds2SimpleTest.java @@ -0,0 +1,109 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +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.AbstractPaymentTest; +import dev.vality.adapter.flow.lib.flow.full.three.ds.config.FullThreeDsFlowConfig; +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.PaymentCallbackResult; +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.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +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.nio.ByteBuffer; +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 = FullThreeDsFlowConfig.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 PaymentSuccess3ds2SimpleTest extends AbstractPaymentTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + + MockUtil.mock3ds1CardData(cdsClientStorage); + MockUtil.mock3ds1SessionData(cdsClientStorage); + MockUtil.mockIdGenerator(benderClient); + + BaseResponseModel baseResponseModelCheck = BeanUtils.createBaseResponseModel(); + baseResponseModelCheck.setThreeDsData(BeanUtils.create3Ds2FullCheck()); + baseResponseModelCheck.setStatus(Status.NEED_REDIRECT); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + + Mockito.when(client.auth(any())).thenReturn(baseResponseModelCheck); + Mockito.when(client.pay(any())).thenReturn(baseResponseModelCheck); + Mockito.when(client.capture(any())).thenReturn(baseResponseModel); + Mockito.when(client.check3dsV2(any())).thenReturn(baseResponseModel); + Mockito.when(client.refund(any())).thenReturn(baseResponseModel); + } + + @Test + public void testOneStage() throws TException, IOException { + Map options = MockUtil.buildOptionsOneStage(); + test3ds2Full(options); + } + + @Test + public void testTwoStage() throws TException, IOException { + Map options = MockUtil.buildOptionsTwoStage(); + test3ds2Full(options); + } + + private void test3ds2Full(Map options) throws TException, JsonProcessingException { + // auth + PaymentContext paymentContext = MockUtil.buildPaymentContext("invoice_id", options); + + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetRedirect()); + assertEquals(Step.CHECK_NEED_3DS_V2, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + baseResponseModel.setThreeDsData(null); + Mockito.when(client.check3dsV2(any())).thenReturn(baseResponseModel); + + ByteBuffer byteBuffer = BeanUtils.createSessionBuffer("cres", "threeDSMethodData"); + paymentContext.getPaymentInfo().getPayment().setTrx(paymentProxyResult.getTrx()); + paymentContext.getSession().setState(paymentProxyResult.getNextState()); + PaymentCallbackResult paymentCallbackResult = serverHandlerLogDecorator.handlePaymentCallback(byteBuffer, + paymentContext); + + //finish three ds + paymentProxyResult = checkSuccessFinishThreeDs(paymentContext, paymentProxyResult, paymentCallbackResult); + + //capture + PaymentProxyResult paymentProxyResultDeposit = + checkSuccessCapture(paymentContext, paymentProxyResult, new byte[] {}); + + //refund + checkSuccessRefund(1100L, paymentContext, paymentProxyResultDeposit); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/RefundPaymentNon3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/RefundPaymentNon3dsTest.java new file mode 100644 index 0000000..b408b02 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/RefundPaymentNon3dsTest.java @@ -0,0 +1,67 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds; + +import dev.vality.adapter.flow.lib.flow.AbstractPaymentTest; +import dev.vality.adapter.flow.lib.flow.full.three.ds.config.FullThreeDsFlowConfig; +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 java.util.Map; + +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = FullThreeDsFlowConfig.class) +@TestPropertySource(properties = {"server.rest.port=8083", + "error-mapping.file=classpath:fixture/errors.json", + "service.secret.enabled=true"}) +public class RefundPaymentNon3dsTest extends AbstractPaymentTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + MockUtil.mockAllWithout3Ds(cdsClientStorage, benderClient); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + Mockito.when(client.auth(any())).thenReturn(baseResponseModel); + Mockito.when(client.pay(any())).thenReturn(baseResponseModel); + Mockito.when(client.refund(any())).thenReturn(baseResponseModel); + } + + @Test + public void testOneStage() throws TException { + testRefund(MockUtil.buildOptionsOneStage()); + } + + @Test + public void testTwoStage() throws TException { + testRefund(MockUtil.buildOptionsOneStage()); + } + + public void testRefund(Map options) throws TException { + + // auth + PaymentContext paymentContext = MockUtil.buildPaymentContext(String.valueOf(new Date().getTime()), options); + PaymentProxyResult paymentProxyResult = processWithDoNothingSuccessResult(paymentContext); + + //capture + PaymentProxyResult paymentProxyResultDeposit = + checkSuccessCapture(paymentContext, paymentProxyResult, new byte[] {}); + + //refund + checkSuccessRefund(1100L, paymentContext, paymentProxyResultDeposit); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/config/FullThreeDsFlowConfig.java b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/config/FullThreeDsFlowConfig.java new file mode 100644 index 0000000..e8f3d44 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/full/three/ds/config/FullThreeDsFlowConfig.java @@ -0,0 +1,104 @@ +package dev.vality.adapter.flow.lib.flow.full.three.ds.config; + +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.entry.RecCtxToEntryModelConverter; +import dev.vality.adapter.flow.lib.converter.exit.ExitModelToProxyResultConverter; +import dev.vality.adapter.flow.lib.converter.exit.ExitModelToRecTokenProxyResultConverter; +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.full.FullThreeDsAllVersionsStepResolverImpl; +import dev.vality.adapter.flow.lib.flow.full.GenerateTokenFullThreeDsAllVersionsStepResolverImpl; +import dev.vality.adapter.flow.lib.flow.full.GenerateTokenResultIntentResolverImpl; +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.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.Processor; +import dev.vality.adapter.flow.lib.service.IntentResultFactory; +import dev.vality.adapter.flow.lib.service.RecurrentIntentResultFactory; +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 java.util.List; + +@Configuration +public class FullThreeDsFlowConfig { + + @Bean + public StepResolver fullThreeDsAllVersionsStepResolverImpl() { + return new FullThreeDsAllVersionsStepResolverImpl(); + } + + + @Bean + public StepResolver generateTokenFullThreeDsAllVersionsStepResolverImpl() { + return new GenerateTokenFullThreeDsAllVersionsStepResolverImpl(); + } + + @Bean + public ServerFlowHandler serverFlowHandler( + RemoteClient client, + EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter, + Processor baseProcessor, + StepResolver fullThreeDsAllVersionsStepResolverImpl, + CtxToEntryModelConverter ctxToEntryModelConverter, + ExitModelToProxyResultConverter exitModelToProxyResultConverter) { + return new ServerFlowHandler<>( + getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor), + fullThreeDsAllVersionsStepResolverImpl, + ctxToEntryModelConverter, + exitModelToProxyResultConverter); + } + + @Bean + public ServerFlowHandler generateTokenFlowHandler( + RemoteClient client, + EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter, + Processor baseProcessor, + StepResolver generateTokenFullThreeDsAllVersionsStepResolverImpl, + RecCtxToEntryModelConverter recCtxToEntryStateModelConverter, + ExitModelToRecTokenProxyResultConverter exitModelToRecTokenProxyResultConverter + ) { + return new ServerFlowHandler<>( + getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor), + generateTokenFullThreeDsAllVersionsStepResolverImpl, + recCtxToEntryStateModelConverter, + exitModelToRecTokenProxyResultConverter); + } + + @Bean + public RecurrentResultIntentResolver recurrentResultIntentResolver( + RecurrentIntentResultFactory recurrentIntentResultFactory) { + return new GenerateTokenResultIntentResolverImpl(recurrentIntentResultFactory); + } + + @Bean + public ResultIntentResolver resultIntentResolver(IntentResultFactory intentResultFactory) { + return new ResultIntentResolverImpl(intentResultFactory); + } + + private List> getHandlers( + RemoteClient client, + EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter, + Processor baseProcessor) { + return List.of(new AuthHandler(client, entryModelToBaseRequestModelConverter, baseProcessor), + new CancelHandler(client, entryModelToBaseRequestModelConverter, baseProcessor), + new CaptureHandler(client, entryModelToBaseRequestModelConverter, baseProcessor), + new Check3dsV2Handler(client, entryModelToBaseRequestModelConverter, baseProcessor), + new DoNothingHandler(), + new Finish3dsHandler(client, entryModelToBaseRequestModelConverter, baseProcessor), + new Finish3dsV2Handler(client, entryModelToBaseRequestModelConverter, baseProcessor), + new PaymentHandler(client, entryModelToBaseRequestModelConverter, baseProcessor), + new RefundHandler(client, entryModelToBaseRequestModelConverter, baseProcessor)); + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/service/ExponentialBackOffPollingServiceTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/service/ExponentialBackOffPollingServiceTest.java new file mode 100644 index 0000000..2c5789c --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/service/ExponentialBackOffPollingServiceTest.java @@ -0,0 +1,91 @@ +package dev.vality.adapter.flow.lib.flow.service; + +import dev.vality.adapter.flow.lib.model.PollingInfo; +import dev.vality.adapter.flow.lib.service.ExponentialBackOffPollingService; +import dev.vality.adapter.flow.lib.utils.backoff.BackOffExecution; +import dev.vality.adapter.flow.lib.utils.backoff.TimeOptionsExtractors; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ExponentialBackOffPollingServiceTest { + + ExponentialBackOffPollingService exponentialBackOffPollingService = new ExponentialBackOffPollingService(); + + @Test + public void testPrepareBackOffExecution() throws InterruptedException { + PollingInfo pollingInfo = new PollingInfo(); + BackOffExecution backOffExecution = + exponentialBackOffPollingService.prepareBackOffExecution(pollingInfo, new HashMap<>()); + Long result = backOffExecution.nextBackOff(); + assertEquals(2, result.longValue()); + pollingInfo.setMaxDateTimePolling(Instant.now()); + backOffExecution = exponentialBackOffPollingService.prepareBackOffExecution(pollingInfo, new HashMap<>()); + result = backOffExecution.nextBackOff(); + assertEquals(2, result.longValue()); + } + + @Test + public void testPrepareNextPollingIntervalWithDefaultValues() throws InterruptedException { + PollingInfo pollingInfo = new PollingInfo(); + Instant now = Instant.now(); + pollingInfo.setStartDateTimePolling(now); + BackOffExecution backOffExecution = + exponentialBackOffPollingService.prepareBackOffExecution(pollingInfo, new HashMap<>()); + Long result = backOffExecution.nextBackOff(); + assertEquals(2, result.longValue()); + + Thread.sleep(result * 1000L); + int nextInterval = exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, new HashMap<>()); + assertEquals(4, nextInterval); + + Thread.sleep(nextInterval * 1000L); + nextInterval = exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, new HashMap<>()); + assertEquals(8, nextInterval); + } + + @Test + public void testPrepareNextPollingIntervalWithInitialEq1() throws InterruptedException { + PollingInfo pollingInfo = new PollingInfo(); + Instant now = Instant.now(); + pollingInfo.setStartDateTimePolling(now); + HashMap options = new HashMap<>(); + options.put(TimeOptionsExtractors.DEFAULT_INITIAL_EXPONENTIAL_SEC, "1"); + BackOffExecution backOffExecution = + exponentialBackOffPollingService.prepareBackOffExecution(pollingInfo, options); + Long result = backOffExecution.nextBackOff(); + assertEquals(1, result.longValue()); + + Thread.sleep(result * 1000L); + int nextInterval = exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, options); + assertEquals(2, nextInterval); + + Thread.sleep(nextInterval * 1000L); + nextInterval = exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, options); + assertEquals(4, nextInterval); + } + + @Test + public void testPrepareNextPollingIntervalWithExponentialEq1() throws InterruptedException { + PollingInfo pollingInfo = new PollingInfo(); + Instant now = Instant.now(); + pollingInfo.setStartDateTimePolling(now); + HashMap options = new HashMap<>(); + options.put("exponential", "1"); + BackOffExecution backOffExecution = + exponentialBackOffPollingService.prepareBackOffExecution(pollingInfo, options); + Long result = backOffExecution.nextBackOff(); + assertEquals(2, result.longValue()); + + Thread.sleep(result * 1000L); + int nextInterval = exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, options); + assertEquals(2, nextInterval); + + Thread.sleep(nextInterval * 1000L); + nextInterval = exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, options); + assertEquals(2, nextInterval); + } +} \ No newline at end of file diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/ErrorPaymentNon3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/ErrorPaymentNon3dsTest.java new file mode 100644 index 0000000..3a741c7 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/ErrorPaymentNon3dsTest.java @@ -0,0 +1,54 @@ +package dev.vality.adapter.flow.lib.flow.simple.redirect; + +import dev.vality.adapter.flow.lib.flow.AbstractPaymentTest; +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.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 = SimpleRedirectWithPollingDsFlowConfig.class) +@TestPropertySource(properties = {"server.rest.port=8083", + "error-mapping.file=classpath:fixture/errors.json"}) +public class ErrorPaymentNon3dsTest extends AbstractPaymentTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + MockUtil.mockAllWithout3Ds(cdsClientStorage, 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()); + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/ForwardRecurrentPaymentNon3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/ForwardRecurrentPaymentNon3dsTest.java new file mode 100644 index 0000000..5965a70 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/ForwardRecurrentPaymentNon3dsTest.java @@ -0,0 +1,123 @@ +package dev.vality.adapter.flow.lib.flow.simple.redirect; + +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.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.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 java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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 ForwardRecurrentPaymentNon3dsTest extends AbstractPaymentTest { + + public static final String RECURRENT_TOKEN = "recurrentToken"; + public static final String RESULT_ID = "RESULT_ID"; + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + MockUtil.mockAllWithout3Ds(cdsClientStorage, benderClient); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + baseResponseModel.setRecurrentToken(RECURRENT_TOKEN); + baseResponseModel.setStatus(Status.NEED_RETRY); + + Mockito.when(client.auth(any())).thenReturn(baseResponseModel); + Mockito.when(client.pay(any())).thenReturn(baseResponseModel); + Mockito.when(client.status(any())).thenReturn(baseResponseModel); + Mockito.when(client.capture(any())).thenReturn(baseResponseModel); + } + + @Test + public void testOneStage() throws TException { + // pay + Map options = MockUtil.buildOptionsOneStage(); + testRecurrentForward(options); + } + + @Test + public void testTwoStage() throws TException { + // pay + Map options = MockUtil.buildOptionsTwoStage(); + testRecurrentForward(options); + } + + private void testRecurrentForward(Map options) throws TException { + PaymentContext paymentContext = MockUtil.buildPaymentContext(String.valueOf(new Date().getTime()), options); + paymentContext.getPaymentInfo().getPayment().setMakeRecurrent(true); + + PaymentProxyResult paymentProxyResult = processWithCheckStatusResult(paymentContext); + + paymentProxyResult = processWithCheckStatusResult(paymentContext, paymentProxyResult); + + BaseResponseModel successResult = BeanUtils.createBaseResponseModel(); + successResult.setProviderTrxId(RESULT_ID); + successResult.setRecurrentToken(RECURRENT_TOKEN); + successResult.setStatus(Status.SUCCESS); + Mockito.when(client.status(any())).thenReturn(successResult); + + paymentProxyResult = processWithDoNothingSuccessResult(paymentContext, paymentProxyResult); + String token = paymentProxyResult.getIntent().getFinish().getStatus().getSuccess().getToken(); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + baseResponseModel.setStatus(Status.SUCCESS); + Mockito.when(client.capture(any())).thenReturn(baseResponseModel); + + //capture + if (Stage.ONE.equals(options.get(OptionFields.STAGE.name()))) { + paymentProxyResult = checkSuccessCapture(paymentContext, paymentProxyResult, new byte[] {}); + paymentProxyResult = processWithDoNothingSuccessResult(paymentContext, paymentProxyResult); + String tokenCapture = paymentProxyResult.getIntent().getFinish().getStatus().getSuccess().getToken(); + assertEquals(token, tokenCapture); + } else { + paymentProxyResult = processCaptureWithCheckStatusResult(paymentContext, paymentProxyResult, new byte[] {}); + paymentProxyResult = processWithCheckStatusResult(paymentContext, paymentProxyResult); + String tokenCapture = paymentProxyResult.getIntent().getFinish().getStatus().getSuccess().getToken(); + assertEquals(token, tokenCapture); + } + + + baseResponseModel = BeanUtils.createBaseResponseModel(); + Mockito.when(client.auth(any())).thenReturn(baseResponseModel); + Mockito.when(client.pay(any())).thenReturn(baseResponseModel); + + //recurrent payment + paymentContext = MockUtil.buildRecurrentPaymentContext(String.valueOf(new Date().getTime()), token); + + paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext); + assertNotNull(paymentProxyResult.getTrx().getId()); + assertEquals(Step.CHECK_STATUS, + temporaryContextDeserializer.read(paymentProxyResult.getNextState()).getNextStep()); + + paymentProxyResult = processWithDoNothingSuccessResult(paymentContext, paymentProxyResult); + + // captured + checkSuccessCapture(paymentContext, paymentProxyResult, paymentProxyResult.getNextState()); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/GenerateToken3ds1Test.java b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/GenerateToken3ds1Test.java new file mode 100644 index 0000000..573233e --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/GenerateToken3ds1Test.java @@ -0,0 +1,71 @@ +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(cdsClientStorage, 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 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); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/PaymentNon3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/PaymentNon3dsTest.java new file mode 100644 index 0000000..663341b --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/PaymentNon3dsTest.java @@ -0,0 +1,82 @@ +package dev.vality.adapter.flow.lib.flow.simple.redirect; + +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.flow.AbstractPaymentTest; +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.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 java.util.Map; + +import static dev.vality.adapter.flow.lib.flow.simple.redirect.ForwardRecurrentPaymentNon3dsTest.RESULT_ID; +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 PaymentNon3dsTest extends AbstractPaymentTest { + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + MockUtil.mockAllWithout3Ds(cdsClientStorage, benderClient); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + Mockito.when(client.auth(any())).thenReturn(baseResponseModel); + Mockito.when(client.pay(any())).thenReturn(baseResponseModel); + Mockito.when(client.capture(any())).thenReturn(baseResponseModel); + Mockito.when(client.status(any())).thenReturn(baseResponseModel); + } + + @Test + public void testPaymentOneStage() throws TException { + // pay + Map options = MockUtil.buildOptionsOneStage(); + testPayment(options); + } + + @Test + public void testPaymentTwoStage() throws TException { + // auth + Map options = MockUtil.buildOptionsTwoStage(); + testPayment(options); + } + + private void testPayment(Map options) throws TException { + PaymentContext paymentContext = MockUtil.buildPaymentContext(String.valueOf(new Date().getTime()), + options); + PaymentProxyResult paymentProxyResult = processWithCheckStatusResult(paymentContext); + paymentProxyResult = processWithCheckStatusResult(paymentContext, paymentProxyResult); + + BaseResponseModel successResult = BeanUtils.createBaseResponseModel(); + successResult.setProviderTrxId(RESULT_ID); + successResult.setStatus(Status.SUCCESS); + Mockito.when(client.status(any())).thenReturn(successResult); + + //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); + } + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/PaymentSuccess3dsTest.java b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/PaymentSuccess3dsTest.java new file mode 100644 index 0000000..31f218b --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/PaymentSuccess3dsTest.java @@ -0,0 +1,113 @@ +package dev.vality.adapter.flow.lib.flow.simple.redirect; + +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.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.PaymentCallbackResult; +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.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +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.nio.ByteBuffer; +import java.util.Map; + +import static dev.vality.adapter.flow.lib.flow.utils.BeanUtils.createParesBuffer; +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 = SimpleRedirectWithPollingDsFlowConfig.class) +@TestPropertySource(properties = {"error-mapping.file=classpath:fixture/errors.json", + "adapter.callbackUrl=http://localhost:8080/test", + "server.rest.endpoint=adapter", + "server.rest.port=8083"}) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class PaymentSuccess3dsTest extends AbstractPaymentTest { + + @LocalServerPort + int randomServerPort; + + @BeforeEach + public void setUp() throws TException { + MockitoAnnotations.openMocks(this); + + MockUtil.mock3ds1CardData(cdsClientStorage); + MockUtil.mock3ds1SessionData(cdsClientStorage); + MockUtil.mockIdGenerator(benderClient); + + BaseResponseModel baseResponseModel = BeanUtils.createBaseResponseModel(); + baseResponseModel.setThreeDsData(BeanUtils.create3Ds1(baseResponseModel)); + 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.finish3ds(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 options = MockUtil.buildOptionsOneStage(); + test3ds1(options); + } + + @Test + public void testTwoStage() throws TException, IOException { + // auth + Map options = MockUtil.buildOptionsTwoStage(); + test3ds1(options); + } + + private void test3ds1(Map options) throws TException, JsonProcessingException { + PaymentContext paymentContext = MockUtil.buildPaymentContext("invoice_id", options); + + PaymentProxyResult paymentProxyResult = serverHandlerLogDecorator.processPayment(paymentContext); + assertTrue(paymentProxyResult.getIntent().getSuspend().getUserInteraction().isSetRedirect()); + 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); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/config/SimpleRedirectWithPollingDsFlowConfig.java b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/config/SimpleRedirectWithPollingDsFlowConfig.java new file mode 100644 index 0000000..44a336d --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/simple/redirect/config/SimpleRedirectWithPollingDsFlowConfig.java @@ -0,0 +1,101 @@ +package dev.vality.adapter.flow.lib.flow.simple.redirect.config; + +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.entry.RecCtxToEntryModelConverter; +import dev.vality.adapter.flow.lib.converter.exit.ExitModelToProxyResultConverter; +import dev.vality.adapter.flow.lib.converter.exit.ExitModelToRecTokenProxyResultConverter; +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.GenerateTokenSimpleRedirectWithPollingStepResolverImpl; +import dev.vality.adapter.flow.lib.flow.simple.SimpleRedirectGenerateTokenResultIntentResolver; +import dev.vality.adapter.flow.lib.flow.simple.SimpleRedirectWIthPollingStepResolverImpl; +import dev.vality.adapter.flow.lib.flow.simple.SimpleRedirectWithPollingResultIntentResolver; +import dev.vality.adapter.flow.lib.handler.CommonHandler; +import dev.vality.adapter.flow.lib.handler.ServerFlowHandler; +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.Processor; +import dev.vality.adapter.flow.lib.service.IntentResultFactory; +import dev.vality.adapter.flow.lib.service.RecurrentIntentResultFactory; +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 java.util.List; + +@Configuration +public class SimpleRedirectWithPollingDsFlowConfig { + + @Bean + public StepResolver stepResolverImpl() { + return new SimpleRedirectWIthPollingStepResolverImpl(); + } + + @Bean + public StepResolver generateTokenStepResolverImpl() { + return new GenerateTokenSimpleRedirectWithPollingStepResolverImpl(); + } + + @Bean + public ServerFlowHandler serverFlowHandler( + RemoteClient client, + EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter, + Processor baseProcessor, + StepResolver stepResolverImpl, + CtxToEntryModelConverter ctxToEntryModelConverter, + ExitModelToProxyResultConverter exitModelToProxyResultConverter) { + return new ServerFlowHandler<>( + getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor), + stepResolverImpl, + ctxToEntryModelConverter, + exitModelToProxyResultConverter); + } + + @Bean + public RecurrentResultIntentResolver recurrentResultIntentResolver( + RecurrentIntentResultFactory recurrentIntentResultFactory) { + return new SimpleRedirectGenerateTokenResultIntentResolver(recurrentIntentResultFactory); + } + + @Bean + public ResultIntentResolver resultIntentResolver(IntentResultFactory intentResultFactory) { + return new SimpleRedirectWithPollingResultIntentResolver(intentResultFactory); + } + + @Bean + public ServerFlowHandler generateTokenFlowHandler( + RemoteClient client, + EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter, + Processor baseProcessor, + StepResolver generateTokenStepResolverImpl, + RecCtxToEntryModelConverter recCtxToEntryStateModelConverter, + ExitModelToRecTokenProxyResultConverter exitModelToRecTokenProxyResultConverter) { + return new ServerFlowHandler<>( + getHandlers(client, entryModelToBaseRequestModelConverter, baseProcessor), + generateTokenStepResolverImpl, + recCtxToEntryStateModelConverter, + exitModelToRecTokenProxyResultConverter); + } + + private List> getHandlers( + RemoteClient client, + EntryModelToBaseRequestModelConverter entryModelToBaseRequestModelConverter, + Processor 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 GenerateTokenHandler(client, entryModelToBaseRequestModelConverter, baseProcessor), + new RefundHandler(client, entryModelToBaseRequestModelConverter, baseProcessor)); + } +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/utils/BeanUtils.java b/src/test/java/dev/vality/adapter/flow/lib/flow/utils/BeanUtils.java new file mode 100644 index 0000000..8e21753 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/utils/BeanUtils.java @@ -0,0 +1,88 @@ +package dev.vality.adapter.flow.lib.flow.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.ThreeDsData; +import dev.vality.java.damsel.converter.CommonConverter; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +public class BeanUtils { + + public static final String TEST_TRX_ID = "testTrxId"; + public static final String PA_RES = "PaRes"; + public static final String MD = "MD"; + public static final String C_RES = "cRes"; + public static final String THREE_DS_SESSION_DATA = "ThreeDSSessionData"; + public static final String THREE_DS_METHOD_DATA = "ThreeDsMethodData"; + public static final String THREE_DS_METHOD_STATE = "ThreeDSMethodState"; + public static final String PA_REQ = "paReq"; + public static final String C_REQ = "cReq"; + + public static BaseResponseModel createBaseResponseModel() { + return BaseResponseModel.builder() + .providerTrxId(TEST_TRX_ID) + .status(Status.SUCCESS) + .build(); + } + + public static ThreeDsData create3Ds1(BaseResponseModel baseResponseModel) { + HashMap parameters = new HashMap<>(); + parameters.put(MD, "test_md"); + parameters.put(PA_REQ, "test_pares"); + return ThreeDsData.builder() + .threeDsType(ThreeDsType.V1) + .acsUrl("http://localhost/3ds") + .parameters(parameters) + .build(); + } + + public static ThreeDsData create3Ds2FullCheck() { + HashMap parameters = new HashMap<>(); + parameters.put(THREE_DS_METHOD_DATA, "test_threeDsMethodData"); + parameters.put(THREE_DS_METHOD_STATE, "test_threeDSMethodState"); + return ThreeDsData.builder() + .threeDsType(ThreeDsType.V2_FULL) + .acsUrl("http://localhost/3ds") + .parameters(parameters) + .build(); + } + + public static ThreeDsData create3Ds2FullFinish() { + HashMap parameters = new HashMap<>(); + parameters.put(C_REQ, "test_creq"); + parameters.put(THREE_DS_SESSION_DATA, "test_threeDSSessionData"); + return ThreeDsData.builder() + .threeDsType(ThreeDsType.V2_FULL) + .acsUrl("http://localhost/3ds") + .parameters(parameters) + .build(); + } + + public static ByteBuffer createParesBuffer(String pares, String md) throws JsonProcessingException { + Map map = new HashMap<>(); + map.put(PA_RES, pares); + map.put(MD, md); + return CommonConverter.mapToByteBuffer(map); + } + + public static ByteBuffer createCresBuffer(String cres, String threeDSSessionData) throws JsonProcessingException { + Map map = new HashMap<>(); + map.put(C_RES, cres); + map.put(THREE_DS_SESSION_DATA, threeDSSessionData); + return CommonConverter.mapToByteBuffer(map); + } + + public static ByteBuffer createSessionBuffer(String methodData, String threeDSSessionData) + throws JsonProcessingException { + Map map = new HashMap<>(); + map.put(THREE_DS_METHOD_DATA, methodData); + map.put(THREE_DS_METHOD_STATE, threeDSSessionData); + return CommonConverter.mapToByteBuffer(map); + } + +} diff --git a/src/test/java/dev/vality/adapter/flow/lib/flow/utils/MockUtil.java b/src/test/java/dev/vality/adapter/flow/lib/flow/utils/MockUtil.java new file mode 100644 index 0000000..3157539 --- /dev/null +++ b/src/test/java/dev/vality/adapter/flow/lib/flow/utils/MockUtil.java @@ -0,0 +1,224 @@ +package dev.vality.adapter.flow.lib.flow.utils; + +import dev.vality.adapter.flow.lib.constant.OptionFields; +import dev.vality.adapter.flow.lib.constant.Stage; +import dev.vality.bender.BenderSrv; +import dev.vality.bender.GenerationResult; +import dev.vality.cds.client.storage.CdsClientStorage; +import dev.vality.cds.storage.*; +import dev.vality.damsel.domain.*; +import dev.vality.damsel.proxy_provider.Cash; +import dev.vality.damsel.proxy_provider.Invoice; +import dev.vality.damsel.proxy_provider.InvoicePayment; +import dev.vality.damsel.proxy_provider.*; +import dev.vality.java.cds.utils.model.CardDataProxyModel; +import org.apache.thrift.TException; +import org.mockito.stubbing.Answer; + +import java.util.Date; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + +public class MockUtil { + + public static final String PAN_SUCCESS_NON3DS = "5543725660917340"; + public static final String PAN_SUCCESS_3DS_1 = "5543735484626654"; + public static final String PAN_SUCCESS_3DS_2 = "2201382000000047"; + public static final String PAN_SUCCESS_3DS_2_SIMPLE_FLOW = "2201382000000013"; + + public static final String CARDHOLDER_NAME = "NONAME"; + + public static final int EXP_MONTH_NON3DS = 01; + public static final int EXP_YEAR_NON3DS = 2022; + + public static final int EXP_MONTH_3DS_2 = 12; + public static final int EXP_YEAR_3DS_2 = 2025; + + public static final String CVV_NON3DS = "087"; + public static final String CVV_3DS_1 = "852"; + public static final String CVV_3DS_2 = "584"; + public static final String CVV_3DS_2_SIMPLE_FLOW = "590"; + + public static void mockAllWithout3Ds(CdsClientStorage cdsClientStorage, BenderSrv.Iface benderClient) + throws TException { + MockUtil.mockCardDataWithout3ds(cdsClientStorage); + MockUtil.mockSessionData(cdsClientStorage); + MockUtil.mockIdGenerator(benderClient); + } + + public static void mockCardDataWithout3ds(CdsClientStorage cdsClientStorage) { + mockCardData(EXP_MONTH_NON3DS, + EXP_YEAR_NON3DS, + CARDHOLDER_NAME, + PAN_SUCCESS_NON3DS, + cdsClientStorage); + } + + public static void mock3ds1CardData(CdsClientStorage cdsClientStorage) { + mockCardData(EXP_MONTH_NON3DS, + EXP_YEAR_NON3DS, + CARDHOLDER_NAME, + PAN_SUCCESS_3DS_1, + cdsClientStorage); + } + + public static void mock3ds2FullFlowCardData(CdsClientStorage cdsClientStorage) { + mockCardData(EXP_MONTH_3DS_2, + EXP_YEAR_3DS_2, + CARDHOLDER_NAME, + PAN_SUCCESS_3DS_2, + cdsClientStorage); + } + + public static void mock3ds2SimpleFlowCardData(CdsClientStorage cdsClientStorage) { + mockCardData(EXP_MONTH_3DS_2, + EXP_YEAR_3DS_2, + CARDHOLDER_NAME, + PAN_SUCCESS_3DS_2_SIMPLE_FLOW, + cdsClientStorage); + } + + private static void mockCardData(int expMonthNon3ds, + int expYearNon3ds, + String cardholderName, + String panSuccessNon3ds, + CdsClientStorage cdsClientStorage) { + doAnswer((Answer) invocationOnMock -> CardDataProxyModel.builder() + .expMonth((byte) expMonthNon3ds) + .expYear((short) expYearNon3ds) + .cardholderName(cardholderName) + .pan(panSuccessNon3ds) + .build()).when(cdsClientStorage).getCardData(any(RecurrentTokenContext.class)); + doAnswer((Answer) invocation -> + new CardData() + .setExpDate(new ExpDate() + .setMonth((byte) expMonthNon3ds) + .setYear((short) expYearNon3ds)) + .setPan(panSuccessNon3ds) + .setCardholderName(cardholderName)) + .when(cdsClientStorage).getCardData(any(PaymentContext.class)); + doAnswer((Answer) invocation -> + new CardData() + .setExpDate(new ExpDate() + .setMonth((byte) expMonthNon3ds) + .setYear((short) expYearNon3ds)) + .setPan(panSuccessNon3ds) + .setCardholderName(cardholderName)) + .when(cdsClientStorage).getCardData(any(String.class)); + } + + public static void mockSessionData(CdsClientStorage cdsClientStorage) { + mockCvv(CVV_NON3DS, cdsClientStorage); + } + + public static void mock3ds1SessionData(CdsClientStorage cdsClientStorage) { + mockCvv(CVV_3DS_1, cdsClientStorage); + } + + public static void mock3ds2FullFlowSessionData(CdsClientStorage cdsClientStorage) { + mockCvv(CVV_3DS_2, cdsClientStorage); + } + + public static void mock3ds2SimpleFlowSessionData(CdsClientStorage cdsClientStorage) { + mockCvv(CVV_3DS_2_SIMPLE_FLOW, cdsClientStorage); + } + + private static void mockCvv(String cvvNon3ds, CdsClientStorage cdsClientStorage) { + doAnswer((Answer) invocation -> + new SessionData(AuthData.card_security_code(new CardSecurityCode(cvvNon3ds)))) + .when(cdsClientStorage).getSessionData(any(RecurrentTokenContext.class)); + doAnswer((Answer) invocation -> + new SessionData(AuthData.card_security_code(new CardSecurityCode(cvvNon3ds)))) + .when(cdsClientStorage).getSessionData(any(PaymentContext.class)); + } + + public static void mockIdGenerator(BenderSrv.Iface benderClient) throws TException { + doAnswer(invocationOnMock -> new GenerationResult().setInternalId(String.valueOf(new Date().getTime()))) + .when(benderClient).generateID(any(), any(), any()); + } + + public static PaymentContext buildPaymentContext(String invoiceId, Map 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(buildPaymentTool()))) + .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 buildOptionsOneStage() { + return Map.of(OptionFields.STAGE.name(), Stage.ONE); + } + + public static Map buildOptionsTwoStage() { + return Map.of(); + } + + public static PaymentTool buildPaymentTool() { + return PaymentTool.bank_card( + new BankCard() + .setToken("kektoken") + .setBin("1234") + .setExpDate(new BankCardExpDate() + .setMonth((byte) EXP_MONTH_NON3DS) + .setYear((short) EXP_YEAR_NON3DS)) + ); + } + + public static PaymentContext buildRecurrentPaymentContext(String invoiceId, String token) { + PaymentContext paymentContext = MockUtil.buildPaymentContext(invoiceId, + MockUtil.buildOptionsOneStage()); + paymentContext.getPaymentInfo().getPayment() + .setPaymentResource(PaymentResource.recurrent_payment_resource(new RecurrentPaymentResource() + .setPaymentTool(MockUtil.buildPaymentTool()) + .setRecToken(token))); + return paymentContext; + } + + public static RecurrentTokenContext buildRecurrentTokenContext(String recurrentId, Map options) { + return new RecurrentTokenContext() + .setSession(new RecurrentTokenSession()) + .setTokenInfo(new RecurrentTokenInfo() + .setPaymentTool(new RecurrentPaymentTool() + .setId(recurrentId) + .setCreatedAt("2016-03-22T06:12:27Z") + .setPaymentResource(new DisposablePaymentResource() + .setPaymentTool(PaymentTool.bank_card(new BankCard() + .setToken("kektoken") + .setBin("1234") + .setExpDate(new BankCardExpDate() + .setMonth((byte) EXP_MONTH_NON3DS) + .setYear((short) EXP_YEAR_NON3DS))))) + .setMinimalPaymentCost(new Cash() + .setAmount(1000) + .setCurrency(new Currency() + .setSymbolicCode("RUB") + .setNumericCode((short) 643))))) + .setOptions(options); + } +} diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml new file mode 100644 index 0000000..db5ae4f --- /dev/null +++ b/src/test/resources/application.yaml @@ -0,0 +1,93 @@ +spring: + application: + name: adapter + description: adapter + main: + allow-bean-definition-overriding: true +--- +management: + security: + flag: false + server: + port: 8083 + metrics: + export: + statsd: + flavor: etsy + enabled: false + prometheus: + enabled: false + endpoint: + health: + show-details: always + metrics: + enabled: true + prometheus: + enabled: true + endpoints: + web: + exposure: + include: health,info,prometheus +--- +info: + version: test + stage: dev +--- +server: + port: 8022 + rest: + port: 8080 + endpoint: adapter +--- +http.properties: + requestTimeout: 60000 + poolTimeout: 10000 + connectionTimeout: 10000 + validationAfterInactivityMs: 3000 + maxTotalPooling: 200 + defaultMaxPerRoute: 200 +--- +hellgate: + client: + adapter: + url: http://127.0.0.1:8023/v1/proxyhost/provider + networkTimeout: 30000 +--- +cds: + client: + storage: + url: http://127.0.0.1:8021/v1/storage + networkTimeout: 5000 +--- +bender: + url: http://bender:8022/kek + networkTimeout: 10000 +--- +adapter: + url: https://egwtest.open.ru/cgi-bin/cgi_link + callbackUrl: http://127.0.0.1:8080 + pathCallbackUrl: /${server.rest.endpoint}/term_url + pathRecurrentCallbackUrl: /${server.rest.endpoint}/rec_term_url + pathCallbackUrlV2: /${server.rest.endpoint}/term_url_v2 + pathRecurrentCallbackUrlV2: /${server.rest.endpoint}/rec_term_url_v2 + successRedirectUrl: https://checkout.empayre.com/v1/finish-interaction.html +--- +service: + secret: + key: testkeyecom19 + enabled: true +--- +time.config: + redirectTimeout: 600 + maxTimePolling: 600 + pollingDelay: 10 +--- +error-mapping: + file: classpath:fixture/errors.json + patternReason: "'%s' - '%s'" # 'code' - 'description' + +spring: + cache: + cache-names: cardData,sessionData + caffeine: + spec: maximumSize=500,expireAfterAccess=100s \ No newline at end of file diff --git a/src/test/resources/fixture/errors.json b/src/test/resources/fixture/errors.json new file mode 100644 index 0000000..e4aaa6f --- /dev/null +++ b/src/test/resources/fixture/errors.json @@ -0,0 +1,6 @@ +[ + { + "codeRegex": ".*", + "mapping": "authorization_failed:unknown" + } +] \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..0b7fc8e --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,10 @@ + + + + + + + + + +