adapter-common-lib/docs/common.md

431 lines
20 KiB
Markdown
Raw Normal View History

2020-06-30 08:56:42 +00:00
# Общие данные по проекту при написании адаптера
### Оглавление:
- [Структура проекта](#Структура-проекта)
- [Конфигурация](#configuration)
2022-01-27 09:09:57 +00:00
- [AppConfiguration](#appconfiguration)
- [HandlerConfiguration](#handlerconfiguration)
- [ProcessorConfiguration](#processorconfiguration)
2020-06-30 08:56:42 +00:00
- [Конвертеры](#converter)
2022-01-27 09:09:57 +00:00
- [entry](#entry)
- [exit](#exit)
- [request](#request)
2020-06-30 08:56:42 +00:00
- [flow](#flow)
- [handler](#handler)
- [processor](#processor)
### Структура проекта
```
├── src
│   ├── main
│   │   ├── java
2022-01-27 09:09:57 +00:00
│   │   │   └── dev
│   │   │   └── vality
2020-06-30 08:56:42 +00:00
│   │   │   └── adapter
│   │   │   └── provider
│   │   │   ├── AdapterProviderApplication.java
│   │   │   ├── client - клиент для обращения к провайдеру
│   │   │   │   ├── ProviderClient.java
│   │   │   │   ├── ProviderClientException.java
│   │   │   │   ├── constant
│   │   │   │   └── model
│   │   │   ├── configuration - конфигурационные файлы
│   │   │   │   ├── AppConfiguration.java - основные конфигурации приложения
│   │   │   │   ├── HandlerConfiguration.java - описание handler-ов
│   │   │   │   ├── ProcessorConfiguration.java - описание processor-ов, они вкладываются друг в друга
│   │   │   │   ├── RestTemplateConfiguration.java
│   │   │   │   ├── TomcatEmbeddedConfiguration.java
│   │   │   │   └── properties - параметры
│   │   │   │   ├── IPayProperties.java
│   │   │   │   └── RestTemplateProperties.java
│   │   │   ├── converter
│   │   │   │   ├── entry - конвертация входящих объектов в модель общего входа
│   │   │   │   │   ├── CtxToEntryModelConverter.java
│   │   │   │   │   └── RecCtxToEntryModelConverter.java
│   │   │   │   ├── exit - конвертация исходящих объектов в модель общего выхода
│   │   │   │   │   ├── ExitModelToProxyResultConverter.java
│   │   │   │   │   └── ExitModelToRecTokenProxyResultConverter.java
│   │   │   │   └── request - конвертация из модели общего входа в модели запросов к провайдеру
│   │   │   │   ├── EntryStateToCancelRequestConverter.java
│   │   │   │   ├── EntryStateToCaptureHandler.java
│   │   │   │   ├── EntryStateToRefundRequestConverter.java
│   │   │   │   ├── EntryStateToRegisterPreAuthRequestConverter.java
│   │   │   │   ├── EntryStateToStatusRequestConverter.java
│   │   │   ├── flow
│   │   │   │   └── StepResolverImpl.java - определяет, какой шаг будет на входе и на выходе
│   │   │   ├── handler - Обработчики шагов. Выполняют роль конвертации общей модели в запрос и его дальнейшее выполнение с обработкой полученного ответа
│   │   │   │   ├── CancelHandler.java
│   │   │   │   ├── CapturedHandler.java
│   │   │   │   ├── PaymentOrderHandler.java
│   │   │   │   ├── RefundHandler.java
│   │   │   │   ├── RegisterPreAuthHandler.java
│   │   │   │   ├── StatusHandler.java
│   │   │   │   └── StepHandler.java
│   │   │   ├── processor - процессоры отвечают за подготовку к общей модели выхода, описываются в ProcessorConfiguration
│   │   │   │   ├── CommonErrorProcessor.java
│   │   │   │   └── CommonProcessor.java
│   │   │   ├── service
│   │   │   │   └── AdapterIPayServerHandler.java - в нем описывается общая логика имплементирующая интерфейс для работы с провайдерами
│   │   │   ├── servlet
│   │   │   │   └── IPayAdapterServlet.java
│   │   │   └── validator - необходимые проверки, например, что в опциях пришли ожидаемые параметры
│   │   │   ├── PaymentContextValidator.java
│   │   │   └── RecurrentTokenContextValidator.java
│   │   └── resources
│   │   ├── application.yml
│   │   └── fixture
│   │   └── errors.json - маппинг ошибок
```
### configuration
#### AppConfiguration
основные конфигурации приложения, там находятся общие `beans`, например:
2022-01-27 09:09:57 +00:00
2020-06-30 08:56:42 +00:00
```
@Bean
@ConfigurationProperties("time.config")
public CommonTimerProperties commonTimerProperties() {
return new CommonTimerProperties();
}
@Bean
public ExponentialBackOffPollingService<PollingInfo> exponentialBackOffPollingService() {
return new ExponentialBackOffPollingService<>();
}
@Bean
public PaymentCallbackHandler paymentCallbackHandler(
AdapterDeserializer adapterDeserializer,
AdapterSerializer adapterSerializer,
CallbackDeserializer callbackDeserializer
) {
return new PaymentCallbackHandler(adapterDeserializer, adapterSerializer, callbackDeserializer);
}
```
#### HandlerConfiguration
2022-01-27 09:09:57 +00:00
Конфигурация каждого описанного раннее обработчика. В примере ниже описан обработчик для шага PRE_AUTH
2020-06-30 08:56:42 +00:00
```
@Configuration
@RequiredArgsConstructor
public class HandlerConfiguration {
private final ProviderClient providerClient;
private final EntryStateToRegisterPreAuthRequestConverter entryStateToRegisterPreAuthRequestConverter;
@Bean
public RegisterPreAuthHandler registerPreAuthHandler(Processor<GeneralExitStateModel, Response, GeneralEntryStateModel> processor) {
return new RegisterPreAuthHandler(providerClient, entryStateToRegisterPreAuthRequestConverter, processor);
}
```
#### ProcessorConfiguration
Описывается в цепочку вызовов, матрешечный метод
```
@Configuration
public class ProcessorConfiguration {
@Bean
public Processor<GeneralExitStateModel, Response, GeneralEntryStateModel> commonErrorProcessor(ProviderClient providerClient) {
Processor<GeneralExitStateModel, Response, GeneralEntryStateModel> commonProcessor = new CommonProcessor(providerClient);
return new CommonErrorProcessor(commonProcessor);
}
}
```
### Converter
#### entry
2022-01-27 09:09:57 +00:00
Содержит в себе классы, которые конвертируют входящие модели и структуры в общую модель входа GeneralEntryStateModel с
которой далее и работаем.
2020-06-30 08:56:42 +00:00
```
@Component
@RequiredArgsConstructor
public class CtxToEntryModelConverter implements Converter<PaymentContext, GeneralEntryStateModel> {
private final CdsClientStorage cdsStorage;
private final AdapterDeserializer adapterDeserializer;
@Override
public GeneralEntryStateModel convert(PaymentContext context) {
// any code
return GeneralEntryStateModel.builder()
.trxId(payment.getTrx() != null ? payment.getTrx().getId() : adapterContext.getTrxId())
.trxExtra(payment.getTrx() != null ? payment.getTrx().getExtra() : new HashMap<>())
.orderId(orderId)
.amount(cash.getAmount())
.refundAmount(cash.getAmount())
.currencySymbolCode(payment.getCost().getCurrency().getSymbolicCode())
.currencyCode(payment.getCost().getCurrency().getNumericCode())
.createdAt(createdAt)
.pan(cardData != null ? cardData.getPan() : null)
.cvv2(CardDataUtils.extractCvv2(sessionData))
.expYear(cardData != null ? cardData.getExpYear() : null)
.expMonth(cardData != null ? cardData.getExpMonth() : null)
.cardHolder(cardData != null ? cardData.getCardholderName() : null)
.email(PaymentDataConverter.extractEmail(context.getPaymentInfo()))
.options(paymentContext.getOptions())
.makeRecurrent(payment.make_recurrent)
.callbackUrl(redirectUrl)
.ip(ipAddress)
.trxExtra(extra)
.adapterContext(adapterContext)
.targetStatus(targetStatus)
.build();
}
}
```
#### exit
2022-01-27 09:09:57 +00:00
Содержит в себе классы, которые конвертируют из общей модели выхода в PaymentProxyResult, в них производится анализ на
основании шага (step), какой intent подготовить
2020-06-30 08:56:42 +00:00
```
@Slf4j
@Component
@RequiredArgsConstructor
public class ExitModelToProxyResultConverter implements Converter<GeneralExitStateModel, PaymentProxyResult> {
private final CommonTimerProperties timerProperties;
private final ErrorMapping errorMapping;
private final AdapterSerializer adapterSerializer;
private final ExponentialBackOffPollingService<PollingInfo> exponentialBackOffPollingService;
@Override
public PaymentProxyResult convert(GeneralExitStateModel exitStateModel) {
log.info("PaymentProxy exit state model: {}", exitStateModel);
if (!StringUtils.isEmpty(exitStateModel.getErrorCode())) {
log.info("Exit error code={}, message={}", exitStateModel.getErrorCode(), exitStateModel.getErrorMessage());
Failure failure = errorMapping.mapFailure(exitStateModel.getErrorCode());
return createProxyResultFailure(failure);
}
AdapterContext adapterContext = exitStateModel.getAdapterContext();
Step nextStep = exitStateModel.getAdapterContext().getStep();
Intent intent;
switch (nextStep) {
case AUTH:
intent = createIntentWithSleepIntent(0);
break;
case FINISH_THREE_DS:
intent = createIntentWithSuspendIntent(exitStateModel);
break;
case CHECK_STATUS:
case CAPTURE:
case REFUND:
case CANCEL:
case DO_NOTHING:
intent = createFinishIntentSuccess();
break;
default:
throw new IllegalStateException("Wrong next step: " + nextStep);
}
PaymentProxyResult paymentProxyResult = new PaymentProxyResult(intent).setNextState(adapterSerializer.writeByte(adapterContext));
paymentProxyResult.setTrx(createTransactionInfo(adapterContext.getTrxId(), exitStateModel.getTrxExtra()));
log.info("Payment proxy result: {}", paymentProxyResult);
return paymentProxyResult;
}
}
```
#### request
Содержит классы, которые из общей модели заполняют структуру для подготовки запроса к провайдеру
Например:
2022-01-27 09:09:57 +00:00
2020-06-30 08:56:42 +00:00
```
@Component
public class EntryStateToCancelRequestConverter implements Converter<GeneralEntryStateModel, Request> {
@Override
public Request convert(GeneralEntryStateModel entryStateModel) {
Map<String, String> options = entryStateModel.getOptions();
String userName = options.get(OptionField.LOGIN.getField());
String password = options.get(OptionField.PASSWORD.getField());
return Request.builder()
.userName(userName)
.password(password)
.orderId(entryStateModel.getTrxId())
.build();
}
}
```
## flow
StepResolverImpl - определяет шаг в самом начале и устанавливает шаг на выходе
```
@Slf4j
@Component
public class StepResolverImpl implements StepResolver<GeneralEntryStateModel, GeneralExitStateModel> {
@Override
public Step resolveEntry(GeneralEntryStateModel entryStateModel) {
TargetStatus targetStatus = entryStateModel.getTargetStatus();
AdapterContext adapterContext = entryStateModel.getAdapterContext();
Step step = adapterContext.getStep();
if (targetStatus == null) {
return Objects.requireNonNullElse(step, Step.PRE_AUTH);
}
log.info("Entry resolver. Target status: {}. Step: {}", targetStatus, step);
switch (targetStatus) {
case PROCESSED:
return Objects.requireNonNullElse(step, Step.PRE_AUTH);
case CAPTURED:
return Step.CAPTURE;
case CANCELLED:
return Step.CANCEL;
case REFUNDED:
return Step.REFUND;
default:
throw new IllegalStateException("Unknown target status: " + targetStatus);
}
}
@Override
public Step resolveExit(GeneralExitStateModel exitStateModel) {
GeneralEntryStateModel entryStateModel = exitStateModel.getGeneralEntryStateModel();
Step step = entryStateModel.getAdapterContext().getStep();
log.info("Exit resolver. Step: {}", step);
switch (step) {
case PRE_AUTH:
return Step.AUTH;
case AUTH:
return Step.FINISH_THREE_DS;
case CAPTURE:
return Step.CAPTURE;
case FINISH_THREE_DS:
return Step.CHECK_STATUS;
case CHECK_STATUS:
return Step.DO_NOTHING;
default:
return step;
}
}
}
```
2022-01-27 09:09:57 +00:00
#### handler
2020-06-30 08:56:42 +00:00
Обработчики шагов.
2022-01-27 09:09:57 +00:00
Выполняют роль конвертации общей модели в запрос и его дальнейшее выполнение с обработкой полученного ответа Handler'ы
описываются в CommonHandler, то есть если не использовать CommonHandler, можно и не использовать processor и converter
2020-06-30 08:56:42 +00:00
2022-01-27 09:09:57 +00:00
В примере ниже видно, что данный класс будет использован, если удовлетворит условию `step == Step.CANCEL`, т.е. в данном
случае будет выбран обработчик для вызова отмены платежа, после того, как будет известен обработчик, у него будет вызван
метод handle(GeneralEntryStateModel entryStateModel) находящийся в `StepHandler`
2020-06-30 08:56:42 +00:00
2022-01-27 09:09:57 +00:00
Передаем в `StepHandler<Request, Response>`(который имплементирует CommonHandler) объекты, которые ожидаем в качестве
запроса и ответа от провайдера
2020-06-30 08:56:42 +00:00
Видно, что в качестве объекта для запроса используется класс Request, в качестве ожидаемого ответа Response
Для обращения к провайдеру используется клиент `ProviderClient` с вызовом метода `reverse`.
Конвертор заполнит модель Request данными из модели GeneralEntryStateModel
Процессор на основании полученного `Response` и `GeneralEntryStateModel` заполнит `GeneralExitStateModel`
```
public class CancelHandler extends StepHandler<Request, Response> {
public CancelHandler(
ProviderClient providerClient,
Converter<GeneralEntryStateModel, Request> converter,
Processor<GeneralExitStateModel, Response, GeneralEntryStateModel> processor
) {
super(providerClient::reverse, converter, processor);
}
@Override
public boolean isHandle(GeneralEntryStateModel entryStateModel) {
Step step = entryStateModel.getAdapterContext().getStep();
return step == Step.CANCEL;
}
}
```
2022-01-27 09:09:57 +00:00
ps важно помнить, что после добавлении нового handler-а, нужно его прописать
в [HandlerConfiguration](#handlerconfiguration), но до этого, придется еще
написать [entry](#entry) [converter-ы](#converter) для [handler-а](#handler), описать [processor](#processor) и модель
ответа, нашем случае это Response
2020-06-30 08:56:42 +00:00
#### Processor
2022-01-27 09:09:57 +00:00
Процессоры отвечают за подготовку к общей модели выхода (GeneralExitStateModel), описываются в папке `processor`, а
матрешочное поведение описывается в конфигурации `ProcessorConfiguration`.
2020-06-30 08:56:42 +00:00
Важно помнить, что после создания процессора, его необходимо добавить в [конфигурацию](#processorconfiguration).
Пример processor-а:
2022-01-27 09:09:57 +00:00
2020-06-30 08:56:42 +00:00
```
@Slf4j
@RequiredArgsConstructor
public class CommonErrorProcessor implements Processor<GeneralExitStateModel, Response, GeneralEntryStateModel> {
private final Processor<GeneralExitStateModel, Response, GeneralEntryStateModel> nextProcessor;
@Override
public GeneralExitStateModel process(Response response, GeneralEntryStateModel entryStateModel) {
log.info("Order response: {}", response);
AdapterContext adapterContext = entryStateModel.getAdapterContext();
if(response.getErrorCode() != null) {
ErrorCode errorCode = ErrorCode.findByCode(response.getErrorCode());
return GeneralExitStateModel.builder()
.errorCode(response.getErrorCode())
.errorMessage(errorCode.getDescription())
.adapterContext(adapterContext)
.generalEntryStateModel(entryStateModel)
.trxExtra(new HashMap<>(entryStateModel.getTrxExtra()))
.build();
} else if (nextProcessor != null) {
return nextProcessor.process(response, entryStateModel);
}
return null;
}
}
```
Таким образом видно, что, если условие с ошибкой не отработало
2022-01-27 09:09:57 +00:00
2020-06-30 08:56:42 +00:00
```
if(response.getErrorCode() != null)
```
2022-01-27 09:09:57 +00:00
2020-06-30 08:56:42 +00:00
то, будет вызван следующий processor
2022-01-27 09:09:57 +00:00
2020-06-30 08:56:42 +00:00
```
return nextProcessor.process(response, entryStateModel);
```
Что, собственно, и описывается в конфигурации, см. раздел [конфигурации](#processorconfiguration)