adapter-common-lib/docs/common.md
2022-01-27 12:09:57 +03:00

20 KiB
Raw Blame History

Общие данные по проекту при написании адаптера

Оглавление:

Структура проекта

├── src
│   ├── main
│   │   ├── java
│   │   │   └── dev
│   │   │       └── vality
│   │   │           └── 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, например:

    @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

Конфигурация каждого описанного раннее обработчика. В примере ниже описан обработчик для шага PRE_AUTH

@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

Содержит в себе классы, которые конвертируют входящие модели и структуры в общую модель входа GeneralEntryStateModel с которой далее и работаем.


@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

Содержит в себе классы, которые конвертируют из общей модели выхода в PaymentProxyResult, в них производится анализ на основании шага (step), какой intent подготовить

@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

Содержит классы, которые из общей модели заполняют структуру для подготовки запроса к провайдеру

Например:

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

}

handler

Обработчики шагов.

Выполняют роль конвертации общей модели в запрос и его дальнейшее выполнение с обработкой полученного ответа Handler'ы описываются в CommonHandler, то есть если не использовать CommonHandler, можно и не использовать processor и converter

В примере ниже видно, что данный класс будет использован, если удовлетворит условию step == Step.CANCEL, т.е. в данном случае будет выбран обработчик для вызова отмены платежа, после того, как будет известен обработчик, у него будет вызван метод handle(GeneralEntryStateModel entryStateModel) находящийся в StepHandler

Передаем в StepHandler<Request, Response>(который имплементирует CommonHandler) объекты, которые ожидаем в качестве запроса и ответа от провайдера

Видно, что в качестве объекта для запроса используется класс 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;
    }
}

ps важно помнить, что после добавлении нового handler-а, нужно его прописать в HandlerConfiguration, но до этого, придется еще написать entry converter-ы для handler-а, описать processor и модель ответа, нашем случае это Response

Processor

Процессоры отвечают за подготовку к общей модели выхода (GeneralExitStateModel), описываются в папке processor, а матрешочное поведение описывается в конфигурации ProcessorConfiguration.

Важно помнить, что после создания процессора, его необходимо добавить в конфигурацию.

Пример processor-а:

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

Таким образом видно, что, если условие с ошибкой не отработало

if(response.getErrorCode() != null)

то, будет вызван следующий processor

return nextProcessor.process(response, entryStateModel);

Что, собственно, и описывается в конфигурации, см. раздел конфигурации