Merge pull request #2 from rbkmoney/ft/JD-599/payments-search

JD-599: Payments search impl
This commit is contained in:
Egor Cherniak 2021-10-14 16:11:06 +03:00 committed by GitHub
commit 97fd4e130e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 4974 additions and 0 deletions

78
.gitignore vendored Normal file
View File

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

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "build_utils"]
path = build_utils
url = git@github.com:rbkmoney/build_utils.git
branch = master

15
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,15 @@
#!groovy
build('anapi-v2', 'java-maven') {
checkoutRepo()
loadBuildUtils()
def javaServicePipeline
runStage('load JavaService pipeline') {
javaServicePipeline = load("build_utils/jenkins_lib/pipeJavaServiceInsideDocker.groovy")
}
def serviceName = env.REPO_NAME
def mvnArgs = '-DjvmArgs="-Xmx256m"'
javaServicePipeline(serviceName, mvnArgs)
}

1
build_utils Submodule

@ -0,0 +1 @@
Subproject commit c12c9dd296912ec3c34ad443b448df98fef2556a

313
pom.xml Normal file
View File

@ -0,0 +1,313 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.rbkmoney</groupId>
<artifactId>service-parent-pom</artifactId>
<version>2.0.7</version>
</parent>
<artifactId>anapi-v2</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>anapi-v2</name>
<description>anapi v2</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>15</java.version>
<server.port>8022</server.port>
<management.port>8023</management.port>
<exposed.ports>${server.port} ${management.port}</exposed.ports>
<dockerfile.registry>${env.REGISTRY}</dockerfile.registry>
<springfox-version>2.9.2</springfox-version>
<servlet-api-version>2.5</servlet-api-version>
<jackson-version>2.12.5</jackson-version>
<spring-version>2.5.3</spring-version>
<javax-annotation-api-version>1.3.2</javax-annotation-api-version>
<jaxb-version>2.3.1</jaxb-version>
</properties>
<dependencies>
<!--rbkmoney-->
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>custom-metrics-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>custom-actuator-endpoints</artifactId>
</dependency>
<dependency>
<groupId>com.rbkmoney.woody</groupId>
<artifactId>woody-thrift</artifactId>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>shared-resources</artifactId>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>bouncer-proto</artifactId>
<version>1.27-cc50cc4</version>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>org-management-proto</artifactId>
<version>1.3-39d8513</version>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>payout-manager-proto</artifactId>
<version>1.17-8aa3eb8</version>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>swag-anapi-v2</artifactId>
<version>1.47-23587c8-server</version>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>magista-proto</artifactId>
<version>1.20-f95c236</version>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>vortigon-proto</artifactId>
<version>1.13-0c92608</version>
</dependency>
<dependency>
<groupId>com.rbkmoney.geck</groupId>
<artifactId>serializer</artifactId>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>damsel</artifactId>
</dependency>
<!--spring-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.5.2</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>15.0.2</version>
<exclusions>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>15.0.2</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--third party-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox-version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet-api-version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>${javax-annotation-api-version}</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb-version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.69</version>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
<targetPath>${project.build.directory}</targetPath>
<includes>
<include>Dockerfile</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>Dockerfile</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-remote-resources-plugin</artifactId>
<version>1.6.0</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-filtering</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<configuration>
<resourceBundles>
<resourceBundle>com.rbkmoney:shared-resources:${shared-resources.version}</resourceBundle>
</resourceBundles>
<attachToMain>false</attachToMain>
<attachToTest>false</attachToTest>
</configuration>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.rbkmoney.anapi.v2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class AnapiV2Application extends SpringApplication {
public static void main(String[] args) {
SpringApplication.run(AnapiV2Application.class, args);
}
}

View File

@ -0,0 +1,70 @@
package com.rbkmoney.anapi.v2.config;
import com.rbkmoney.bouncer.decisions.ArbiterSrv;
import com.rbkmoney.damsel.vortigon.VortigonServiceSrv;
import com.rbkmoney.magista.MerchantStatisticsServiceSrv;
import com.rbkmoney.orgmanagement.AuthContextProviderSrv;
import com.rbkmoney.woody.api.trace.context.metadata.user.UserIdentityEmailExtensionKit;
import com.rbkmoney.woody.api.trace.context.metadata.user.UserIdentityIdExtensionKit;
import com.rbkmoney.woody.api.trace.context.metadata.user.UserIdentityRealmExtensionKit;
import com.rbkmoney.woody.api.trace.context.metadata.user.UserIdentityUsernameExtensionKit;
import com.rbkmoney.woody.thrift.impl.http.THSpawnClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.util.List;
@Configuration
public class ApplicationConfig {
@Bean
public MerchantStatisticsServiceSrv.Iface magistaClient(
@Value("${service.magista.url}") Resource resource,
@Value("${service.magista.networkTimeout}") int networkTimeout
) throws IOException {
return new THSpawnClientBuilder()
.withNetworkTimeout(networkTimeout)
.withAddress(resource.getURI()).build(MerchantStatisticsServiceSrv.Iface.class);
}
@Bean
public VortigonServiceSrv.Iface vortigonClient(
@Value("${service.vortigon.url}") Resource resource,
@Value("${service.vortigon.networkTimeout}") int networkTimeout
) throws IOException {
return new THSpawnClientBuilder()
.withNetworkTimeout(networkTimeout)
.withAddress(resource.getURI()).build(VortigonServiceSrv.Iface.class);
}
@Bean
public AuthContextProviderSrv.Iface orgManagerClient(
@Value("${service.orgManagement.url}") Resource resource,
@Value("${service.orgManagement.networkTimeout}") int networkTimeout
) throws IOException {
return new THSpawnClientBuilder()
.withNetworkTimeout(networkTimeout)
.withMetaExtensions(
List.of(
UserIdentityIdExtensionKit.INSTANCE,
UserIdentityEmailExtensionKit.INSTANCE,
UserIdentityUsernameExtensionKit.INSTANCE,
UserIdentityRealmExtensionKit.INSTANCE
)
)
.withAddress(resource.getURI()).build(AuthContextProviderSrv.Iface.class);
}
@Bean
public ArbiterSrv.Iface bouncerClient(
@Value("${service.bouncer.url}") Resource resource,
@Value("${service.bouncer.networkTimeout}") int networkTimeout
) throws IOException {
return new THSpawnClientBuilder()
.withNetworkTimeout(networkTimeout)
.withAddress(resource.getURI()).build(ArbiterSrv.Iface.class);
}
}

View File

@ -0,0 +1,135 @@
package com.rbkmoney.anapi.v2.config;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
@EnableWebSecurity
@ComponentScan(
basePackageClasses = KeycloakSecurityComponents.class,
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "org.keycloak.adapters.springsecurity.management.HttpSessionManager"))
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
@ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Value("${keycloak.realm}")
private String keycloakRealmName;
@Value("${keycloak.resource}")
private String keycloakResourceName;
@Value("${keycloak.realm-public-key}")
private String keycloakRealmPublicKey;
@Value("${keycloak.realm-public-key.file-path:}")
private String keycloakRealmPublicKeyFile;
@Value("${keycloak.auth-server-url}")
private String keycloakAuthServerUrl;
@Value("${keycloak.ssl-required}")
private String keycloakSSLRequired;
@Value("${keycloak.not-before}")
private int keycloakTokenNotBefore;
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.GET, "/**/health").permitAll()
.anyRequest().authenticated();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return facade -> {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(adapterConfig());
deployment.setNotBefore(keycloakTokenNotBefore);
return deployment;
};
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.applyPermitDefaultValues();
configuration.addAllowedMethod(HttpMethod.PUT);
configuration.addAllowedMethod(HttpMethod.DELETE);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
private AdapterConfig adapterConfig() {
if (StringUtils.hasLength(keycloakRealmPublicKeyFile)) {
keycloakRealmPublicKey = readKeyFromFile(keycloakRealmPublicKeyFile);
}
AdapterConfig adapterConfig = new AdapterConfig();
adapterConfig.setRealm(keycloakRealmName);
adapterConfig.setRealmKey(keycloakRealmPublicKey);
adapterConfig.setResource(keycloakResourceName);
adapterConfig.setAuthServerUrl(keycloakAuthServerUrl);
adapterConfig.setUseResourceRoleMappings(true);
adapterConfig.setBearerOnly(true);
adapterConfig.setSslRequired(keycloakSSLRequired);
return adapterConfig;
}
private String readKeyFromFile(String filePath) {
try {
List<String> strings = Files.readAllLines(Paths.get(filePath));
strings.remove(strings.size() - 1);
strings.remove(0);
return strings.stream().map(String::trim).collect(Collectors.joining());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@ -0,0 +1,105 @@
package com.rbkmoney.anapi.v2.config;
import com.rbkmoney.woody.api.flow.WFlow;
import com.rbkmoney.woody.api.trace.context.metadata.user.UserIdentityEmailExtensionKit;
import com.rbkmoney.woody.api.trace.context.metadata.user.UserIdentityIdExtensionKit;
import com.rbkmoney.woody.api.trace.context.metadata.user.UserIdentityRealmExtensionKit;
import com.rbkmoney.woody.api.trace.context.metadata.user.UserIdentityUsernameExtensionKit;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.representations.AccessToken;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
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;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import static com.rbkmoney.anapi.v2.util.DeadlineUtil.*;
import static com.rbkmoney.woody.api.trace.ContextUtils.setCustomMetadataValue;
import static com.rbkmoney.woody.api.trace.ContextUtils.setDeadline;
@Configuration
@SuppressWarnings({"ParameterName", "LocalVariableName"})
public class WebConfig {
@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 {
woodyFlow.createServiceFork(
() -> {
try {
if (request.getUserPrincipal() != null) {
addWoodyContext(request.getUserPrincipal());
}
setWoodyDeadline(request);
filterChain.doFilter(request, response);
} catch (IOException | ServletException e) {
sneakyThrow(e);
}
}
)
.run();
}
private <E extends Throwable, T> T sneakyThrow(Throwable t) throws E {
throw (E) t;
}
};
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.setOrder(-50);
filterRegistrationBean.setName("woodyFilter");
filterRegistrationBean.addUrlPatterns("*");
return filterRegistrationBean;
}
private void addWoodyContext(Principal principal) {
KeycloakSecurityContext keycloakSecurityContext =
((KeycloakAuthenticationToken) principal).getAccount().getKeycloakSecurityContext();
AccessToken accessToken = keycloakSecurityContext.getToken();
setCustomMetadataValue(UserIdentityIdExtensionKit.KEY, accessToken.getSubject());
setCustomMetadataValue(UserIdentityUsernameExtensionKit.KEY, accessToken.getPreferredUsername());
setCustomMetadataValue(UserIdentityEmailExtensionKit.KEY, accessToken.getEmail());
setCustomMetadataValue(UserIdentityRealmExtensionKit.KEY, keycloakSecurityContext.getRealm());
}
private void setWoodyDeadline(HttpServletRequest request) {
String xRequestDeadline = request.getHeader("X-Request-Deadline");
String xRequestId = request.getHeader("X-Request-ID");
if (xRequestDeadline != null) {
setDeadline(getInstant(xRequestDeadline, xRequestId));
}
}
private Instant getInstant(String xRequestDeadline, String xRequestId) {
Instant instant;
if (containsRelativeValues(xRequestDeadline, xRequestId)) {
instant = Instant.now()
.plus(extractMilliseconds(xRequestDeadline, xRequestId), ChronoUnit.MILLIS)
.plus(extractSeconds(xRequestDeadline, xRequestId), ChronoUnit.MILLIS)
.plus(extractMinutes(xRequestDeadline, xRequestId), ChronoUnit.MILLIS);
} else {
instant = Instant.parse(xRequestDeadline);
}
return instant;
}
}

View File

@ -0,0 +1,28 @@
package com.rbkmoney.anapi.v2.config.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
@Getter
@Setter
@Component
@Validated
@ConfigurationProperties(prefix = "service.bouncer")
public class BouncerProperties {
@NotEmpty
private String contextFragmentId;
@NotEmpty
private String deploymentId;
@NotEmpty
private String authMethod;
@NotEmpty
private String realm;
@NotEmpty
private String ruleSetId;
}

View File

@ -0,0 +1,86 @@
package com.rbkmoney.anapi.v2.controller;
import com.rbkmoney.anapi.v2.exception.AuthorizationException;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.anapi.v2.exception.DeadlineException;
import com.rbkmoney.openapi.anapi_v2.model.DefaultLogicError;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class ErrorControllerAdvice {
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleConstraintViolationException(ConstraintViolationException e) {
log.warn("<- Res [400]: Not valid", e);
Set<ConstraintViolation<?>> constraintViolations =
e.getConstraintViolations();
String errorMessage =
constraintViolations.stream()
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
.collect(Collectors.joining(", "));
return new DefaultLogicError()
.code(DefaultLogicError.CodeEnum.INVALIDREQUEST)
.message(errorMessage);
}
@ExceptionHandler({BadRequestException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleBadRequestException(BadRequestException e) {
log.warn("<- Res [400]: Not valid", e);
return new DefaultLogicError()
.code(DefaultLogicError.CodeEnum.INVALIDREQUEST)
.message(e.getMessage());
}
@ExceptionHandler({MissingServletRequestParameterException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
log.warn("<- Res [400]: Missing ServletRequestParameter", e);
return new DefaultLogicError()
.code(DefaultLogicError.CodeEnum.INVALIDREQUEST)
.message(e.getMessage());
}
@ExceptionHandler({DeadlineException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleDeadlineException(DeadlineException e) {
log.warn("<- Res [400]: Not valid", e);
return new DefaultLogicError()
.code(DefaultLogicError.CodeEnum.INVALIDDEADLINE)
.message(e.getMessage());
}
@ExceptionHandler({AccessDeniedException.class})
@ResponseStatus(HttpStatus.FORBIDDEN)
public void handleAccessDeniedException(AccessDeniedException e) {
log.warn("<- Res [403]: Request denied access", e);
}
@ExceptionHandler({AuthorizationException.class})
@ResponseStatus(HttpStatus.FORBIDDEN)
public void handleAccessDeniedException(AuthorizationException e) {
log.warn("<- Res [403]: Request denied access", e);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public void handleException(Exception e) {
log.error("<- Res [500]: Unrecognized inner error", e);
}
}

View File

@ -0,0 +1,269 @@
package com.rbkmoney.anapi.v2.controller;
import com.rbkmoney.anapi.v2.converter.search.request.*;
import com.rbkmoney.anapi.v2.security.AccessService;
import com.rbkmoney.anapi.v2.service.SearchService;
import com.rbkmoney.magista.*;
import com.rbkmoney.openapi.anapi_v2.api.*;
import com.rbkmoney.openapi.anapi_v2.model.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.context.request.NativeWebRequest;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.merge;
import static com.rbkmoney.anapi.v2.util.DeadlineUtil.checkDeadline;
@Slf4j
@PreAuthorize("hasAuthority('invoices:read')")
@Controller
@RequiredArgsConstructor
@SuppressWarnings("ParameterName")
public class SearchController implements PaymentsApi, ChargebacksApi, InvoicesApi, PayoutsApi, RefundsApi {
private final SearchService searchService;
private final AccessService accessService;
private final ParamsToPaymentSearchQueryConverter paymentSearchConverter;
private final ParamsToChargebackSearchQueryConverter chargebackSearchConverter;
private final ParamsToInvoiceSearchQueryConverter invoiceSearchConverter;
private final ParamsToPayoutSearchQueryConverter payoutSearchConverter;
private final ParamsToRefundSearchQueryConverter refundSearchConverter;
@Override
public Optional<NativeWebRequest> getRequest() {
return PaymentsApi.super.getRequest();
}
@Override
public ResponseEntity<InlineResponse20010> searchPayments(String xRequestID,
@NotNull @Size(min = 1, max = 40) @Valid String partyID,
@NotNull @Valid OffsetDateTime fromTime,
@NotNull @Valid OffsetDateTime toTime,
@NotNull @Min(1L) @Max(1000L) @Valid Integer limit,
String xRequestDeadline,
@Size(min = 1, max = 40) @Valid String shopID,
@Valid List<String> shopIDs,
@Valid String paymentInstitutionRealm,
@Valid List<String> invoiceIDs,
@Valid String paymentStatus, @Valid String paymentFlow,
@Valid String paymentMethod,
@Valid String paymentTerminalProvider,
@Size(min = 1, max = 40) @Valid String invoiceID,
@Size(min = 1, max = 40) @Valid String paymentID,
@Size(min = 1, max = 40) @Valid String externalID,
@Size(max = 100) @Email @Valid String payerEmail,
@Size(max = 45) @Valid String payerIP,
@Size(max = 1000) @Valid String payerFingerprint,
@Size(min = 1, max = 40) @Valid String customerID,
@Pattern(regexp = "^\\d{6}$") @Valid String first6,
@Pattern(regexp = "^\\d{4}$") @Valid String last4,
@Pattern(regexp = "^[a-zA-Z0-9]{12}$") @Valid String rrn,
@Size(min = 1, max = 40) @Valid String approvalCode,
@Valid String bankCardTokenProvider,
@Valid String bankCardPaymentSystem,
@Min(1L) @Valid Long paymentAmountFrom,
@Min(1L) @Valid Long paymentAmountTo,
@Valid List<String> excludedShops,
@Valid String continuationToken) {
log.info("-> Req: xRequestID={}", xRequestID);
checkDeadline(xRequestDeadline, xRequestID);
shopIDs = accessService
.getAccessibleShops("searchPayments", partyID, merge(shopID, shopIDs), paymentInstitutionRealm);
PaymentSearchQuery query = paymentSearchConverter.convert(partyID,
fromTime,
toTime,
limit,
shopIDs,
invoiceIDs,
paymentStatus, paymentFlow,
paymentMethod,
paymentTerminalProvider,
invoiceID,
paymentID,
externalID,
payerEmail,
payerIP,
payerFingerprint,
customerID,
first6,
last4,
rrn,
approvalCode,
bankCardTokenProvider,
bankCardPaymentSystem,
paymentAmountFrom,
paymentAmountTo,
excludedShops,
continuationToken);
InlineResponse20010 response = searchService.findPayments(query);
log.info("<- Res [200]: xRequestID={}", xRequestID);
return ResponseEntity.ok(response);
}
@Override
public ResponseEntity<InlineResponse2008> searchChargebacks(String xRequestID,
@NotNull @Size(min = 1, max = 40) @Valid String partyID,
@NotNull @Valid OffsetDateTime fromTime,
@NotNull @Valid OffsetDateTime toTime,
@NotNull @Min(1L) @Max(1000L) @Valid Integer limit,
String xRequestDeadline,
@Size(min = 1, max = 40) @Valid String shopID,
@Valid List<String> shopIDs,
@Valid String paymentInstitutionRealm,
//Not used by magista
@Min(0L) @Valid @Deprecated Integer offset,
@Size(min = 1, max = 40) @Valid String invoiceID,
@Size(min = 1, max = 40) @Valid String paymentID,
@Size(min = 1, max = 40) @Valid String chargebackID,
@Valid List<String> chargebackStatuses,
@Valid List<String> chargebackStages,
@Valid List<String> chargebackCategories,
@Valid String continuationToken) {
log.info("-> Req: xRequestID={}", xRequestID);
checkDeadline(xRequestDeadline, xRequestID);
shopIDs = accessService
.getAccessibleShops("searchChargebacks", partyID, merge(shopID, shopIDs), paymentInstitutionRealm);
ChargebackSearchQuery query = chargebackSearchConverter.convert(partyID,
fromTime,
toTime,
limit,
shopIDs,
invoiceID,
paymentID,
chargebackID,
chargebackStatuses,
chargebackStages,
chargebackCategories,
continuationToken);
InlineResponse2008 response = searchService
.findChargebacks(query);
log.info("<- Res [200]: xRequestID={}", xRequestID);
return ResponseEntity.ok(response);
}
@Override
public ResponseEntity<InlineResponse2009> searchInvoices(String xRequestID,
@NotNull @Size(min = 1, max = 40) @Valid String partyID,
@NotNull @Valid OffsetDateTime fromTime,
@NotNull @Valid OffsetDateTime toTime,
@NotNull @Min(1L) @Max(1000L) @Valid Integer limit,
String xRequestDeadline,
@Size(min = 1, max = 40) @Valid String shopID,
@Valid List<String> shopIDs,
@Valid String paymentInstitutionRealm,
@Valid List<String> invoiceIDs,
@Valid String invoiceStatus,
@Size(min = 1, max = 40) @Valid String invoiceID,
@Size(min = 1, max = 40) @Valid String externalID,
@Min(1L) @Valid Long invoiceAmountFrom,
@Min(1L) @Valid Long invoiceAmountTo,
//Not used by magista
@Valid @Deprecated List<String> excludedShops,
@Valid String continuationToken) {
log.info("-> Req: xRequestID={}", xRequestID);
checkDeadline(xRequestDeadline, xRequestID);
shopIDs = accessService
.getAccessibleShops("searchInvoices", partyID, merge(shopID, shopIDs), paymentInstitutionRealm);
InvoiceSearchQuery query = invoiceSearchConverter.convert(partyID,
fromTime,
toTime,
limit,
shopIDs,
invoiceIDs,
invoiceStatus,
invoiceID,
externalID,
invoiceAmountFrom,
invoiceAmountTo,
continuationToken);
InlineResponse2009 response = searchService.findInvoices(query);
log.info("<- Res [200]: xRequestID={}", xRequestID);
return ResponseEntity.ok(response);
}
@Override
public ResponseEntity<InlineResponse20011> searchPayouts(String xRequestID,
@NotNull @Size(min = 1, max = 40) @Valid String partyID,
@NotNull @Valid OffsetDateTime fromTime,
@NotNull @Valid OffsetDateTime toTime,
@NotNull @Min(1L) @Max(1000L) @Valid Integer limit,
String xRequestDeadline,
@Size(min = 1, max = 40) @Valid String shopID,
@Valid List<String> shopIDs,
@Valid String paymentInstitutionRealm,
//Not used by magista
@Min(0L) @Valid @Deprecated Integer offset,
@Size(min = 1, max = 40) @Valid String payoutID,
@Valid String payoutToolType,
//Not used by magista
@Valid @Deprecated List<String> excludedShops,
@Valid String continuationToken) {
log.info("-> Req: xRequestID={}", xRequestID);
checkDeadline(xRequestDeadline, xRequestID);
shopIDs = accessService
.getAccessibleShops("searchPayouts", partyID, merge(shopID, shopIDs), paymentInstitutionRealm);
PayoutSearchQuery query = payoutSearchConverter.convert(partyID,
fromTime,
toTime,
limit,
shopIDs,
payoutID,
payoutToolType,
continuationToken);
InlineResponse20011 response = searchService.findPayouts(query);
log.info("<- Res [200]: xRequestID={}", xRequestID);
return ResponseEntity.ok(response);
}
@Override
public ResponseEntity<InlineResponse20012> searchRefunds(String xRequestID,
@NotNull @Size(min = 1, max = 40) @Valid String partyID,
@NotNull @Valid OffsetDateTime fromTime,
@NotNull @Valid OffsetDateTime toTime,
@NotNull @Min(1L) @Max(1000L) @Valid Integer limit,
String xRequestDeadline,
@Size(min = 1, max = 40) @Valid String shopID,
@Valid List<String> shopIDs,
@Valid String paymentInstitutionRealm,
//Not used by magista
@Min(0L) @Valid @Deprecated Integer offset,
@Valid List<String> invoiceIDs,
@Size(min = 1, max = 40) @Valid String invoiceID,
@Size(min = 1, max = 40) @Valid String paymentID,
@Size(min = 1, max = 40) @Valid String refundID,
@Size(min = 1, max = 40) @Valid String externalID,
@Valid String refundStatus,
//Not used by magista
@Valid @Deprecated List<String> excludedShops,
@Valid String continuationToken) {
log.info("-> Req: xRequestID={}", xRequestID);
checkDeadline(xRequestDeadline, xRequestID);
shopIDs = accessService
.getAccessibleShops("searchRefunds", partyID, merge(shopID, shopIDs), paymentInstitutionRealm);
RefundSearchQuery query = refundSearchConverter.convert(partyID,
fromTime,
toTime,
limit,
shopIDs,
invoiceIDs,
invoiceID,
paymentID,
refundID,
externalID,
refundStatus,
continuationToken);
InlineResponse20012 response = searchService.findRefunds(query);
log.info("<- Res [200]: xRequestID={}", xRequestID);
return ResponseEntity.ok(response);
}
}

View File

@ -0,0 +1,118 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.damsel.domain.*;
import com.rbkmoney.magista.ChargebackSearchQuery;
import com.rbkmoney.openapi.anapi_v2.model.ChargebackCategory;
import com.rbkmoney.openapi.anapi_v2.model.ChargebackStage;
import com.rbkmoney.openapi.anapi_v2.model.ChargebackStatus;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.stream.Collectors;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.fillCommonParams;
@Component
public class ParamsToChargebackSearchQueryConverter {
public ChargebackSearchQuery convert(String partyID,
OffsetDateTime fromTime,
OffsetDateTime toTime,
Integer limit,
List<String> shopIDs,
String invoiceID,
String paymentID,
String chargebackID,
List<String> chargebackStatuses,
List<String> chargebackStages,
List<String> chargebackCategories,
String continuationToken) {
return new ChargebackSearchQuery()
.setCommonSearchQueryParams(
fillCommonParams(fromTime, toTime, limit, partyID, shopIDs, continuationToken))
.setInvoiceIds(invoiceID != null ? List.of(invoiceID) : null)
.setPaymentId(paymentID)
.setChargebackId(chargebackID)
.setChargebackStatuses(chargebackStatuses != null
? chargebackStatuses.stream()
.map(this::mapStatus)
.collect(Collectors.toList())
: null
)
.setChargebackStages(chargebackStages != null
? chargebackStages.stream()
.map(this::mapStage)
.collect(Collectors.toList())
: null
)
.setChargebackCategories(chargebackCategories != null
? chargebackCategories.stream()
.map(this::mapCategory)
.collect(Collectors.toList())
: null);
}
protected InvoicePaymentChargebackStage mapStage(String chargebackStage) {
try {
var stage = ChargebackStage.fromValue(chargebackStage);
var damselStage = new InvoicePaymentChargebackStage();
switch (stage) {
case CHARGEBACK -> damselStage.setChargeback(new InvoicePaymentChargebackStageChargeback());
case PRE_ARBITRATION -> damselStage.setPreArbitration(
new InvoicePaymentChargebackStagePreArbitration());
case ARBITRATION -> damselStage.setArbitration(new InvoicePaymentChargebackStageArbitration());
default -> throw new BadRequestException(
String.format("Chargeback stage %s cannot be processed", chargebackStage));
}
return damselStage;
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
protected InvoicePaymentChargebackStatus mapStatus(String chargebackStatus) {
try {
var status = ChargebackStatus.fromValue(chargebackStatus);
var damselStatus = new InvoicePaymentChargebackStatus();
switch (status) {
case PENDING -> damselStatus.setPending(new InvoicePaymentChargebackPending());
case ACCEPTED -> damselStatus.setAccepted(new InvoicePaymentChargebackAccepted());
case REJECTED -> damselStatus.setRejected(new InvoicePaymentChargebackRejected());
case CANCELLED -> damselStatus.setCancelled(new InvoicePaymentChargebackCancelled());
default -> throw new BadRequestException(
String.format("Chargeback status %s cannot be processed", chargebackStatus));
}
return damselStatus;
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
protected InvoicePaymentChargebackCategory mapCategory(String chargebackCategory) {
try {
var category = ChargebackCategory.fromValue(chargebackCategory);
var damselCategory = new InvoicePaymentChargebackCategory();
switch (category) {
case FRAUD -> damselCategory.setFraud(new InvoicePaymentChargebackCategoryFraud());
case DISPUTE -> damselCategory.setDispute(new InvoicePaymentChargebackCategoryDispute());
case AUTHORISATION -> damselCategory
.setAuthorisation(new InvoicePaymentChargebackCategoryAuthorisation());
case PROCESSING_ERROR -> damselCategory
.setProcessingError(new InvoicePaymentChargebackCategoryProcessingError());
default -> throw new BadRequestException(
String.format("Chargeback category %s cannot be processed", chargebackCategory));
}
return damselCategory;
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
}

View File

@ -0,0 +1,59 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.magista.InvoiceSearchQuery;
import com.rbkmoney.magista.InvoiceStatus;
import com.rbkmoney.magista.PaymentParams;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.util.List;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.fillCommonParams;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.merge;
@Component
public class ParamsToInvoiceSearchQueryConverter {
public InvoiceSearchQuery convert(String partyID,
OffsetDateTime fromTime,
OffsetDateTime toTime,
Integer limit,
List<String> shopIDs,
List<String> invoiceIDs,
String invoiceStatus,
String invoiceID,
String externalID,
Long invoiceAmountFrom,
Long invoiceAmountTo,
String continuationToken) {
return new InvoiceSearchQuery()
.setCommonSearchQueryParams(
fillCommonParams(fromTime, toTime, limit, partyID, shopIDs, continuationToken))
.setPaymentParams(
mapPaymentParams(invoiceAmountFrom, invoiceAmountTo)
)
.setInvoiceStatus(invoiceStatus != null ? mapStatus(invoiceStatus) : null)
.setInvoiceIds(merge(invoiceID, invoiceIDs))
.setExternalId(externalID);
}
protected PaymentParams mapPaymentParams(Long invoiceAmountFrom, Long invoiceAmountTo) {
var params = new PaymentParams();
if (invoiceAmountFrom != null) {
params.setPaymentAmountFrom(invoiceAmountFrom);
}
if (invoiceAmountTo != null) {
params.setPaymentAmountTo(invoiceAmountTo);
}
return params;
}
protected InvoiceStatus mapStatus(String status) {
try {
return InvoiceStatus.valueOf(status);
} catch (IllegalArgumentException e) {
throw new BadRequestException(String.format("Invoice status %s cannot be processed", status));
}
}
}

View File

@ -0,0 +1,146 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.damsel.domain.LegacyBankCardPaymentSystem;
import com.rbkmoney.damsel.domain.LegacyBankCardTokenProvider;
import com.rbkmoney.damsel.domain.LegacyTerminalPaymentProvider;
import com.rbkmoney.magista.*;
import com.rbkmoney.openapi.anapi_v2.model.PaymentStatus;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.util.List;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.fillCommonParams;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.merge;
import static com.rbkmoney.magista.InvoicePaymentStatus.*;
import static com.rbkmoney.magista.PaymentToolType.bank_card;
import static com.rbkmoney.magista.PaymentToolType.payment_terminal;
@Component
public class ParamsToPaymentSearchQueryConverter {
public PaymentSearchQuery convert(String partyID,
OffsetDateTime fromTime,
OffsetDateTime toTime,
Integer limit,
List<String> shopIDs,
List<String> invoiceIDs,
String paymentStatus, String paymentFlow,
String paymentMethod,
String paymentTerminalProvider,
String invoiceID,
String paymentID,
String externalID,
String payerEmail,
String payerIP,
String payerFingerprint,
String customerID,
String first6,
String last4,
String rrn,
String approvalCode,
String bankCardTokenProvider,
String bankCardPaymentSystem,
Long paymentAmountFrom,
Long paymentAmountTo,
List<String> excludedShops,
String continuationToken) {
PaymentSearchQuery query = new PaymentSearchQuery()
.setCommonSearchQueryParams(
fillCommonParams(fromTime, toTime, limit, partyID, shopIDs, continuationToken))
.setExcludedShopIds(excludedShops)
.setExternalId(externalID)
.setInvoiceIds(merge(invoiceID, invoiceIDs));
PaymentParams paymentParams = new PaymentParams()
.setPaymentTool(paymentMethod != null ? mapPaymentTool(paymentMethod) : null)
.setPaymentFlow(paymentFlow != null ? mapInvoicePaymentFlow(paymentFlow) : null)
.setPaymentTerminalProvider(
paymentTerminalProvider != null ? mapTerminalProvider(paymentTerminalProvider) : null)
.setPaymentTokenProvider(
bankCardTokenProvider != null ? mapTokenProvider(bankCardTokenProvider) : null)
.setPaymentEmail(payerEmail)
.setPaymentApprovalCode(approvalCode)
.setPaymentCustomerId(customerID)
.setPaymentFingerprint(payerFingerprint)
.setPaymentFirst6(first6)
.setPaymentLast4(last4)
.setPaymentId(paymentID)
.setPaymentIp(payerIP)
.setPaymentRrn(rrn)
.setPaymentStatus(paymentStatus != null ? mapStatus(paymentStatus) : null)
.setPaymentSystem(bankCardPaymentSystem != null
? mapPaymentSystem(bankCardPaymentSystem) : null);
if (paymentAmountFrom != null) {
paymentParams.setPaymentAmountFrom(paymentAmountFrom);
}
if (paymentAmountTo != null) {
paymentParams.setPaymentAmountTo(paymentAmountTo);
}
query.setPaymentParams(paymentParams);
return query;
}
protected LegacyTerminalPaymentProvider mapTerminalProvider(String provider) {
try {
return LegacyTerminalPaymentProvider.valueOf(provider);
} catch (IllegalArgumentException e) {
throw new BadRequestException(String.format("Terminal provider %s cannot be processed", provider));
}
}
protected LegacyBankCardTokenProvider mapTokenProvider(String provider) {
try {
return LegacyBankCardTokenProvider.valueOf(provider);
} catch (IllegalArgumentException e) {
throw new BadRequestException(String.format("Token provider %s cannot be processed", provider));
}
}
protected LegacyBankCardPaymentSystem mapPaymentSystem(String system) {
try {
return LegacyBankCardPaymentSystem.valueOf(system);
} catch (IllegalArgumentException e) {
throw new BadRequestException(
String.format("Payment system %s cannot be processed", system));
}
}
protected PaymentToolType mapPaymentTool(String paymentMethod) {
return switch (paymentMethod) {
case "bankCard" -> bank_card;
case "paymentTerminal" -> payment_terminal;
default -> throw new BadRequestException(
String.format("Payment method %s cannot be processed", paymentMethod));
};
}
protected com.rbkmoney.magista.InvoicePaymentFlowType mapInvoicePaymentFlow(String paymentFlow) {
try {
return InvoicePaymentFlowType.valueOf(paymentFlow);
} catch (IllegalArgumentException e) {
throw new BadRequestException(
String.format("Payment flow %s cannot be processed", paymentFlow));
}
}
protected InvoicePaymentStatus mapStatus(String paymentStatus) {
try {
var status = PaymentStatus.StatusEnum.fromValue(paymentStatus);
return switch (status) {
case PENDING -> pending;
case PROCESSED -> processed;
case CAPTURED -> captured;
case CANCELLED -> cancelled;
case REFUNDED -> refunded;
case FAILED -> failed;
case CHARGEDBACK -> charged_back;
default -> throw new BadRequestException(
String.format("Payment status %s cannot be processed", paymentStatus));
};
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
}

View File

@ -0,0 +1,40 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.magista.PayoutSearchQuery;
import com.rbkmoney.magista.PayoutToolType;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.util.List;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.fillCommonParams;
@Component
public class ParamsToPayoutSearchQueryConverter {
public PayoutSearchQuery convert(String partyID,
OffsetDateTime fromTime,
OffsetDateTime toTime,
Integer limit,
List<String> shopIDs,
String payoutID,
String payoutToolType,
String continuationToken) {
return new PayoutSearchQuery()
.setCommonSearchQueryParams(
fillCommonParams(fromTime, toTime, limit, partyID, shopIDs, continuationToken))
.setPayoutId(payoutID)
.setPayoutType(payoutToolType != null ? mapPayoutToolType(payoutToolType) : null);
}
protected PayoutToolType mapPayoutToolType(String payoutToolType) {
return switch (payoutToolType) {
case "PayoutAccount" -> PayoutToolType.payout_account;
case "Wallet" -> PayoutToolType.wallet;
case "PaymentInstitutionAccount" -> PayoutToolType.payment_institution_account;
default -> throw new BadRequestException(
String.format("PayoutToolType %s cannot be processed", payoutToolType));
};
}
}

View File

@ -0,0 +1,48 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.magista.InvoicePaymentRefundStatus;
import com.rbkmoney.magista.RefundSearchQuery;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.util.List;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.fillCommonParams;
import static com.rbkmoney.anapi.v2.util.ConverterUtil.merge;
@Component
public class ParamsToRefundSearchQueryConverter {
public RefundSearchQuery convert(String partyID,
OffsetDateTime fromTime,
OffsetDateTime toTime,
Integer limit,
List<String> shopIDs,
List<String> invoiceIDs,
String invoiceID,
String paymentID,
String refundID,
String externalID,
String refundStatus,
String continuationToken) {
return new RefundSearchQuery()
.setCommonSearchQueryParams(
fillCommonParams(fromTime, toTime, limit, partyID, shopIDs, continuationToken))
.setRefundStatus(refundStatus != null ? mapStatus(refundStatus) : null)
.setInvoiceIds(merge(invoiceID, invoiceIDs))
.setExternalId(externalID)
.setPaymentId(paymentID)
.setRefundId(refundID);
}
protected InvoicePaymentRefundStatus mapStatus(String status) {
try {
return InvoicePaymentRefundStatus.valueOf(status);
} catch (IllegalArgumentException e) {
throw new BadRequestException(
String.format("Refund status %s cannot be processed", status));
}
}
}

View File

@ -0,0 +1,49 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.InvoicePaymentChargebackCategory;
import com.rbkmoney.geck.common.util.TypeUtil;
import com.rbkmoney.magista.StatChargeback;
import com.rbkmoney.openapi.anapi_v2.model.Chargeback;
import com.rbkmoney.openapi.anapi_v2.model.ChargebackCategory;
import com.rbkmoney.openapi.anapi_v2.model.ChargebackReason;
import com.rbkmoney.openapi.anapi_v2.model.Content;
import org.springframework.stereotype.Component;
import java.time.ZoneOffset;
@Component
public class StatChargebackToChargebackConverter {
public Chargeback convert(StatChargeback chargeback) {
return new Chargeback()
.bodyAmount(chargeback.getAmount())
.createdAt(TypeUtil.stringToInstant(chargeback.getCreatedAt()).atOffset(ZoneOffset.UTC))
.chargebackId(chargeback.getChargebackId())
.fee(chargeback.getFee())
.chargebackReason(chargeback.isSetChargebackReason()
? new ChargebackReason()
.category(mapCategory(chargeback.getChargebackReason().getCategory()))
.code(chargeback.getChargebackReason().getCode()) : null)
.content(chargeback.isSetContent()
? new Content().data(chargeback.getContent().getData())
.type(chargeback.getContent().getType()) : null)
.bodyCurrency(chargeback.getCurrencyCode().getSymbolicCode());
}
protected ChargebackCategory mapCategory(InvoicePaymentChargebackCategory chargebackCategory) {
try {
var field = InvoicePaymentChargebackCategory._Fields.findByName(
chargebackCategory.getSetField().getFieldName());
return switch (field) {
case FRAUD -> ChargebackCategory.FRAUD;
case DISPUTE -> ChargebackCategory.DISPUTE;
case AUTHORISATION -> ChargebackCategory.AUTHORISATION;
case PROCESSING_ERROR -> ChargebackCategory.PROCESSING_ERROR;
default -> throw new IllegalArgumentException();
};
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("Chargeback category %s cannot be processed", chargebackCategory));
}
}
}

View File

@ -0,0 +1,75 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.InvoiceStatus;
import com.rbkmoney.damsel.msgpack.Value;
import com.rbkmoney.geck.common.util.TypeUtil;
import com.rbkmoney.magista.StatInvoice;
import com.rbkmoney.openapi.anapi_v2.model.Invoice;
import com.rbkmoney.openapi.anapi_v2.model.InvoiceLine;
import com.rbkmoney.openapi.anapi_v2.model.InvoiceLineTaxMode;
import com.rbkmoney.openapi.anapi_v2.model.InvoiceLineTaxVAT;
import org.springframework.stereotype.Component;
import java.time.ZoneOffset;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class StatInvoiceToInvoiceConverter {
public Invoice convert(StatInvoice invoice) {
Invoice result = new Invoice()
.amount(invoice.getAmount())
.createdAt(TypeUtil.stringToInstant(invoice.getCreatedAt()).atOffset(ZoneOffset.UTC))
.currency(invoice.getCurrencySymbolicCode())
.externalID(invoice.getExternalId())
.cart(invoice.isSetCart()
? invoice.getCart().getLines().stream().map(invoiceLine -> new InvoiceLine()
.cost(invoiceLine.getQuantity() * invoiceLine.getPrice().getAmount())
.price(invoiceLine.getPrice().getAmount())
.product(invoiceLine.getProduct())
.taxMode(mapTaxMode(invoiceLine.getMetadata()))
).collect(Collectors.toList()) : null)
.description(invoice.getDescription())
.dueDate(TypeUtil.stringToInstant(invoice.getDue()).atOffset(ZoneOffset.UTC))
.id(invoice.getId())
.product(invoice.getProduct())
.shopID(invoice.getShopId());
mapStatusInfo(result, invoice.getStatus());
return result;
}
protected void mapStatusInfo(Invoice invoice, InvoiceStatus status) {
try {
var field = InvoiceStatus._Fields.findByName(status.getSetField().getFieldName());
switch (field) {
case FULFILLED -> {
invoice.setReason(status.getFulfilled().getDetails());
invoice.setStatus(Invoice.StatusEnum.FULFILLED);
}
case CANCELLED -> {
invoice.setReason(status.getCancelled().getDetails());
invoice.setStatus(Invoice.StatusEnum.CANCELLED);
}
case PAID -> invoice.setStatus(Invoice.StatusEnum.PAID);
case UNPAID -> invoice.setStatus(Invoice.StatusEnum.UNPAID);
default -> throw new IllegalArgumentException();
}
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("Invoice status %s cannot be processed", status));
}
}
protected InvoiceLineTaxMode mapTaxMode(Map<String, Value> metadata) {
Value taxMode = metadata.get("TaxMode");
if (taxMode != null) {
return new InvoiceLineTaxVAT()
.rate(InvoiceLineTaxVAT.RateEnum.fromValue(
taxMode.getStr()));
}
return null;
}
}

View File

@ -0,0 +1,83 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.InvoicePaymentStatus;
import com.rbkmoney.geck.common.util.TypeUtil;
import com.rbkmoney.magista.StatPayment;
import com.rbkmoney.openapi.anapi_v2.model.*;
import org.springframework.stereotype.Component;
import java.time.ZoneOffset;
import static com.rbkmoney.openapi.anapi_v2.model.PaymentSearchResult.StatusEnum.*;
@Component
public class StatPaymentToPaymentSearchResultConverter {
public PaymentSearchResult convert(StatPayment payment) {
return new PaymentSearchResult()
.amount(payment.getAmount())
.createdAt(TypeUtil.stringToInstant(payment.getCreatedAt()).atOffset(ZoneOffset.UTC))
.currency(payment.getCurrencySymbolicCode())
.externalID(payment.getExternalId())
.fee(payment.getFee())
.flow(new PaymentFlow()
.type(payment.getFlow().isSetHold() ? PaymentFlow.TypeEnum.PAYMENTFLOWHOLD :
PaymentFlow.TypeEnum.PAYMENTFLOWINSTANT))
.geoLocationInfo(payment.isSetLocationInfo() ? new GeoLocationInfo()
.cityGeoID(payment.getLocationInfo().getCityGeoId())
.countryGeoID(payment.getLocationInfo().getCountryGeoId())
: null)
.status(mapStatus(payment.getStatus()))
.error(payment.getStatus().isSetFailed()
? new PaymentError().code(payment.getStatus().getFailed().getFailure().getFailure().getCode())
: null)
.statusChangedAt(payment.isSetStatusChangedAt()
? TypeUtil.stringToInstant(payment.getStatusChangedAt()).atOffset(ZoneOffset.UTC) : null)
.id(payment.getId())
.invoiceID(payment.getInvoiceId())
.makeRecurrent(payment.isMakeRecurrent())
.payer(mapPayer(payment.getPayer()))
.shopID(payment.getShopId())
.shortID(payment.getShortId())
.transactionInfo(payment.isSetAdditionalTransactionInfo()
? new TransactionInfo()
.approvalCode(payment.getAdditionalTransactionInfo().getApprovalCode())
.rrn(payment.getAdditionalTransactionInfo().getRrn())
: null);
}
protected Payer mapPayer(com.rbkmoney.magista.Payer payer) {
try {
var field = com.rbkmoney.magista.Payer._Fields.findByName(payer.getSetField().getFieldName());
return switch (field) {
case CUSTOMER -> new Payer().payerType(Payer.PayerTypeEnum.CUSTOMERPAYER);
case PAYMENT_RESOURCE -> new Payer().payerType(Payer.PayerTypeEnum.PAYMENTRESOURCEPAYER);
case RECURRENT -> new Payer().payerType(Payer.PayerTypeEnum.RECURRENTPAYER);
default -> throw new IllegalArgumentException();
};
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("Payer %s cannot be processed", payer));
}
}
protected PaymentSearchResult.StatusEnum mapStatus(InvoicePaymentStatus status) {
try {
var field = InvoicePaymentStatus._Fields.findByName(status.getSetField().getFieldName());
return switch (field) {
case PENDING -> PENDING;
case PROCESSED -> PROCESSED;
case CAPTURED -> CAPTURED;
case CANCELLED -> CANCELLED;
case REFUNDED -> REFUNDED;
case FAILED -> FAILED;
case CHARGED_BACK -> CHARGEDBACK;
default -> throw new IllegalArgumentException();
};
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("Payment status %s cannot be processed", status));
}
}
}

View File

@ -0,0 +1,125 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.CountryCode;
import com.rbkmoney.damsel.domain.PayoutToolInfo;
import com.rbkmoney.geck.common.util.TypeUtil;
import com.rbkmoney.magista.PayoutStatus;
import com.rbkmoney.magista.StatPayout;
import com.rbkmoney.openapi.anapi_v2.model.*;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
import java.time.ZoneOffset;
@Component
public class StatPayoutToPayoutConverter {
public Payout convert(StatPayout payout) {
return new Payout()
.amount(payout.getAmount())
.createdAt(TypeUtil.stringToInstant(payout.getCreatedAt()).atOffset(ZoneOffset.UTC))
.currency(payout.getCurrencySymbolicCode())
.fee(payout.getFee())
.id(payout.getId())
.payoutToolDetails(mapPayoutToolDetails(payout.getPayoutToolInfo()))
.shopID(payout.getShopId())
.status(mapStatus(payout.getStatus()))
.cancellationDetails(
payout.getStatus().isSetCancelled()
? payout.getStatus().getCancelled().getDetails()
: null);
}
protected String mapStatus(PayoutStatus status) {
try {
var field = PayoutStatus._Fields.findByName(status.getSetField().getFieldName());
return switch (field) {
case UNPAID -> "Unpaid";
case PAID -> "Paid";
case CANCELLED -> "Cancelled";
case CONFIRMED -> "Confirmed";
default -> throw new IllegalArgumentException();
};
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("Payout status %s cannot be processed", status));
}
}
protected PayoutToolDetails mapPayoutToolDetails(PayoutToolInfo payoutToolInfo) {
try {
var field = PayoutToolInfo._Fields.findByName(payoutToolInfo.getSetField().getFieldName());
switch (field) {
case RUSSIAN_BANK_ACCOUNT -> {
var account = payoutToolInfo.getRussianBankAccount();
return new PayoutToolDetailsBankAccount()
.account(account.getAccount())
.bankBik(account.getBankBik())
.bankName(account.getBankName())
.bankPostAccount(account.getBankPostAccount())
.detailsType("PayoutToolDetailsBankAccount");
}
case INTERNATIONAL_BANK_ACCOUNT -> {
var account = payoutToolInfo.getInternationalBankAccount();
return new PayoutToolDetailsInternationalBankAccount()
.iban(account.getIban())
.number(account.getNumber())
.bankDetails(account.isSetBank()
? new InternationalBankDetails()
.name(account.getBank().getName())
.bic(account.getBank().getBic())
.countryCode(mapCountryCode(account.getBank().getCountry()))
.address(account.getBank().getAddress())
.abartn(account.getBank().getAbaRtn())
: null)
.correspondentBankAccount(
mapInternationalCorrespondentBankAccount(account.getCorrespondentAccount()))
.detailsType("PayoutToolDetailsInternationalBankAccount");
}
case WALLET_INFO -> {
return new PayoutToolDetailsWalletInfo()
.walletID(payoutToolInfo.getWalletInfo().getWalletId())
.detailsType("PayoutToolDetailsWalletInfo");
}
case PAYMENT_INSTITUTION_ACCOUNT -> {
return new PayoutToolDetailsPaymentInstitutionAccount()
.detailsType("PayoutToolDetailsPaymentInstitutionAccount");
}
default -> throw new IllegalArgumentException();
}
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("PayoutToolInfo %s cannot be processed", payoutToolInfo));
}
}
protected String mapCountryCode(@Nullable CountryCode countryCode) {
if (countryCode == null) {
return null;
}
return countryCode.name();
}
protected InternationalCorrespondentBankAccount mapInternationalCorrespondentBankAccount(
com.rbkmoney.damsel.domain.InternationalBankAccount account) {
if (account == null) {
return null;
}
var details = account.getBank();
return new InternationalCorrespondentBankAccount()
.bankDetails(details != null
? new InternationalBankDetails()
.name(details.getName())
.bic(details.getBic())
.countryCode(details.getCountry().name())
.address(details.getAddress())
.abartn(details.getAbaRtn())
: null)
.iban(account.getIban())
.number(account.getNumber())
.correspondentBankAccount(account.isSetCorrespondentAccount()
? mapInternationalCorrespondentBankAccount(account.getCorrespondentAccount())
: null);
}
}

View File

@ -0,0 +1,55 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.InvoicePaymentRefundStatus;
import com.rbkmoney.geck.common.util.TypeUtil;
import com.rbkmoney.magista.StatRefund;
import com.rbkmoney.openapi.anapi_v2.model.RefundSearchResult;
import com.rbkmoney.openapi.anapi_v2.model.RefundStatusError;
import org.springframework.stereotype.Component;
import java.time.ZoneOffset;
@Component
public class StatRefundToRefundSearchResultConverter {
public RefundSearchResult convert(StatRefund refund) {
return new RefundSearchResult()
.amount(refund.getAmount())
.createdAt(TypeUtil.stringToInstant(refund.getCreatedAt()).atOffset(ZoneOffset.UTC))
.currency(refund.getCurrencySymbolicCode())
.id(refund.getId())
.shopID(refund.getShopId())
.status(mapStatus(refund.getStatus()))
.externalID(refund.getExternalId())
.error(mapStatusError(refund.getStatus()))
.invoiceID(refund.getInvoiceId())
.paymentID(refund.getPaymentId())
.reason(refund.getReason());
}
protected RefundStatusError mapStatusError(InvoicePaymentRefundStatus status) {
if (status.isSetFailed() && status.getFailed().getFailure().isSetFailure()) {
var failure = status.getFailed().getFailure().getFailure();
return new RefundStatusError()
.code(failure.getCode())
.message(failure.getReason());
}
return null;
}
protected RefundSearchResult.StatusEnum mapStatus(InvoicePaymentRefundStatus status) {
try {
var field = InvoicePaymentRefundStatus._Fields.findByName(status.getSetField().getFieldName());
return switch (field) {
case PENDING -> RefundSearchResult.StatusEnum.PENDING;
case SUCCEEDED -> RefundSearchResult.StatusEnum.SUCCEEDED;
case FAILED -> RefundSearchResult.StatusEnum.FAILED;
default -> throw new IllegalArgumentException();
};
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("Refund status %s cannot be processed", status));
}
}
}

View File

@ -0,0 +1,12 @@
package com.rbkmoney.anapi.v2.exception;
public class AuthorizationException extends RuntimeException {
public AuthorizationException(String s) {
super(s);
}
public AuthorizationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,19 @@
package com.rbkmoney.anapi.v2.exception;
import com.rbkmoney.openapi.anapi_v2.model.DefaultLogicError;
import lombok.Getter;
public class BadRequestException extends IllegalArgumentException {
@Getter
private DefaultLogicError.CodeEnum errorCode = DefaultLogicError.CodeEnum.INVALIDREQUEST;
public BadRequestException(String s) {
super(s);
}
public BadRequestException(String s, DefaultLogicError.CodeEnum errorCode) {
super(s);
this.errorCode = errorCode;
}
}

View File

@ -0,0 +1,11 @@
package com.rbkmoney.anapi.v2.exception;
public class BouncerException extends AuthorizationException {
public BouncerException(String s) {
super(s);
}
public BouncerException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,23 @@
package com.rbkmoney.anapi.v2.exception;
public class DeadlineException extends RuntimeException {
public DeadlineException() {
}
public DeadlineException(String message) {
super(message);
}
public DeadlineException(String message, Throwable cause) {
super(message, cause);
}
public DeadlineException(Throwable cause) {
super(cause);
}
public DeadlineException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -0,0 +1,77 @@
package com.rbkmoney.anapi.v2.security;
import com.rbkmoney.anapi.v2.exception.AuthorizationException;
import com.rbkmoney.anapi.v2.service.BouncerService;
import com.rbkmoney.anapi.v2.service.KeycloakService;
import com.rbkmoney.anapi.v2.service.VortigonService;
import com.rbkmoney.bouncer.base.Entity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.AccessToken;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
@Service
public class AccessService {
private final VortigonService vortigonService;
private final BouncerService bouncerService;
private final KeycloakService keycloakService;
@Value("${service.bouncer.auth.enabled}")
private boolean authEnabled;
public List<String> getAccessibleShops(String operationId, String partyId, List<String> requestShopIds,
String realm) {
List<String> shopIds = vortigonService.getShopIds(partyId, Objects.requireNonNullElse(realm, "live"));
if (!requestShopIds.isEmpty()) {
shopIds = requestShopIds.stream()
.filter(shopIds::contains)
.collect(Collectors.toList());
}
log.info("Check the user's rights to perform the operation {}", operationId);
var ctx = buildAnapiBouncerContext(operationId, partyId, shopIds);
var resolution = bouncerService.getResolution(ctx);
if (resolution.isSetForbidden()) {
if (authEnabled) {
throw new AuthorizationException(String.format("No rights to perform %s", operationId));
} else {
log.warn("No rights to perform {}", operationId);
}
}
if (resolution.isSetRestricted()) {
if (authEnabled) {
return resolution.getRestricted().getRestrictions().getAnapi().getOp().getShops().stream()
.map(Entity::getId)
.collect(Collectors.toList());
} else {
log.warn("Rights to perform {} are restricted", operationId);
}
}
return new ArrayList<>(shopIds);
}
private AnapiBouncerContext buildAnapiBouncerContext(String operationId, String partyId, List<String> shopIds) {
AccessToken token = keycloakService.getAccessToken();
return AnapiBouncerContext.builder()
.operationId(operationId)
.partyId(partyId)
.shopIds(shopIds)
.tokenExpiration(token.getExp())
.tokenId(token.getId())
.userId(token.getSubject())
.build();
}
}

View File

@ -0,0 +1,18 @@
package com.rbkmoney.anapi.v2.security;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Builder
@Data
public class AnapiBouncerContext {
private final long tokenExpiration;
private final String tokenId;
private final String userId;
private final String operationId;
private final String partyId;
private final List<String> shopIds;
}

View File

@ -0,0 +1,81 @@
package com.rbkmoney.anapi.v2.security;
import com.rbkmoney.anapi.v2.config.properties.BouncerProperties;
import com.rbkmoney.anapi.v2.service.KeycloakService;
import com.rbkmoney.anapi.v2.service.OrgMgmtService;
import com.rbkmoney.bouncer.base.Entity;
import com.rbkmoney.bouncer.context.v1.*;
import com.rbkmoney.bouncer.ctx.ContextFragmentType;
import com.rbkmoney.bouncer.decisions.Context;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.thrift.TSerializer;
import org.keycloak.representations.AccessToken;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Set;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Component
public class BouncerContextFactory {
private final BouncerProperties bouncerProperties;
private final OrgMgmtService orgMgmtService;
private final KeycloakService keycloakService;
@SneakyThrows
public Context buildContext(AnapiBouncerContext bouncerContext) {
ContextFragment contextFragment = buildContextFragment(bouncerContext);
var serializer = new TSerializer();
var fragment = new com.rbkmoney.bouncer.ctx.ContextFragment()
.setType(ContextFragmentType.v1_thrift_binary)
.setContent(serializer.serialize(contextFragment));
com.rbkmoney.bouncer.ctx.ContextFragment
userFragment = orgMgmtService.getUserAuthContext(keycloakService.getAccessToken().getSubject());
Context context = new Context();
context.putToFragments(bouncerProperties.getContextFragmentId(), fragment);
context.putToFragments("user", userFragment);
return context;
}
private ContextFragment buildContextFragment(AnapiBouncerContext bouncerContext) {
Environment env = buildEnvironment();
AccessToken accessToken = keycloakService.getAccessToken();
ContextAnalyticsAPI contextAnalyticsApi = buildAnapiContext(bouncerContext);
return new ContextFragment()
.setAuth(buildAuth(bouncerContext, accessToken))
.setEnv(env)
.setAnapi(contextAnalyticsApi);
}
private Auth buildAuth(AnapiBouncerContext bouncerContext, AccessToken accessToken) {
Auth auth = new Auth();
Set<AuthScope> authScopeSet = bouncerContext.getShopIds().stream()
.map(shopId -> new AuthScope()
.setParty(new Entity().setId(bouncerContext.getPartyId()))
.setShop(new Entity().setId(shopId)))
.collect(Collectors.toSet());
return auth.setToken(new Token().setId(accessToken.getId()))
.setMethod(bouncerProperties.getAuthMethod())
.setExpiration(Instant.ofEpochSecond(accessToken.getExp()).toString())
.setScope(authScopeSet);
}
private Environment buildEnvironment() {
Deployment deployment = new Deployment()
.setId(bouncerProperties.getDeploymentId());
return new Environment()
.setDeployment(deployment)
.setNow(Instant.now().toString());
}
private ContextAnalyticsAPI buildAnapiContext(AnapiBouncerContext ctx) {
return new ContextAnalyticsAPI()
.setOp(new AnalyticsAPIOperation()
.setId(ctx.getOperationId()));
}
}

View File

@ -0,0 +1,39 @@
package com.rbkmoney.anapi.v2.service;
import com.rbkmoney.anapi.v2.config.properties.BouncerProperties;
import com.rbkmoney.anapi.v2.exception.BouncerException;
import com.rbkmoney.anapi.v2.security.AnapiBouncerContext;
import com.rbkmoney.anapi.v2.security.BouncerContextFactory;
import com.rbkmoney.bouncer.decisions.ArbiterSrv;
import com.rbkmoney.bouncer.decisions.Context;
import com.rbkmoney.bouncer.decisions.Judgement;
import com.rbkmoney.bouncer.decisions.Resolution;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class BouncerService {
private final BouncerProperties bouncerProperties;
private final BouncerContextFactory bouncerContextFactory;
private final ArbiterSrv.Iface bouncerClient;
public Resolution getResolution(AnapiBouncerContext bouncerContext) {
log.debug("Check access with bouncer context: {}", bouncerContext);
Context context = bouncerContextFactory.buildContext(bouncerContext);
log.debug("Built thrift context: {}", context);
try {
Judgement judge = bouncerClient.judge(bouncerProperties.getRuleSetId(), context);
log.debug("Have judge: {}", judge);
Resolution resolution = judge.getResolution();
log.debug("Resolution: {}", resolution);
return resolution;
} catch (TException e) {
throw new BouncerException("Error while call bouncer", e);
}
}
}

View File

@ -0,0 +1,17 @@
package com.rbkmoney.anapi.v2.service;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.representations.AccessToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
public class KeycloakService {
public AccessToken getAccessToken() {
var keycloakPrincipal = (KeycloakPrincipal) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
return keycloakPrincipal.getKeycloakSecurityContext().getToken();
}
}

View File

@ -0,0 +1,22 @@
package com.rbkmoney.anapi.v2.service;
import com.rbkmoney.bouncer.ctx.ContextFragment;
import com.rbkmoney.orgmanagement.AuthContextProviderSrv;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class OrgMgmtService {
private final AuthContextProviderSrv.Iface orgMgmtClient;
public ContextFragment getUserAuthContext(String userId) {
try {
return orgMgmtClient.getUserContext(userId);
} catch (Exception e) {
throw new RuntimeException(String.format("Can't get user auth context: userId = %s", userId), e);
}
}
}

View File

@ -0,0 +1,74 @@
package com.rbkmoney.anapi.v2.service;
import com.rbkmoney.anapi.v2.converter.search.response.*;
import com.rbkmoney.magista.*;
import com.rbkmoney.openapi.anapi_v2.model.*;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class SearchService {
private final MerchantStatisticsServiceSrv.Iface magistaClient;
private final StatPaymentToPaymentSearchResultConverter paymentResponseConverter;
private final StatChargebackToChargebackConverter chargebackResponseConverter;
private final StatInvoiceToInvoiceConverter invoiceResponseConverter;
private final StatPayoutToPayoutConverter payoutResponseConverter;
private final StatRefundToRefundSearchResultConverter refundResponseConverter;
@SneakyThrows
public InlineResponse20010 findPayments(PaymentSearchQuery query) {
StatPaymentResponse magistaResponse = magistaClient.searchPayments(query);
return new InlineResponse20010()
.result(magistaResponse.getPayments().stream()
.map(paymentResponseConverter::convert)
.collect(Collectors.toList()))
.continuationToken(magistaResponse.getContinuationToken());
}
@SneakyThrows
public InlineResponse2008 findChargebacks(ChargebackSearchQuery query) {
StatChargebackResponse magistaResponse = magistaClient.searchChargebacks(query);
return new InlineResponse2008()
.result(magistaResponse.getChargebacks().stream()
.map(chargebackResponseConverter::convert)
.collect(Collectors.toList()))
.continuationToken(magistaResponse.getContinuationToken());
}
@SneakyThrows
public InlineResponse2009 findInvoices(InvoiceSearchQuery query) {
StatInvoiceResponse magistaResponse = magistaClient.searchInvoices(query);
return new InlineResponse2009()
.result(magistaResponse.getInvoices().stream()
.map(invoiceResponseConverter::convert)
.collect(Collectors.toList()))
.continuationToken(magistaResponse.getContinuationToken());
}
@SneakyThrows
public InlineResponse20011 findPayouts(PayoutSearchQuery query) {
StatPayoutResponse magistaResponse = magistaClient.searchPayouts(query);
return new InlineResponse20011()
.result(magistaResponse.getPayouts().stream()
.map(payoutResponseConverter::convert)
.collect(Collectors.toList()))
.continuationToken(magistaResponse.getContinuationToken());
}
@SneakyThrows
public InlineResponse20012 findRefunds(RefundSearchQuery query) {
StatRefundResponse magistaResponse = magistaClient.searchRefunds(query);
return new InlineResponse20012()
.result(magistaResponse.getRefunds().stream()
.map(refundResponseConverter::convert)
.collect(Collectors.toList()))
.continuationToken(magistaResponse.getContinuationToken());
}
}

View File

@ -0,0 +1,31 @@
package com.rbkmoney.anapi.v2.service;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.damsel.vortigon.PaymentInstitutionRealm;
import com.rbkmoney.damsel.vortigon.VortigonServiceSrv;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class VortigonService {
private final VortigonServiceSrv.Iface vortigonClient;
@SneakyThrows
public List<String> getShopIds(String partyId, String realm) {
return vortigonClient.getShopsIds(partyId, mapRealm(realm));
}
private PaymentInstitutionRealm mapRealm(String realm) {
try {
return PaymentInstitutionRealm.valueOf(realm);
} catch (IllegalArgumentException e) {
throw new BadRequestException(
String.format("Realm %s cannot be processed", realm));
}
}
}

View File

@ -0,0 +1,39 @@
package com.rbkmoney.anapi.v2.util;
import com.rbkmoney.geck.common.util.TypeUtil;
import com.rbkmoney.magista.CommonSearchQueryParams;
import lombok.experimental.UtilityClass;
import javax.annotation.Nullable;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
@UtilityClass
public class ConverterUtil {
public static List<String> merge(@Nullable String id, @Nullable List<String> ids) {
List<String> identifiers = new ArrayList<>();
if (id != null) {
identifiers.add(id);
}
if (ids != null && !ids.isEmpty()) {
identifiers.addAll(ids);
}
return identifiers;
}
public static CommonSearchQueryParams fillCommonParams(OffsetDateTime fromTime, OffsetDateTime toTime,
Integer limit,
String partyId, List<String> shopIDs,
String continuationToken) {
return new CommonSearchQueryParams()
.setContinuationToken(continuationToken)
.setFromTime(TypeUtil.temporalToString(fromTime.toLocalDateTime()))
.setToTime(TypeUtil.temporalToString(toTime.toLocalDateTime()))
.setLimit(limit)
.setPartyId(partyId)
.setShopIds(shopIDs);
}
}

View File

@ -0,0 +1,111 @@
package com.rbkmoney.anapi.v2.util;
import com.rbkmoney.anapi.v2.exception.DeadlineException;
import lombok.experimental.UtilityClass;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@UtilityClass
@SuppressWarnings("ParameterName")
public class DeadlineUtil {
public static void checkDeadline(String xRequestDeadline, String xRequestId) {
if (xRequestDeadline == null) {
return;
}
if (containsRelativeValues(xRequestDeadline, xRequestId)) {
return;
}
try {
Instant instant = Instant.parse(xRequestDeadline);
if (Instant.now().isAfter(instant)) {
throw new DeadlineException(String.format("Deadline has expired, xRequestId=%s ", xRequestId));
}
} catch (Exception ex) {
throw new DeadlineException(
String.format("Deadline has invalid 'Instant' format, xRequestId=%s ", xRequestId));
}
}
public static boolean containsRelativeValues(String xRequestDeadline, String xRequestId) {
return (extractMinutes(xRequestDeadline, xRequestId) + extractSeconds(xRequestDeadline, xRequestId) +
extractMilliseconds(xRequestDeadline, xRequestId)) > 0;
}
public static Long extractMinutes(String xRequestDeadline, String xRequestId) {
String format = "minutes";
checkNegativeValues(xRequestDeadline, xRequestId, "([-][0-9]+([.][0-9]+)?(?!ms)[m])", format);
Double minutes = extractValue(xRequestDeadline, "([0-9]+([.][0-9]+)?(?!ms)[m])", xRequestId, format);
return Optional.ofNullable(minutes).map(min -> min * 60000.0).map(Double::longValue).orElse(0L);
}
public static Long extractSeconds(String xRequestDeadline, String xRequestId) {
String format = "seconds";
checkNegativeValues(xRequestDeadline, xRequestId, "([-][0-9]+([.][0-9]+)?[s])", format);
Double seconds = extractValue(xRequestDeadline, "([0-9]+([.][0-9]+)?[s])", xRequestId, format);
return Optional.ofNullable(seconds).map(s -> s * 1000.0).map(Double::longValue).orElse(0L);
}
public static Long extractMilliseconds(String xRequestDeadline, String xRequestId) {
String format = "milliseconds";
checkNegativeValues(xRequestDeadline, xRequestId, "([-][0-9]+([.][0-9]+)?[m][s])", format);
Double milliseconds = extractValue(xRequestDeadline, "([0-9]+([.][0-9]+)?[m][s])", xRequestId, format);
if (milliseconds != null && Math.ceil(milliseconds % 1) > 0) {
throw new DeadlineException(
String.format("Deadline 'milliseconds' parameter can have only integer value, xRequestId=%s ",
xRequestId));
}
return Optional.ofNullable(milliseconds).map(Double::longValue).orElse(0L);
}
private static void checkNegativeValues(String xRequestDeadline, String xRequestId, String regex, String format) {
if (!match(regex, xRequestDeadline).isEmpty()) {
throw new DeadlineException(
String.format("Deadline '%s' parameter has negative value, xRequestId=%s ", format, xRequestId));
}
}
private static Double extractValue(String xRequestDeadline, String formatRegex, String xRequestId, String format) {
String numberRegex = "([0-9]+([.][0-9]+)?)";
List<String> doubles = new ArrayList<>();
for (String string : match(formatRegex, xRequestDeadline)) {
doubles.addAll(match(numberRegex, string));
}
if (doubles.size() > 1) {
throw new DeadlineException(
String.format("Deadline '%s' parameter has a few relative value, xRequestId=%s ", format,
xRequestId));
}
if (doubles.isEmpty()) {
return null;
}
return Double.valueOf(doubles.get(0));
}
private static List<String> match(String regex, String data) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(data);
List<String> strings = new ArrayList<>();
while (matcher.find()) {
strings.add(matcher.group());
}
return strings;
}
}

View File

@ -0,0 +1,67 @@
server:
port: '@server.port@'
management:
security:
flag: false
server:
port: '@management.port@'
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
service:
magista:
url: http://localhost:8022/change_it
networkTimeout: 5000
vortigon:
url: http://localhost:8022/change_it
networkTimeout: 5000
orgManagement:
url: http://localhost:8022/change_it
networkTimeout: 5000
bouncer:
url: http://localhost:8022/change_it
networkTimeout: 10000
context-fragment-id: anapi
deployment-id: production
auth-method: SessionToken
realm: external
rule-set-id: change_it
auth:
enabled: false
spring:
application:
name: '@project.name@'
output:
ansi:
enabled: always
info:
version: '@project.version@'
stage: dev
keycloak:
realm: internal
auth-server-url: http://keycloak:8080/auth/
resource: common-api
not-before: 0
ssl-required: none
realm-public-key:
auth.enabled: true

View File

@ -0,0 +1,153 @@
package com.rbkmoney.anapi.v2;
import com.rbkmoney.anapi.v2.config.AbstractKeycloakOpenIdAsWiremockConfig;
import com.rbkmoney.anapi.v2.testutil.MagistaUtil;
import com.rbkmoney.anapi.v2.testutil.OpenApiUtil;
import com.rbkmoney.bouncer.decisions.ArbiterSrv;
import com.rbkmoney.damsel.vortigon.VortigonServiceSrv;
import com.rbkmoney.magista.MerchantStatisticsServiceSrv;
import com.rbkmoney.openapi.anapi_v2.model.DefaultLogicError;
import com.rbkmoney.orgmanagement.AuthContextProviderSrv;
import lombok.SneakyThrows;
import org.apache.thrift.TException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.MultiValueMap;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createContextFragment;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createJudgementAllowed;
import static java.util.UUID.randomUUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class SearchChargebacksTest extends AbstractKeycloakOpenIdAsWiremockConfig {
@MockBean
public MerchantStatisticsServiceSrv.Iface magistaClient;
@MockBean
public VortigonServiceSrv.Iface vortigonClient;
@MockBean
public AuthContextProviderSrv.Iface orgMgmtClient;
@MockBean
public ArbiterSrv.Iface bouncerClient;
@Autowired
private MockMvc mvc;
private AutoCloseable mocks;
private Object[] preparedMocks;
@BeforeEach
public void init() {
mocks = MockitoAnnotations.openMocks(this);
preparedMocks = new Object[] {magistaClient, vortigonClient, orgMgmtClient, bouncerClient};
}
@AfterEach
public void clean() throws Exception {
verifyNoMoreInteractions(preparedMocks);
mocks.close();
}
@Test
@SneakyThrows
void searchChargebacksRequiredParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchChargebacks(any())).thenReturn(MagistaUtil.createSearchChargebackRequiredResponse());
mvc.perform(get("/chargebacks")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchChargebacks(any());
}
@Test
@SneakyThrows
void searchChargebacksAllParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchChargebacks(any())).thenReturn(MagistaUtil.createSearchChargebackAllResponse());
mvc.perform(get("/chargebacks")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchChargebackAllParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchChargebacks(any());
}
@Test
@SneakyThrows
void searchChargebacksRequestInvalid() {
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
params.remove("partyID");
mvc.perform(get("/chargebacks")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDREQUEST.getValue()))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
@SneakyThrows
void searchChargebacksRequestMagistaUnavailable() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchPayments(any())).thenThrow(TException.class);
mvc.perform(get("/chargebacks")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is5xxServerError());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchChargebacks(any());
}
}

View File

@ -0,0 +1,152 @@
package com.rbkmoney.anapi.v2;
import com.rbkmoney.anapi.v2.config.AbstractKeycloakOpenIdAsWiremockConfig;
import com.rbkmoney.anapi.v2.testutil.MagistaUtil;
import com.rbkmoney.anapi.v2.testutil.OpenApiUtil;
import com.rbkmoney.bouncer.decisions.ArbiterSrv;
import com.rbkmoney.damsel.vortigon.VortigonServiceSrv;
import com.rbkmoney.magista.MerchantStatisticsServiceSrv;
import com.rbkmoney.openapi.anapi_v2.model.DefaultLogicError;
import com.rbkmoney.orgmanagement.AuthContextProviderSrv;
import lombok.SneakyThrows;
import org.apache.thrift.TException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.MultiValueMap;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createContextFragment;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createJudgementAllowed;
import static java.util.UUID.randomUUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class SearchInvoicesTest extends AbstractKeycloakOpenIdAsWiremockConfig {
@MockBean
public MerchantStatisticsServiceSrv.Iface magistaClient;
@MockBean
public VortigonServiceSrv.Iface vortigonClient;
@MockBean
public AuthContextProviderSrv.Iface orgMgmtClient;
@MockBean
public ArbiterSrv.Iface bouncerClient;
@Autowired
private MockMvc mvc;
private AutoCloseable mocks;
private Object[] preparedMocks;
@BeforeEach
public void init() {
mocks = MockitoAnnotations.openMocks(this);
preparedMocks = new Object[] {magistaClient, vortigonClient, orgMgmtClient, bouncerClient};
}
@AfterEach
public void clean() throws Exception {
verifyNoMoreInteractions(preparedMocks);
mocks.close();
}
@Test
@SneakyThrows
void searchInvoicesRequiredParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchInvoices(any())).thenReturn(MagistaUtil.createSearchInvoiceRequiredResponse());
mvc.perform(get("/invoices")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchInvoices(any());
}
@Test
@SneakyThrows
void searchInvoicesAllParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchInvoices(any())).thenReturn(MagistaUtil.createSearchInvoiceAllResponse());
mvc.perform(get("/invoices")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchInvoiceAllParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchInvoices(any());
}
@Test
@SneakyThrows
void searchInvoicesRequestInvalid() {
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
params.remove("partyID");
mvc.perform(get("/invoices")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDREQUEST.getValue()))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
@SneakyThrows
void searchInvoicesRequestMagistaUnavailable() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchInvoices(any())).thenThrow(TException.class);
mvc.perform(get("/invoices")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is5xxServerError());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchInvoices(any());
}
}

View File

@ -0,0 +1,153 @@
package com.rbkmoney.anapi.v2;
import com.rbkmoney.anapi.v2.config.AbstractKeycloakOpenIdAsWiremockConfig;
import com.rbkmoney.anapi.v2.testutil.MagistaUtil;
import com.rbkmoney.anapi.v2.testutil.OpenApiUtil;
import com.rbkmoney.bouncer.decisions.ArbiterSrv;
import com.rbkmoney.damsel.vortigon.VortigonServiceSrv;
import com.rbkmoney.magista.MerchantStatisticsServiceSrv;
import com.rbkmoney.openapi.anapi_v2.model.DefaultLogicError;
import com.rbkmoney.orgmanagement.AuthContextProviderSrv;
import lombok.SneakyThrows;
import org.apache.thrift.TException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.MultiValueMap;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createContextFragment;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createJudgementAllowed;
import static java.util.UUID.randomUUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class SearchPaymentsTest extends AbstractKeycloakOpenIdAsWiremockConfig {
@MockBean
public MerchantStatisticsServiceSrv.Iface magistaClient;
@MockBean
public VortigonServiceSrv.Iface vortigonClient;
@MockBean
public AuthContextProviderSrv.Iface orgMgmtClient;
@MockBean
public ArbiterSrv.Iface bouncerClient;
@Autowired
private MockMvc mvc;
private AutoCloseable mocks;
private Object[] preparedMocks;
@BeforeEach
public void init() {
mocks = MockitoAnnotations.openMocks(this);
preparedMocks = new Object[] {magistaClient, vortigonClient, orgMgmtClient, bouncerClient};
}
@AfterEach
public void clean() throws Exception {
verifyNoMoreInteractions(preparedMocks);
mocks.close();
}
@Test
@SneakyThrows
void searchPaymentsRequiredParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchPayments(any())).thenReturn(MagistaUtil.createSearchPaymentRequiredResponse());
mvc.perform(get("/payments")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchPayments(any());
}
@Test
@SneakyThrows
void searchPaymentsAllParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchPayments(any())).thenReturn(MagistaUtil.createSearchPaymentAllResponse());
mvc.perform(get("/payments")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchPaymentAllParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchPayments(any());
}
@Test
@SneakyThrows
void searchPaymentsRequestInvalid() {
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
params.remove("partyID");
mvc.perform(get("/payments")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDREQUEST.getValue()))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
@SneakyThrows
void searchPaymentsRequestMagistaUnavailable() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchPayments(any())).thenThrow(TException.class);
mvc.perform(get("/payments")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is5xxServerError());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchPayments(any());
}
}

View File

@ -0,0 +1,152 @@
package com.rbkmoney.anapi.v2;
import com.rbkmoney.anapi.v2.config.AbstractKeycloakOpenIdAsWiremockConfig;
import com.rbkmoney.anapi.v2.testutil.MagistaUtil;
import com.rbkmoney.anapi.v2.testutil.OpenApiUtil;
import com.rbkmoney.bouncer.decisions.ArbiterSrv;
import com.rbkmoney.damsel.vortigon.VortigonServiceSrv;
import com.rbkmoney.magista.MerchantStatisticsServiceSrv;
import com.rbkmoney.openapi.anapi_v2.model.DefaultLogicError;
import com.rbkmoney.orgmanagement.AuthContextProviderSrv;
import lombok.SneakyThrows;
import org.apache.thrift.TException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.MultiValueMap;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createContextFragment;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createJudgementAllowed;
import static java.util.UUID.randomUUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class SearchPayoutsTest extends AbstractKeycloakOpenIdAsWiremockConfig {
@MockBean
public MerchantStatisticsServiceSrv.Iface magistaClient;
@MockBean
public VortigonServiceSrv.Iface vortigonClient;
@MockBean
public AuthContextProviderSrv.Iface orgMgmtClient;
@MockBean
public ArbiterSrv.Iface bouncerClient;
@Autowired
private MockMvc mvc;
private AutoCloseable mocks;
private Object[] preparedMocks;
@BeforeEach
public void init() {
mocks = MockitoAnnotations.openMocks(this);
preparedMocks = new Object[] {magistaClient, vortigonClient, orgMgmtClient, bouncerClient};
}
@AfterEach
public void clean() throws Exception {
verifyNoMoreInteractions(preparedMocks);
mocks.close();
}
@Test
@SneakyThrows
void searchPayoutsRequiredParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchPayouts(any())).thenReturn(MagistaUtil.createSearchPayoutRequiredResponse());
mvc.perform(get("/payouts")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchPayouts(any());
}
@Test
@SneakyThrows
void searchPayoutsAllParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchPayouts(any())).thenReturn(MagistaUtil.createSearchPayoutAllResponse());
mvc.perform(get("/payouts")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchPayoutAllParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchPayouts(any());
}
@Test
@SneakyThrows
void searchPayoutsRequestInvalid() {
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
params.remove("partyID");
mvc.perform(get("/payouts")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDREQUEST.getValue()))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
@SneakyThrows
void searchPayoutsRequestMagistaUnavailable() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchPayouts(any())).thenThrow(TException.class);
mvc.perform(get("/payouts")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is5xxServerError());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchPayouts(any());
}
}

View File

@ -0,0 +1,152 @@
package com.rbkmoney.anapi.v2;
import com.rbkmoney.anapi.v2.config.AbstractKeycloakOpenIdAsWiremockConfig;
import com.rbkmoney.anapi.v2.testutil.MagistaUtil;
import com.rbkmoney.anapi.v2.testutil.OpenApiUtil;
import com.rbkmoney.bouncer.decisions.ArbiterSrv;
import com.rbkmoney.damsel.vortigon.VortigonServiceSrv;
import com.rbkmoney.magista.MerchantStatisticsServiceSrv;
import com.rbkmoney.openapi.anapi_v2.model.DefaultLogicError;
import com.rbkmoney.orgmanagement.AuthContextProviderSrv;
import lombok.SneakyThrows;
import org.apache.thrift.TException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.MultiValueMap;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createContextFragment;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createJudgementAllowed;
import static java.util.UUID.randomUUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class SearchRefundsTest extends AbstractKeycloakOpenIdAsWiremockConfig {
@MockBean
public MerchantStatisticsServiceSrv.Iface magistaClient;
@MockBean
public VortigonServiceSrv.Iface vortigonClient;
@MockBean
public AuthContextProviderSrv.Iface orgMgmtClient;
@MockBean
public ArbiterSrv.Iface bouncerClient;
@Autowired
private MockMvc mvc;
private AutoCloseable mocks;
private Object[] preparedMocks;
@BeforeEach
public void init() {
mocks = MockitoAnnotations.openMocks(this);
preparedMocks = new Object[] {magistaClient, vortigonClient, orgMgmtClient, bouncerClient};
}
@AfterEach
public void clean() throws Exception {
verifyNoMoreInteractions(preparedMocks);
mocks.close();
}
@Test
@SneakyThrows
void searchRefundsRequiredParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchRefunds(any())).thenReturn(MagistaUtil.createSearchRefundRequiredResponse());
mvc.perform(get("/refunds")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchRefunds(any());
}
@Test
@SneakyThrows
void searchRefundsAllParamsRequestSuccess() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchRefunds(any())).thenReturn(MagistaUtil.createSearchRefundAllResponse());
mvc.perform(get("/refunds")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRefundAllParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$").exists());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchRefunds(any());
}
@Test
@SneakyThrows
void searchRefundsRequestInvalid() {
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
params.remove("partyID");
mvc.perform(get("/refunds")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDREQUEST.getValue()))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
@SneakyThrows
void searchRefundsRequestMagistaUnavailable() {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
when(magistaClient.searchRefunds(any())).thenThrow(TException.class);
mvc.perform(get("/refunds")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().is5xxServerError());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(magistaClient, times(1)).searchRefunds(any());
}
}

View File

@ -0,0 +1,45 @@
package com.rbkmoney.anapi.v2.auth;
import com.rbkmoney.anapi.v2.auth.utils.JwtTokenBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Properties;
@Configuration
public class JwtTokenTestConfiguration {
@Bean
public static PropertySourcesPlaceholderConfigurer properties(KeyPair keyPair)
throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
KeyFactory fact = KeyFactory.getInstance("RSA");
X509EncodedKeySpec spec = fact.getKeySpec(keyPair.getPublic(), X509EncodedKeySpec.class);
String publicKey = Base64.getEncoder().encodeToString(spec.getEncoded());
PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
Properties properties = new Properties();
properties.load(new ClassPathResource("application.yml").getInputStream());
properties.setProperty("keycloak.realm-public-key", publicKey);
pspc.setProperties(properties);
pspc.setLocalOverride(true);
return pspc;
}
@Bean
public JwtTokenBuilder jwtTokenBuilder(KeyPair keyPair) {
return new JwtTokenBuilder(keyPair.getPrivate());
}
@Bean
public KeyPair keyPair() throws GeneralSecurityException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
return keyGen.generateKeyPair();
}
}

View File

@ -0,0 +1,18 @@
package com.rbkmoney.anapi.v2.auth;
import com.rbkmoney.anapi.v2.auth.utils.JwtTokenBuilder;
import com.rbkmoney.anapi.v2.auth.utils.KeycloakOpenIdStub;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class KeycloakOpenIdTestConfiguration {
@Bean
public KeycloakOpenIdStub keycloakOpenIdStub(@Value("${keycloak.auth-server-url}") String keycloakAuthServerUrl,
@Value("${keycloak.realm}") String keycloakRealm,
JwtTokenBuilder jwtTokenBuilder) {
return new KeycloakOpenIdStub(keycloakAuthServerUrl, keycloakRealm, jwtTokenBuilder);
}
}

View File

@ -0,0 +1,73 @@
package com.rbkmoney.anapi.v2.auth.utils;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.PrivateKey;
import java.time.Instant;
import java.util.UUID;
public class JwtTokenBuilder {
public static final String DEFAULT_USERNAME = "Darth Vader";
public static final String DEFAULT_EMAIL = "darkside-the-best@mail.com";
private final String userId;
private final String username;
private final String email;
private final PrivateKey privateKey;
public JwtTokenBuilder(PrivateKey privateKey) {
this(UUID.randomUUID().toString(), DEFAULT_USERNAME, DEFAULT_EMAIL, privateKey);
}
public JwtTokenBuilder(String userId, String username, String email, PrivateKey privateKey) {
this.userId = userId;
this.username = username;
this.email = email;
this.privateKey = privateKey;
}
public String generateJwtWithRoles(String issuer, String... roles) {
long iat = Instant.now().getEpochSecond();
long exp = iat + 60 * 10;
return generateJwtWithRoles(iat, exp, issuer, roles);
}
public String generateJwtWithRoles(long iat, long exp, String issuer, String... roles) {
String payload;
try {
payload = new JSONObject()
.put("jti", UUID.randomUUID().toString())
.put("exp", exp)
.put("nbf", "0")
.put("iat", iat)
.put("iss", issuer)
.put("aud", "private-api")
.put("sub", userId)
.put("typ", "Bearer")
.put("azp", "private-api")
.put("resource_access", new JSONObject()
.put("common-api", new JSONObject()
.put("roles", new JSONArray(roles))))
.put("preferred_username", username)
.put("email", email).toString();
} catch (JSONException e) {
throw new RuntimeException(e);
}
String jwt = Jwts.builder()
.setPayload(payload)
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
return jwt;
}
}

View File

@ -0,0 +1,55 @@
package com.rbkmoney.anapi.v2.auth.utils;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
public class KeycloakOpenIdStub {
private final String keycloakRealm;
private final String issuer;
private final String openidConfig;
private final JwtTokenBuilder jwtTokenBuilder;
public KeycloakOpenIdStub(String keycloakAuthServerUrl, String keycloakRealm, JwtTokenBuilder jwtTokenBuilder) {
this.keycloakRealm = keycloakRealm;
this.jwtTokenBuilder = jwtTokenBuilder;
this.issuer = keycloakAuthServerUrl + "/realms/" + keycloakRealm;
this.openidConfig = "{\n" +
" \"issuer\": \"" + issuer + "\",\n" +
" \"authorization_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/auth\",\n" +
" \"token_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/token\",\n" +
" \"token_introspection_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/token/introspect\",\n" +
" \"userinfo_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/userinfo\",\n" +
" \"end_session_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/logout\",\n" +
" \"jwks_uri\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/certs\",\n" +
" \"check_session_iframe\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/login-status-iframe.html\",\n" +
" \"registration_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/clients-registrations/openid-connect\",\n" +
" \"introspection_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/token/introspect\"\n" +
"}";
}
public void givenStub() {
stubFor(get(urlEqualTo(String.format("/auth/realms/%s/.well-known/openid-configuration", keycloakRealm)))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody(openidConfig)
)
);
}
public String generateJwt(String... roles) {
return jwtTokenBuilder.generateJwtWithRoles(issuer, roles);
}
public String generateJwt(long iat, long exp, String... roles) {
return jwtTokenBuilder.generateJwtWithRoles(iat, exp, issuer, roles);
}
}

View File

@ -0,0 +1,35 @@
package com.rbkmoney.anapi.v2.config;
import com.rbkmoney.anapi.v2.AnapiV2Application;
import com.rbkmoney.anapi.v2.auth.utils.KeycloakOpenIdStub;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {AnapiV2Application.class},
properties = {
"wiremock.server.baseUrl=http://localhost:${wiremock.server.port}",
"keycloak.auth-server-url=${wiremock.server.baseUrl}/auth"})
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 0)
@ExtendWith(SpringExtension.class)
public abstract class AbstractKeycloakOpenIdAsWiremockConfig {
@Autowired
private KeycloakOpenIdStub keycloakOpenIdStub;
@BeforeAll
public static void setUp(@Autowired KeycloakOpenIdStub keycloakOpenIdStub) throws Exception {
keycloakOpenIdStub.givenStub();
}
protected String generateInvoicesReadJwt() {
return keycloakOpenIdStub.generateJwt("invoices:read");
}
}

View File

@ -0,0 +1,199 @@
package com.rbkmoney.anapi.v2.controller;
import com.rbkmoney.anapi.v2.config.AbstractKeycloakOpenIdAsWiremockConfig;
import com.rbkmoney.anapi.v2.converter.search.request.ParamsToRefundSearchQueryConverter;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.anapi.v2.testutil.OpenApiUtil;
import com.rbkmoney.bouncer.decisions.ArbiterSrv;
import com.rbkmoney.damsel.vortigon.VortigonServiceSrv;
import com.rbkmoney.openapi.anapi_v2.model.DefaultLogicError;
import com.rbkmoney.orgmanagement.AuthContextProviderSrv;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.MultiValueMap;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createContextFragment;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createJudgementAllowed;
import static java.util.UUID.randomUUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class ErrorControllerTest extends AbstractKeycloakOpenIdAsWiremockConfig {
@Autowired
private MockMvc mockMvc;
@MockBean
private ParamsToRefundSearchQueryConverter refundSearchConverter;
@MockBean
public VortigonServiceSrv.Iface vortigonClient;
@MockBean
public AuthContextProviderSrv.Iface orgMgmtClient;
@MockBean
public ArbiterSrv.Iface bouncerClient;
private AutoCloseable mocks;
private Object[] preparedMocks;
@BeforeEach
public void init() {
mocks = MockitoAnnotations.openMocks(this);
preparedMocks = new Object[] {refundSearchConverter, vortigonClient, orgMgmtClient, bouncerClient};
}
@AfterEach
public void clean() throws Exception {
verifyNoMoreInteractions(preparedMocks);
mocks.close();
}
@Test
void testConstraintViolationException() throws Exception {
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
params.set("limit", "1001");
mockMvc.perform(
get("/payments")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDREQUEST.getValue()))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
void testBadRequestException() throws Exception {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
String message = "Error!";
doThrow(new BadRequestException(message)).when(refundSearchConverter)
.convert(any(), any(), any(), any(),
any(), any(), any(), any(),
any(), any(), any(), any());
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
mockMvc.perform(
get("/refunds")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDREQUEST.getValue()))
.andExpect(jsonPath("$.message").value(message));
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(refundSearchConverter, times(1))
.convert(any(), any(), any(), any(),
any(), any(), any(), any(),
any(), any(), any(), any());
}
@Test
void testMissingServletRequestParameterException() throws Exception {
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
params.remove("limit");
mockMvc.perform(
get("/refunds")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDREQUEST.getValue()))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
void testDeadlineException() throws Exception {
mockMvc.perform(
get("/refunds")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", "fail")
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(DefaultLogicError.CodeEnum.INVALIDDEADLINE.getValue()))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
void testInternalException() throws Exception {
when(vortigonClient.getShopsIds(any(), any())).thenReturn(List.of("1", "2", "3"));
when(orgMgmtClient.getUserContext(any())).thenReturn(createContextFragment());
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
doThrow(new RuntimeException()).when(refundSearchConverter)
.convert(any(), any(), any(), any(),
any(), any(), any(), any(),
any(), any(), any(), any());
MultiValueMap<String, String> params = OpenApiUtil.getSearchRequiredParams();
mockMvc.perform(
get("/refunds")
.header("Authorization", "Bearer " + generateInvoicesReadJwt())
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
.params(params)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$").doesNotExist());
verify(vortigonClient, times(1)).getShopsIds(any(), any());
verify(orgMgmtClient, times(1)).getUserContext(any());
verify(bouncerClient, times(1)).judge(any(), any());
verify(refundSearchConverter, times(1))
.convert(any(), any(), any(), any(),
any(), any(), any(), any(),
any(), any(), any(), any());
}
@Test
void testUnauthorizedException() throws Exception {
mockMvc.perform(
get("/refunds")
.header("X-Request-ID", randomUUID())
.header("X-Request-Deadline", "fail")
.params(OpenApiUtil.getSearchRequiredParams())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(""))
.andDo(print())
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$").doesNotExist());
}
}

View File

@ -0,0 +1,61 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.magista.ChargebackSearchQuery;
import org.junit.jupiter.api.Test;
import java.time.OffsetDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ParamsToChargebackSearchQueryConverterTest {
private static final ParamsToChargebackSearchQueryConverter converter =
new ParamsToChargebackSearchQueryConverter();
@Test
void convert() {
ChargebackSearchQuery query = converter.convert("1",
OffsetDateTime.MIN,
OffsetDateTime.MAX,
10,
List.of("1", "2"),
"1",
"2",
"3",
List.of("pending", "accepted"),
List.of("pre_arbitration"),
List.of("fraud"),
"test"
);
assertNotNull(query);
}
@Test
void mapStage() {
assertTrue(converter.mapStage("chargeback").isSetChargeback());
assertTrue(converter.mapStage("pre_arbitration").isSetPreArbitration());
assertTrue(converter.mapStage("arbitration").isSetArbitration());
assertThrows(BadRequestException.class, () -> converter.mapStage("unexpected"));
}
@Test
void mapStatus() {
assertTrue(converter.mapStatus("pending").isSetPending());
assertTrue(converter.mapStatus("accepted").isSetAccepted());
assertTrue(converter.mapStatus("rejected").isSetRejected());
assertTrue(converter.mapStatus("cancelled").isSetCancelled());
assertThrows(BadRequestException.class, () -> converter.mapStatus("unexpected"));
}
@Test
void mapCategory() {
assertTrue(converter.mapCategory("fraud").isSetFraud());
assertTrue(converter.mapCategory("dispute").isSetDispute());
assertTrue(converter.mapCategory("authorisation").isSetAuthorisation());
assertTrue(converter.mapCategory("processing_error").isSetProcessingError());
assertThrows(BadRequestException.class, () -> converter.mapCategory("unexpected"));
}
}

View File

@ -0,0 +1,52 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.magista.InvoiceSearchQuery;
import com.rbkmoney.magista.InvoiceStatus;
import com.rbkmoney.magista.PaymentParams;
import org.junit.jupiter.api.Test;
import java.time.OffsetDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ParamsToInvoiceSearchQueryConverterTest {
private static final ParamsToInvoiceSearchQueryConverter converter =
new ParamsToInvoiceSearchQueryConverter();
@Test
void convert() {
InvoiceSearchQuery query = converter.convert("1",
OffsetDateTime.MIN,
OffsetDateTime.MAX,
10,
List.of("1", "2"),
List.of("1", "2"),
"paid",
"1",
"2",
0L,
1000L,
"test");
assertNotNull(query);
}
@Test
void mapPaymentParams() {
Long amountFrom = 0L;
Long amountTo = 1000L;
PaymentParams params = converter.mapPaymentParams(amountFrom, amountTo);
assertEquals(amountFrom, params.getPaymentAmountFrom());
assertEquals(amountTo, params.getPaymentAmountTo());
}
@Test
void mapStatus() {
for (InvoiceStatus status : InvoiceStatus.values()) {
assertEquals(status, converter.mapStatus(status.name()));
}
assertThrows(BadRequestException.class, () -> converter.mapStatus("unexpected"));
}
}

View File

@ -0,0 +1,100 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.damsel.domain.LegacyBankCardPaymentSystem;
import com.rbkmoney.damsel.domain.LegacyBankCardTokenProvider;
import com.rbkmoney.damsel.domain.LegacyTerminalPaymentProvider;
import com.rbkmoney.magista.InvoicePaymentFlowType;
import com.rbkmoney.magista.PaymentSearchQuery;
import com.rbkmoney.magista.PaymentToolType;
import com.rbkmoney.openapi.anapi_v2.model.PaymentStatus;
import org.junit.jupiter.api.Test;
import java.time.OffsetDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ParamsToPaymentSearchQueryConverterTest {
private static final ParamsToPaymentSearchQueryConverter converter = new ParamsToPaymentSearchQueryConverter();
@Test
void convert() {
PaymentSearchQuery query = converter.convert("1",
OffsetDateTime.MIN,
OffsetDateTime.MAX,
10,
List.of("1", "2", "3"),
List.of("1", "2", "3"),
"cancelled",
"hold",
"paymentTerminal",
"euroset",
"1",
"1",
"1",
"mail@mail.com",
"127.0.0.1",
"fingerprint",
"1",
"123456",
"7890",
"012345678",
"123456",
"applepay",
"mastercard",
0L,
1000L,
List.of("1", "2", "3"),
"test");
assertNotNull(query);
}
@Test
void mapPaymentTool() {
assertEquals(PaymentToolType.bank_card, converter.mapPaymentTool("bankCard"));
assertEquals(PaymentToolType.payment_terminal, converter.mapPaymentTool("paymentTerminal"));
assertThrows(BadRequestException.class, () -> converter.mapPaymentTool("unexpected"));
}
@Test
void mapInvoicePaymentFlow() {
assertEquals(InvoicePaymentFlowType.instant, converter.mapInvoicePaymentFlow("instant"));
assertEquals(InvoicePaymentFlowType.hold, converter.mapInvoicePaymentFlow("hold"));
assertThrows(BadRequestException.class, () -> converter.mapInvoicePaymentFlow("unexpected"));
}
@Test
void mapStatus() {
for (PaymentStatus.StatusEnum status : PaymentStatus.StatusEnum.values()) {
assertNotNull(converter.mapStatus(status.getValue()));
}
assertThrows(BadRequestException.class, () -> converter.mapStatus("unexpected"));
}
@Test
void mapTerminalProvider() {
for (LegacyTerminalPaymentProvider provider : LegacyTerminalPaymentProvider.values()) {
assertEquals(provider, converter.mapTerminalProvider(provider.name()));
}
assertThrows(BadRequestException.class, () -> converter.mapTerminalProvider("unexpected"));
}
@Test
void mapTokenProvider() {
for (LegacyBankCardTokenProvider provider : LegacyBankCardTokenProvider.values()) {
assertEquals(provider, converter.mapTokenProvider(provider.name()));
}
assertThrows(BadRequestException.class, () -> converter.mapTokenProvider("unexpected"));
}
@Test
void mapPaymentSystem() {
for (LegacyBankCardPaymentSystem system : LegacyBankCardPaymentSystem.values()) {
assertEquals(system, converter.mapPaymentSystem(system.name()));
}
assertThrows(BadRequestException.class, () -> converter.mapPaymentSystem("unexpected"));
}
}

View File

@ -0,0 +1,38 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.magista.PayoutSearchQuery;
import com.rbkmoney.magista.PayoutToolType;
import org.junit.jupiter.api.Test;
import java.time.OffsetDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ParamsToPayoutSearchQueryConverterTest {
private static final ParamsToPayoutSearchQueryConverter converter = new ParamsToPayoutSearchQueryConverter();
@Test
void convert() {
PayoutSearchQuery query = converter.convert("1",
OffsetDateTime.MIN,
OffsetDateTime.MAX,
10,
List.of("1", "2", "3"),
"1",
"Wallet",
"test");
assertNotNull(query);
}
@Test
void mapPayoutToolType() {
assertEquals(PayoutToolType.payout_account, converter.mapPayoutToolType("PayoutAccount"));
assertEquals(PayoutToolType.wallet, converter.mapPayoutToolType("Wallet"));
assertEquals(PayoutToolType.payment_institution_account,
converter.mapPayoutToolType("PaymentInstitutionAccount"));
assertThrows(BadRequestException.class, () -> converter.mapPayoutToolType("unexpected"));
}
}

View File

@ -0,0 +1,41 @@
package com.rbkmoney.anapi.v2.converter.search.request;
import com.rbkmoney.anapi.v2.exception.BadRequestException;
import com.rbkmoney.magista.InvoicePaymentRefundStatus;
import com.rbkmoney.magista.RefundSearchQuery;
import org.junit.jupiter.api.Test;
import java.time.OffsetDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ParamsToRefundSearchQueryConverterTest {
private static final ParamsToRefundSearchQueryConverter converter = new ParamsToRefundSearchQueryConverter();
@Test
void convert() {
RefundSearchQuery query = converter.convert("1",
OffsetDateTime.MIN,
OffsetDateTime.MAX,
10,
List.of("1", "2", "3"),
List.of("1", "2", "3"),
"1",
"1",
"1",
"1",
"pending",
"test");
assertNotNull(query);
}
@Test
void mapRefundStatus() {
assertEquals(InvoicePaymentRefundStatus.succeeded, converter.mapStatus("succeeded"));
assertEquals(InvoicePaymentRefundStatus.failed, converter.mapStatus("failed"));
assertEquals(InvoicePaymentRefundStatus.pending, converter.mapStatus("pending"));
assertThrows(BadRequestException.class, () -> converter.mapStatus("unexpected"));
}
}

View File

@ -0,0 +1,56 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.base.Content;
import com.rbkmoney.damsel.domain.*;
import com.rbkmoney.magista.StatChargeback;
import com.rbkmoney.magista.StatChargebackResponse;
import com.rbkmoney.openapi.anapi_v2.model.Chargeback;
import org.junit.jupiter.api.Test;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createSearchChargebackAllResponse;
import static com.rbkmoney.anapi.v2.testutil.RandomUtil.randomBytes;
import static com.rbkmoney.anapi.v2.testutil.RandomUtil.randomString;
import static com.rbkmoney.openapi.anapi_v2.model.ChargebackCategory.*;
import static org.junit.jupiter.api.Assertions.*;
class StatChargebackToChargebackConverterTest {
private static final StatChargebackToChargebackConverter converter = new StatChargebackToChargebackConverter();
@Test
void convert() {
StatChargebackResponse magistaResponse = createSearchChargebackAllResponse();
StatChargeback magistaChargeback = magistaResponse.getChargebacks().get(0);
magistaChargeback.setContent(new Content()
.setType(randomString(10))
.setData(randomBytes(10)));
Chargeback result = converter.convert(magistaChargeback);
assertAll(
() -> assertEquals(magistaChargeback.getAmount(), result.getBodyAmount()),
() -> assertEquals(magistaChargeback.getCreatedAt(), result.getCreatedAt().toString()),
() -> assertEquals(magistaChargeback.getChargebackId(), result.getChargebackId()),
() -> assertEquals(magistaChargeback.getChargebackReason().getCode(),
result.getChargebackReason().getCode()),
() -> assertArrayEquals(magistaChargeback.getContent().getData(), result.getContent().getData()),
() -> assertEquals(magistaChargeback.getContent().getType(), result.getContent().getType()),
() -> assertEquals(magistaChargeback.getCurrencyCode().getSymbolicCode(), result.getBodyCurrency())
);
}
@Test
void mapCategory() {
assertAll(
() -> assertEquals(AUTHORISATION, converter.mapCategory(InvoicePaymentChargebackCategory.authorisation(
new InvoicePaymentChargebackCategoryAuthorisation()))),
() -> assertEquals(DISPUTE, converter.mapCategory(
InvoicePaymentChargebackCategory.dispute(new InvoicePaymentChargebackCategoryDispute()))),
() -> assertEquals(FRAUD, converter.mapCategory(
InvoicePaymentChargebackCategory.fraud(new InvoicePaymentChargebackCategoryFraud()))),
() -> assertEquals(PROCESSING_ERROR, converter.mapCategory(
InvoicePaymentChargebackCategory.processing_error(
new InvoicePaymentChargebackCategoryProcessingError()))),
() -> assertThrows(IllegalArgumentException.class,
() -> converter.mapCategory(new InvoicePaymentChargebackCategory()))
);
}
}

View File

@ -0,0 +1,91 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.*;
import com.rbkmoney.damsel.msgpack.Value;
import com.rbkmoney.magista.StatInvoice;
import com.rbkmoney.magista.StatInvoiceResponse;
import com.rbkmoney.openapi.anapi_v2.model.Invoice;
import com.rbkmoney.openapi.anapi_v2.model.InvoiceLineTaxVAT;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createSearchInvoiceAllResponse;
import static com.rbkmoney.anapi.v2.testutil.RandomUtil.randomInt;
import static com.rbkmoney.anapi.v2.testutil.RandomUtil.randomString;
import static com.rbkmoney.openapi.anapi_v2.model.Invoice.StatusEnum.*;
import static org.junit.jupiter.api.Assertions.*;
class StatInvoiceToInvoiceConverterTest {
private static final StatInvoiceToInvoiceConverter converter = new StatInvoiceToInvoiceConverter();
@Test
void convert() {
StatInvoiceResponse magistaResponse = createSearchInvoiceAllResponse();
StatInvoice magistaInvoice = magistaResponse.getInvoices().get(0);
magistaInvoice.getCart().getLines().get(0).setMetadata(Map.of("TaxMode", Value.str("10%")))
.setQuantity(randomInt(1, 10000))
.setPrice(new Cash()
.setAmount(randomInt(0, 1000000)));
Invoice result = converter.convert(magistaInvoice);
var expectedLine = magistaInvoice.getCart().getLines().get(0);
var actualLine = result.getCart().get(0);
assertAll(
() -> assertEquals(magistaInvoice.getAmount(), result.getAmount()),
() -> assertEquals(magistaInvoice.getCreatedAt(), result.getCreatedAt().toString()),
() -> assertEquals(magistaInvoice.getCurrencySymbolicCode(), result.getCurrency()),
() -> assertEquals(magistaInvoice.getExternalId(), result.getExternalID()),
() -> assertEquals(expectedLine.getPrice().getAmount(), actualLine.getPrice()),
() -> assertEquals(expectedLine.getProduct(), actualLine.getProduct()),
() -> assertEquals(expectedLine.getQuantity() * expectedLine.getPrice().getAmount(),
actualLine.getCost()),
() -> assertEquals(expectedLine.getMetadata().get("TaxMode").getStr(),
((InvoiceLineTaxVAT) actualLine.getTaxMode()).getRate().getValue()),
() -> assertEquals(magistaInvoice.getDescription(), result.getDescription()),
() -> assertEquals(magistaInvoice.getDue(), result.getDueDate().toString()),
() -> assertEquals(magistaInvoice.getId(), result.getId()),
() -> assertEquals(magistaInvoice.getProduct(), result.getProduct()),
() -> assertEquals(magistaInvoice.getShopId(), result.getShopID())
);
}
@Test
void mapStatusInfo() {
Invoice invoice = new Invoice();
InvoiceStatus status = InvoiceStatus.fulfilled(new InvoiceFulfilled()
.setDetails(randomString(10)));
converter.mapStatusInfo(invoice, status);
assertEquals(FULFILLED, invoice.getStatus());
assertEquals(status.getFulfilled().getDetails(), invoice.getReason());
invoice = new Invoice();
status = InvoiceStatus.paid(new InvoicePaid());
converter.mapStatusInfo(invoice, status);
assertEquals(PAID, invoice.getStatus());
invoice = new Invoice();
status = InvoiceStatus.unpaid(new InvoiceUnpaid());
converter.mapStatusInfo(invoice, status);
assertEquals(UNPAID, invoice.getStatus());
invoice = new Invoice();
status = InvoiceStatus.cancelled(new InvoiceCancelled()
.setDetails(randomString(10)));
converter.mapStatusInfo(invoice, status);
assertEquals(CANCELLED, invoice.getStatus());
assertEquals(status.getCancelled().getDetails(), invoice.getReason());
assertThrows(IllegalArgumentException.class, () -> converter.mapStatusInfo(new Invoice(), new InvoiceStatus()));
}
@Test
void mapTaxMode() {
assertNull(converter.mapTaxMode(Map.of()));
String taxMode = "10%";
Map<String, Value> metadata = new HashMap<>();
metadata.put("TaxMode", Value.str(taxMode));
assertEquals(taxMode, ((InvoiceLineTaxVAT)converter.mapTaxMode(metadata)).getRate().getValue());
}
}

View File

@ -0,0 +1,96 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.InvoicePaymentStatus;
import com.rbkmoney.damsel.domain.*;
import com.rbkmoney.geck.common.util.TypeUtil;
import com.rbkmoney.magista.CustomerPayer;
import com.rbkmoney.magista.InvoicePaymentFlow;
import com.rbkmoney.magista.InvoicePaymentFlowHold;
import com.rbkmoney.magista.Payer;
import com.rbkmoney.magista.*;
import com.rbkmoney.openapi.anapi_v2.model.PaymentFlow;
import com.rbkmoney.openapi.anapi_v2.model.PaymentSearchResult;
import org.junit.jupiter.api.Test;
import java.time.OffsetDateTime;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createSearchPaymentAllResponse;
import static com.rbkmoney.anapi.v2.testutil.RandomUtil.randomString;
import static com.rbkmoney.openapi.anapi_v2.model.Payer.PayerTypeEnum.*;
import static com.rbkmoney.openapi.anapi_v2.model.PaymentSearchResult.StatusEnum.*;
import static org.junit.jupiter.api.Assertions.*;
class StatPaymentToPaymentSearchResultConverterTest {
private static final StatPaymentToPaymentSearchResultConverter converter =
new StatPaymentToPaymentSearchResultConverter();
@Test
void convert() {
StatPaymentResponse magistaResponse = createSearchPaymentAllResponse();
StatPayment magistaPayment = magistaResponse.getPayments().get(0);
magistaPayment.setFlow(InvoicePaymentFlow.hold(new InvoicePaymentFlowHold()));
magistaPayment.setStatusChangedAt(TypeUtil.temporalToString(OffsetDateTime.now().toLocalDateTime()));
magistaPayment.setAdditionalTransactionInfo(new AdditionalTransactionInfo()
.setRrn(randomString(10))
.setApprovalCode(randomString(10)));
PaymentSearchResult result = converter.convert(magistaPayment);
assertAll(
() -> assertEquals(magistaPayment.getAmount(), result.getAmount()),
() -> assertEquals(magistaPayment.getCreatedAt(), result.getCreatedAt().toString()),
() -> assertEquals(magistaPayment.getCurrencySymbolicCode(), result.getCurrency()),
() -> assertEquals(magistaPayment.getExternalId(), result.getExternalID()),
() -> assertEquals(magistaPayment.getFee(), result.getFee()),
() -> assertEquals(PaymentFlow.TypeEnum.PAYMENTFLOWHOLD, result.getFlow().getType()),
() -> assertEquals(magistaPayment.getLocationInfo().getCityGeoId(),
result.getGeoLocationInfo().getCityGeoID()),
() -> assertEquals(magistaPayment.getLocationInfo().getCountryGeoId(),
result.getGeoLocationInfo().getCountryGeoID()),
() -> assertEquals(magistaPayment.getStatusChangedAt(), result.getStatusChangedAt().toString()),
() -> assertEquals(magistaPayment.getId(), result.getId()),
() -> assertEquals(magistaPayment.getInvoiceId(), result.getInvoiceID()),
() -> assertEquals(magistaPayment.isMakeRecurrent(), result.getMakeRecurrent()),
() -> assertEquals(magistaPayment.getShopId(), result.getShopID()),
() -> assertEquals(magistaPayment.getShortId(), result.getShortID()),
() -> assertEquals(magistaPayment.getAdditionalTransactionInfo().getApprovalCode(),
result.getTransactionInfo().getApprovalCode()),
() -> assertEquals(magistaPayment.getAdditionalTransactionInfo().getRrn(),
result.getTransactionInfo().getRrn())
);
}
@Test
void mapPayer() {
assertEquals(CUSTOMERPAYER, converter.mapPayer(Payer.customer(new CustomerPayer())).getPayerType());
assertEquals(PAYMENTRESOURCEPAYER,
converter.mapPayer(Payer.payment_resource(new PaymentResourcePayer())).getPayerType());
assertEquals(RECURRENTPAYER, converter.mapPayer(Payer.recurrent(new RecurrentPayer())).getPayerType());
assertThrows(IllegalArgumentException.class, () -> converter.mapPayer(new Payer()));
}
@Test
void mapStatus() {
var status = InvoicePaymentStatus.pending(new InvoicePaymentPending());
assertEquals(PENDING, converter.mapStatus(status));
status = InvoicePaymentStatus.processed(new InvoicePaymentProcessed());
assertEquals(PROCESSED, converter.mapStatus(status));
status = InvoicePaymentStatus.captured(new InvoicePaymentCaptured());
assertEquals(CAPTURED, converter.mapStatus(status));
status = InvoicePaymentStatus.cancelled(new InvoicePaymentCancelled());
assertEquals(CANCELLED, converter.mapStatus(status));
status = InvoicePaymentStatus.refunded(new InvoicePaymentRefunded());
assertEquals(REFUNDED, converter.mapStatus(status));
status = InvoicePaymentStatus.failed(new InvoicePaymentFailed());
assertEquals(FAILED, converter.mapStatus(status));
status = InvoicePaymentStatus.charged_back(new InvoicePaymentChargedBack());
assertEquals(CHARGEDBACK, converter.mapStatus(status));
assertThrows(IllegalArgumentException.class, () -> converter.mapStatus(new InvoicePaymentStatus()));
}
}

View File

@ -0,0 +1,190 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.InternationalBankAccount;
import com.rbkmoney.damsel.domain.InternationalBankDetails;
import com.rbkmoney.damsel.domain.*;
import com.rbkmoney.magista.*;
import com.rbkmoney.openapi.anapi_v2.model.*;
import org.junit.jupiter.api.Test;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createSearchPayoutAllResponse;
import static com.rbkmoney.anapi.v2.testutil.RandomUtil.randomString;
import static org.junit.jupiter.api.Assertions.*;
class StatPayoutToPayoutConverterTest {
private static final StatPayoutToPayoutConverter converter = new StatPayoutToPayoutConverter();
@Test
void convert() {
StatPayoutResponse magistaResponse = createSearchPayoutAllResponse();
StatPayout magistaPayout = magistaResponse.getPayouts().get(0);
Payout result = converter.convert(magistaPayout);
assertAll(
() -> assertEquals(magistaPayout.getAmount(), result.getAmount()),
() -> assertEquals(magistaPayout.getCreatedAt(), result.getCreatedAt().toString()),
() -> assertEquals(magistaPayout.getCurrencySymbolicCode(), result.getCurrency()),
() -> assertEquals(magistaPayout.getFee(), result.getFee()),
() -> assertEquals(magistaPayout.getId(), result.getId()),
() -> assertEquals(magistaPayout.getShopId(), result.getShopID())
);
}
@Test
void mapPayoutStatus() {
assertAll(
() -> assertEquals("Cancelled", converter.mapStatus(PayoutStatus.cancelled(new PayoutCancelled()))),
() -> assertEquals("Paid", converter.mapStatus(PayoutStatus.paid(new PayoutPaid()))),
() -> assertEquals("Confirmed", converter.mapStatus(PayoutStatus.confirmed(new PayoutConfirmed()))),
() -> assertEquals("Unpaid", converter.mapStatus(PayoutStatus.unpaid(new PayoutUnpaid()))),
() -> assertThrows(IllegalArgumentException.class, () -> converter.mapStatus(new PayoutStatus()))
);
}
@Test
void mapPayoutToolDetails() {
//RussianBankAccount
PayoutToolInfo toolInfo = new PayoutToolInfo();
toolInfo.setRussianBankAccount(new RussianBankAccount()
.setAccount(randomString(10))
.setBankBik(randomString(10))
.setBankName(randomString(10))
.setBankPostAccount(randomString(10)));
PayoutToolDetailsBankAccount actualRussianBankAccount =
(PayoutToolDetailsBankAccount) converter.mapPayoutToolDetails(toolInfo);
RussianBankAccount expectedRussianBankAccount = toolInfo.getRussianBankAccount();
assertAll(
() -> assertEquals(expectedRussianBankAccount.getAccount(), actualRussianBankAccount.getAccount()),
() -> assertEquals(expectedRussianBankAccount.getBankBik(), actualRussianBankAccount.getBankBik()),
() -> assertEquals(expectedRussianBankAccount.getBankName(), actualRussianBankAccount.getBankName()),
() -> assertEquals(expectedRussianBankAccount.getBankPostAccount(),
actualRussianBankAccount.getBankPostAccount())
);
//WalletInfo
toolInfo = new PayoutToolInfo();
toolInfo.setWalletInfo(new WalletInfo()
.setWalletId(randomString(10)));
PayoutToolDetailsWalletInfo walletActual =
(PayoutToolDetailsWalletInfo) converter.mapPayoutToolDetails(toolInfo);
WalletInfo walletExpected = toolInfo.getWalletInfo();
assertEquals(walletExpected.getWalletId(), walletActual.getWalletID());
//PaymentInstitutionAccount
toolInfo = new PayoutToolInfo();
toolInfo.setPaymentInstitutionAccount(new PaymentInstitutionAccount());
PayoutToolDetailsPaymentInstitutionAccount actualPaymentInstitutionAccount =
(PayoutToolDetailsPaymentInstitutionAccount) converter.mapPayoutToolDetails(toolInfo);
PaymentInstitutionAccount expectedPaymentInstitutionAccount = toolInfo.getPaymentInstitutionAccount();
assertNotNull(expectedPaymentInstitutionAccount);
//InternationalBankAccount
toolInfo = new PayoutToolInfo();
toolInfo.setInternationalBankAccount(new InternationalBankAccount()
.setAccountHolder(randomString(10))
.setIban(randomString(10))
.setNumber(randomString(10))
.setBank(new InternationalBankDetails()
.setName(randomString(10))
.setAbaRtn(randomString(10))
.setAddress(randomString(10))
.setBic(randomString(10))
.setCountry(CountryCode.ABW)));
PayoutToolDetailsInternationalBankAccount actualInternationalBankAccount =
(PayoutToolDetailsInternationalBankAccount) converter.mapPayoutToolDetails(toolInfo);
InternationalBankAccount expectedInternationalBankAccount = toolInfo.getInternationalBankAccount();
assertAll(
() -> assertEquals(expectedInternationalBankAccount.getIban(),
actualInternationalBankAccount.getIban()),
() -> assertEquals(expectedInternationalBankAccount.getNumber(),
actualInternationalBankAccount.getNumber()),
() -> assertEquals(expectedInternationalBankAccount.getBank().getAbaRtn(),
actualInternationalBankAccount.getBankDetails().getAbartn()),
() -> assertEquals(expectedInternationalBankAccount.getBank().getAddress(),
actualInternationalBankAccount.getBankDetails().getAddress()),
() -> assertEquals(expectedInternationalBankAccount.getBank().getBic(),
actualInternationalBankAccount.getBankDetails().getBic()),
() -> assertEquals(expectedInternationalBankAccount.getBank().getName(),
actualInternationalBankAccount.getBankDetails().getName()),
() -> assertEquals(expectedInternationalBankAccount.getBank().getCountry().name(),
actualInternationalBankAccount.getBankDetails().getCountryCode()),
//tested via mapInternationalCorrespondentBankAccount test
() -> assertNull(actualInternationalBankAccount.getCorrespondentBankAccount())
);
//Some missing type
assertThrows(IllegalArgumentException.class, () -> converter.mapPayoutToolDetails(new PayoutToolInfo()));
}
@Test
void mapCountryCode() {
CountryCode countryCode = CountryCode.ABH;
assertEquals("ABH", converter.mapCountryCode(countryCode));
assertNull(converter.mapCountryCode(null));
}
@Test
void mapInternationalCorrespondentBankAccount() {
InternationalBankAccount expected = new InternationalBankAccount()
.setAccountHolder(randomString(10))
.setIban(randomString(10))
.setNumber(randomString(10))
.setBank(new InternationalBankDetails()
.setName(randomString(10))
.setAbaRtn(randomString(10))
.setAddress(randomString(10))
.setBic(randomString(10))
.setCountry(CountryCode.ABW))
.setCorrespondentAccount(new InternationalBankAccount()
.setAccountHolder(randomString(5))
.setIban(randomString(5))
.setNumber(randomString(5))
.setBank(new InternationalBankDetails()
.setName(randomString(5))
.setAbaRtn(randomString(5))
.setAddress(randomString(5))
.setBic(randomString(5))
.setCountry(CountryCode.RUS)));
InternationalCorrespondentBankAccount actual = converter.mapInternationalCorrespondentBankAccount(expected);
assertAll(
() -> assertEquals(expected.getIban(),
actual.getIban()),
() -> assertEquals(expected.getNumber(),
actual.getNumber()),
() -> assertEquals(expected.getBank().getAbaRtn(),
actual.getBankDetails().getAbartn()),
() -> assertEquals(expected.getBank().getAddress(),
actual.getBankDetails().getAddress()),
() -> assertEquals(expected.getBank().getBic(),
actual.getBankDetails().getBic()),
() -> assertEquals(expected.getBank().getName(),
actual.getBankDetails().getName()),
() -> assertEquals(expected.getBank().getCountry().name(),
actual.getBankDetails().getCountryCode()),
() -> assertEquals(expected.getCorrespondentAccount().getIban(),
actual.getCorrespondentBankAccount().getIban()),
() -> assertEquals(expected.getCorrespondentAccount().getNumber(),
actual.getCorrespondentBankAccount().getNumber()),
() -> assertEquals(expected.getCorrespondentAccount().getBank().getAbaRtn(),
actual.getCorrespondentBankAccount().getBankDetails().getAbartn()),
() -> assertEquals(expected.getCorrespondentAccount().getBank().getAddress(),
actual.getCorrespondentBankAccount().getBankDetails().getAddress()),
() -> assertEquals(expected.getCorrespondentAccount().getBank().getBic(),
actual.getCorrespondentBankAccount().getBankDetails().getBic()),
() -> assertEquals(expected.getCorrespondentAccount().getBank().getName(),
actual.getCorrespondentBankAccount().getBankDetails().getName()),
() -> assertEquals(expected.getCorrespondentAccount().getBank().getCountry().name(),
actual.getCorrespondentBankAccount().getBankDetails().getCountryCode()),
() -> assertNull(actual.getCorrespondentBankAccount().getCorrespondentBankAccount())
);
}
}

View File

@ -0,0 +1,63 @@
package com.rbkmoney.anapi.v2.converter.search.response;
import com.rbkmoney.damsel.domain.*;
import com.rbkmoney.magista.StatRefund;
import com.rbkmoney.magista.StatRefundResponse;
import com.rbkmoney.openapi.anapi_v2.model.RefundSearchResult;
import com.rbkmoney.openapi.anapi_v2.model.RefundStatusError;
import org.junit.jupiter.api.Test;
import static com.rbkmoney.anapi.v2.testutil.MagistaUtil.createSearchRefundAllResponse;
import static com.rbkmoney.anapi.v2.testutil.RandomUtil.randomString;
import static com.rbkmoney.openapi.anapi_v2.model.RefundSearchResult.StatusEnum.*;
import static org.junit.jupiter.api.Assertions.*;
class StatRefundToRefundSearchResultConverterTest {
private static final StatRefundToRefundSearchResultConverter converter =
new StatRefundToRefundSearchResultConverter();
@Test
void convert() {
StatRefundResponse magistaResponse = createSearchRefundAllResponse();
StatRefund magistaRefund = magistaResponse.getRefunds().get(0);
RefundSearchResult result = converter.convert(magistaRefund);
assertAll(
() -> assertEquals(magistaRefund.getAmount(), result.getAmount()),
() -> assertEquals(magistaRefund.getCreatedAt(), result.getCreatedAt().toString()),
() -> assertEquals(magistaRefund.getCurrencySymbolicCode(), result.getCurrency()),
() -> assertEquals(magistaRefund.getId(), result.getId()),
() -> assertEquals(magistaRefund.getShopId(), result.getShopID()),
() -> assertEquals(magistaRefund.getExternalId(), result.getExternalID()),
() -> assertEquals(magistaRefund.getInvoiceId(), result.getInvoiceID()),
() -> assertEquals(magistaRefund.getPaymentId(), result.getPaymentID()),
() -> assertEquals(magistaRefund.getReason(), result.getReason())
);
}
@Test
void mapStatusError() {
assertNull(converter.mapStatusError(new InvoicePaymentRefundStatus()));
String reason = randomString(10);
String code = randomString(10);
InvoicePaymentRefundStatus status = InvoicePaymentRefundStatus
.failed(new InvoicePaymentRefundFailed()
.setFailure(OperationFailure.failure(new Failure()
.setReason(reason)
.setCode(code))));
RefundStatusError statusError = converter.mapStatusError(status);
assertEquals(reason, statusError.getMessage());
assertEquals(code, statusError.getCode());
}
@Test
void mapStatus() {
assertEquals(PENDING,
converter.mapStatus(InvoicePaymentRefundStatus.pending(new InvoicePaymentRefundPending())));
assertEquals(FAILED,
converter.mapStatus(InvoicePaymentRefundStatus.failed(new InvoicePaymentRefundFailed())));
assertEquals(SUCCEEDED,
converter.mapStatus(InvoicePaymentRefundStatus.succeeded(new InvoicePaymentRefundSucceeded())));
assertThrows(IllegalArgumentException.class, () -> converter.mapStatus(new InvoicePaymentRefundStatus()));
}
}

View File

@ -0,0 +1,158 @@
package com.rbkmoney.anapi.v2.testutil;
import com.rbkmoney.bouncer.ctx.ContextFragment;
import com.rbkmoney.bouncer.decisions.Judgement;
import com.rbkmoney.bouncer.decisions.Resolution;
import com.rbkmoney.bouncer.decisions.ResolutionAllowed;
import com.rbkmoney.damsel.domain.InvoicePaymentRefundStatus;
import com.rbkmoney.damsel.domain.InvoicePaymentStatus;
import com.rbkmoney.damsel.domain.InvoiceStatus;
import com.rbkmoney.damsel.domain.*;
import com.rbkmoney.damsel.geo_ip.LocationInfo;
import com.rbkmoney.geck.serializer.kit.mock.FieldHandler;
import com.rbkmoney.geck.serializer.kit.mock.MockMode;
import com.rbkmoney.geck.serializer.kit.mock.MockTBaseProcessor;
import com.rbkmoney.geck.serializer.kit.tbase.TBaseHandler;
import com.rbkmoney.magista.InvoicePaymentFlow;
import com.rbkmoney.magista.InvoicePaymentFlowInstant;
import com.rbkmoney.magista.*;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.apache.thrift.TBase;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@UtilityClass
public class MagistaUtil {
private static final MockTBaseProcessor mockRequiredTBaseProcessor;
static {
mockRequiredTBaseProcessor = new MockTBaseProcessor(MockMode.REQUIRED_ONLY, 15, 1);
Map.Entry<FieldHandler, String[]> timeFields = Map.entry(
structHandler -> structHandler.value(Instant.now().toString()),
new String[] {"created_at", "at", "due", "status_changed_at"}
);
mockRequiredTBaseProcessor.addFieldHandler(timeFields.getKey(), timeFields.getValue());
}
public static StatPaymentResponse createSearchPaymentRequiredResponse() {
return fillRequiredTBaseObject(new StatPaymentResponse(), StatPaymentResponse.class);
}
public static StatChargebackResponse createSearchChargebackRequiredResponse() {
return fillRequiredTBaseObject(new StatChargebackResponse(), StatChargebackResponse.class);
}
public static StatPaymentResponse createSearchPaymentAllResponse() {
var payment = fillRequiredTBaseObject(new StatPayment(), StatPayment.class);
var status = new InvoicePaymentStatus();
status.setPending(new InvoicePaymentPending());
var cart = fillRequiredTBaseObject(new InvoiceCart(), InvoiceCart.class);
var line = fillRequiredTBaseObject(new InvoiceLine(), InvoiceLine.class);
var instant = fillRequiredTBaseObject(new InvoicePaymentFlowInstant(), InvoicePaymentFlowInstant.class);
var locationInfo = fillRequiredTBaseObject(new LocationInfo(), LocationInfo.class);
var response = fillRequiredTBaseObject(new StatPaymentResponse(), StatPaymentResponse.class);
return response.setPayments(
List.of(payment
.setStatus(status)
.setCart(cart.setLines(List.of(line)))
.setFlow(InvoicePaymentFlow
.instant(instant))
.setLocationInfo(locationInfo)));
}
public static StatChargebackResponse createSearchChargebackAllResponse() {
var chargeback = fillRequiredTBaseObject(new StatChargeback(), StatChargeback.class);
var stage = fillRequiredTBaseObject(new InvoicePaymentChargebackStage(), InvoicePaymentChargebackStage.class);
var reason =
fillRequiredTBaseObject(new InvoicePaymentChargebackReason(), InvoicePaymentChargebackReason.class);
var status =
fillRequiredTBaseObject(new InvoicePaymentChargebackStatus(), InvoicePaymentChargebackStatus.class);
var response = fillRequiredTBaseObject(new StatChargebackResponse(), StatChargebackResponse.class);
return response.setChargebacks(
List.of(chargeback
.setStage(stage)
.setChargebackReason(reason)
.setChargebackStatus(status))
);
}
public static StatRefundResponse createSearchRefundRequiredResponse() {
return fillRequiredTBaseObject(new StatRefundResponse(), StatRefundResponse.class);
}
public static StatRefundResponse createSearchRefundAllResponse() {
var refund = fillRequiredTBaseObject(new StatRefund(), StatRefund.class);
var cart = fillRequiredTBaseObject(new InvoiceCart(), InvoiceCart.class);
var line = fillRequiredTBaseObject(new InvoiceLine(), InvoiceLine.class);
var cash = fillRequiredTBaseObject(new Cash(), Cash.class);
var status = fillRequiredTBaseObject(new InvoicePaymentRefundStatus(), InvoicePaymentRefundStatus.class);
var response = fillRequiredTBaseObject(new StatRefundResponse(), StatRefundResponse.class);
return response.setRefunds(
List.of(refund
.setCart(cart
.setLines(List.of(line.setPrice(cash))))
.setStatus(status))
);
}
public static StatInvoiceResponse createSearchInvoiceRequiredResponse() {
return fillRequiredTBaseObject(new StatInvoiceResponse(), StatInvoiceResponse.class);
}
public static StatInvoiceResponse createSearchInvoiceAllResponse() {
var invoice = fillRequiredTBaseObject(new StatInvoice(), StatInvoice.class);
var cart = fillRequiredTBaseObject(new InvoiceCart(), InvoiceCart.class);
var line = fillRequiredTBaseObject(new InvoiceLine(), InvoiceLine.class);
var cash = fillRequiredTBaseObject(new Cash(), Cash.class);
var status = fillRequiredTBaseObject(new InvoiceStatus(),
InvoiceStatus.class);
var response = fillRequiredTBaseObject(new StatInvoiceResponse(), StatInvoiceResponse.class);
return response.setInvoices(
List.of(invoice
.setCart(cart
.setLines(List.of(line.setPrice(cash))))
.setStatus(status))
);
}
public static StatPayoutResponse createSearchPayoutRequiredResponse() {
return fillRequiredTBaseObject(new StatPayoutResponse(), StatPayoutResponse.class);
}
public static StatPayoutResponse createSearchPayoutAllResponse() {
var payout = fillRequiredTBaseObject(new StatPayout(), StatPayout.class);
var toolInfo = fillRequiredTBaseObject(new PayoutToolInfo(), PayoutToolInfo.class);
var bank = fillRequiredTBaseObject(new RussianBankAccount(), RussianBankAccount.class);
var status = fillRequiredTBaseObject(new PayoutStatus(), PayoutStatus.class);
var response = fillRequiredTBaseObject(new StatPayoutResponse(), StatPayoutResponse.class);
toolInfo.setRussianBankAccount(bank);
return response.setPayouts(
List.of(payout
.setPayoutToolInfo(toolInfo)
.setStatus(status))
);
}
public static ContextFragment createContextFragment() {
return fillRequiredTBaseObject(new ContextFragment(), ContextFragment.class);
}
public static Judgement createJudgementAllowed() {
Resolution resolution = new Resolution();
resolution.setAllowed(new ResolutionAllowed());
return new Judgement().setResolution(resolution);
}
@SneakyThrows
public static <T extends TBase> T fillRequiredTBaseObject(T tbase, Class<T> type) {
return mockRequiredTBaseProcessor.process(tbase, new TBaseHandler<>(type));
}
}

View File

@ -0,0 +1,127 @@
package com.rbkmoney.anapi.v2.testutil;
import com.rbkmoney.damsel.domain.PaymentInstitutionRealm;
import com.rbkmoney.damsel.merch_stat.TerminalPaymentProvider;
import com.rbkmoney.openapi.anapi_v2.model.*;
import lombok.experimental.UtilityClass;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static com.rbkmoney.anapi.v2.testutil.RandomUtil.randomIntegerAsString;
@UtilityClass
public class OpenApiUtil {
public static MultiValueMap<String, String> getSearchRequiredParams() {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("partyID", randomIntegerAsString(1, 1000));
params.add("fromTime", "2007-12-03T10:15:30+01:00");
params.add("toTime", "2020-12-03T10:15:30+01:00");
params.add("limit", randomIntegerAsString(1, 40));
return params;
}
public static MultiValueMap<String, String> getSearchPaymentAllParams() {
MultiValueMap<String, String> params = getSearchRequiredParams();
params.add("shopID", randomIntegerAsString(1, 10));
params.add("shopIDs", randomIntegerAsString(11, 20));
params.add("shopIDs", randomIntegerAsString(21, 30));
params.add("paymentInstitutionRealm", PaymentInstitutionRealm.live.name());
params.add("invoiceIDs", randomIntegerAsString(1, 10));
params.add("invoiceIDs", randomIntegerAsString(11, 20));
params.add("paymentStatus", PaymentStatus.StatusEnum.PENDING.getValue());
params.add("paymentFlow", "instant");
params.add("paymentMethod", "paymentTerminal");
params.add("paymentTerminalProvider", TerminalPaymentProvider.alipay.name());
params.add("invoiceID", randomIntegerAsString(1, 1000));
params.add("paymentID", randomIntegerAsString(1, 1000));
params.add("externalID", randomIntegerAsString(1, 1000));
params.add("payerEmail", "payer@mail.com");
params.add("payerIP", "0.0.0.0");
params.add("payerFingerprint", "iamveryunique");
params.add("first6", randomIntegerAsString(100000, 999999));
params.add("last4", randomIntegerAsString(1000, 9999));
params.add("rrn", "123456789010");
params.add("approvalCode", "QWERTY");
params.add("bankCardTokenProvider", "applepay");
params.add("bankCardPaymentSystem", "mastercard");
params.add("paymentAmountFrom", randomIntegerAsString(1, 9999));
params.add("paymentAmountTo", randomIntegerAsString(9999, 999999));
params.add("excludedShops", randomIntegerAsString(1, 10));
params.add("excludedShops", randomIntegerAsString(11, 20));
params.add("continuationToken", "test");
return params;
}
public static MultiValueMap<String, String> getSearchChargebackAllParams() {
MultiValueMap<String, String> params = getSearchRequiredParams();
params.add("shopID", randomIntegerAsString(1, 10));
params.add("shopIDs", randomIntegerAsString(11, 20));
params.add("shopIDs", randomIntegerAsString(21, 30));
params.add("paymentInstitutionRealm", PaymentInstitutionRealm.live.name());
params.add("offset", randomIntegerAsString(1, 10));
params.add("invoiceID", randomIntegerAsString(1, 1000));
params.add("paymentID", randomIntegerAsString(1, 1000));
params.add("chargebackID", randomIntegerAsString(1, 1000));
params.add("chargebackStatuses", ChargebackStatus.PENDING.getValue());
params.add("chargebackStages", ChargebackStage.CHARGEBACK.getValue());
params.add("chargebackCategories", ChargebackCategory.AUTHORISATION.getValue());
params.add("continuationToken", "test");
return params;
}
public static MultiValueMap<String, String> getSearchRefundAllParams() {
MultiValueMap<String, String> params = getSearchRequiredParams();
params.add("shopID", randomIntegerAsString(1, 10));
params.add("shopIDs", randomIntegerAsString(11, 20));
params.add("shopIDs", randomIntegerAsString(21, 30));
params.add("paymentInstitutionRealm", PaymentInstitutionRealm.live.name());
params.add("offset", randomIntegerAsString(1, 10));
params.add("invoiceIDs", randomIntegerAsString(1, 10));
params.add("invoiceIDs", randomIntegerAsString(11, 20));
params.add("invoiceID", randomIntegerAsString(1, 1000));
params.add("paymentID", randomIntegerAsString(1, 1000));
params.add("refundID", randomIntegerAsString(1, 1000));
params.add("externalID", randomIntegerAsString(1, 1000));
params.add("refundStatus", "pending");
params.add("excludedShops", randomIntegerAsString(1, 10));
params.add("excludedShops", randomIntegerAsString(11, 20));
params.add("continuationToken", "test");
return params;
}
public static MultiValueMap<String, String> getSearchInvoiceAllParams() {
MultiValueMap<String, String> params = getSearchRequiredParams();
params.add("shopID", randomIntegerAsString(1, 10));
params.add("shopIDs", randomIntegerAsString(11, 20));
params.add("shopIDs", randomIntegerAsString(21, 30));
params.add("paymentInstitutionRealm", PaymentInstitutionRealm.live.name());
params.add("invoiceIDs", randomIntegerAsString(1, 10));
params.add("invoiceIDs", randomIntegerAsString(11, 20));
params.add("invoiceStatus", "paid");
params.add("invoiceID", randomIntegerAsString(1, 1000));
params.add("externalID", randomIntegerAsString(1, 1000));
params.add("invoiceAmountFrom", randomIntegerAsString(1, 1000));
params.add("invoiceAmountTo", randomIntegerAsString(1, 1000));
params.add("excludedShops", randomIntegerAsString(1, 10));
params.add("excludedShops", randomIntegerAsString(11, 20));
params.add("continuationToken", "test");
return params;
}
public static MultiValueMap<String, String> getSearchPayoutAllParams() {
MultiValueMap<String, String> params = getSearchRequiredParams();
params.add("shopID", randomIntegerAsString(1, 10));
params.add("shopIDs", randomIntegerAsString(11, 20));
params.add("shopIDs", randomIntegerAsString(21, 30));
params.add("paymentInstitutionRealm", PaymentInstitutionRealm.live.name());
params.add("offset", randomIntegerAsString(1, 10));
params.add("payoutID", randomIntegerAsString(1, 1000));
params.add("payoutToolType", "PayoutAccount");
params.add("excludedShops", randomIntegerAsString(1, 10));
params.add("excludedShops", randomIntegerAsString(11, 20));
params.add("continuationToken", "test");
return params;
}
}

View File

@ -0,0 +1,31 @@
package com.rbkmoney.anapi.v2.testutil;
import lombok.experimental.UtilityClass;
import java.nio.charset.StandardCharsets;
import java.util.Random;
@UtilityClass
public class RandomUtil {
private static final Random random = new Random();
public static int randomInt(int from, int to) {
return random.nextInt(to - from) + from;
}
public static String randomIntegerAsString(int from, int to) {
return String.valueOf(random.nextInt(to - from) + from);
}
public static String randomString(int length) {
return new String(randomBytes(length), StandardCharsets.UTF_8);
}
public static byte[] randomBytes(int length) {
byte[] array = new byte[length];
new Random().nextBytes(array);
return array;
}
}

View File

@ -0,0 +1,28 @@
package com.rbkmoney.anapi.v2.util;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class ConverterUtilTest {
@Test
void testMerge() {
String id = "1";
List<String> ids = List.of("2", "3");
List<String> result = ConverterUtil.merge(id, ids);
assertEquals(List.of("1", "2", "3"), result);
result = ConverterUtil.merge(null, ids);
assertEquals(List.of("2", "3"), result);
result = ConverterUtil.merge(id, null);
assertEquals(List.of("1"), result);
result = ConverterUtil.merge(null, null);
assertNotNull(result);
}
}

View File

@ -0,0 +1,87 @@
package com.rbkmoney.anapi.v2.util;
import com.rbkmoney.anapi.v2.exception.DeadlineException;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import static com.rbkmoney.anapi.v2.util.DeadlineUtil.*;
import static org.junit.jupiter.api.Assertions.*;
class DeadlineUtilTest {
@Test
void checkDeadlineTest() {
checkDeadline(null, null);
checkDeadline("12m", null);
checkDeadline("1.2m", null);
assertThrows(DeadlineException.class, () -> checkDeadline("-1.2m", null));
checkDeadline("12s", null);
checkDeadline("1.2s", null);
assertThrows(DeadlineException.class, () -> checkDeadline("-1.2s", null));
checkDeadline("12ms", null);
assertThrows(DeadlineException.class, () -> checkDeadline("1.2ms", null));
assertThrows(DeadlineException.class, () -> checkDeadline("-12ms", null));
checkDeadline("12m12s12ms", null);
checkDeadline("1.2m1.2s12ms", null);
assertThrows(DeadlineException.class, () -> checkDeadline("1.2m1.2s1.2ms", null));
assertThrows(DeadlineException.class, () -> checkDeadline("12s12s", null));
assertThrows(DeadlineException.class, () -> checkDeadline("12m12m", null));
assertThrows(DeadlineException.class, () -> checkDeadline("12ms12ms", null));
assertThrows(DeadlineException.class, () -> checkDeadline("12s12ms12ms", null));
assertThrows(DeadlineException.class, () -> checkDeadline("12s12s12ms", null));
checkDeadline(Instant.now().plus(1, ChronoUnit.DAYS).toString(), null);
assertThrows(DeadlineException.class,
() -> checkDeadline(Instant.now().minus(1, ChronoUnit.DAYS).toString(), null));
assertThrows(DeadlineException.class, () -> checkDeadline("undefined", null));
}
@Test
void extractMillisecondsTest() {
assertEquals(12, (long) extractMilliseconds("12ms", null));
assertEquals(12, (long) extractMilliseconds("1.2m1.2s12ms", null));
assertThrows(DeadlineException.class, () -> extractMilliseconds("1.2ms", null));
assertThrows(DeadlineException.class, () -> extractMilliseconds("-12ms", null));
assertThrows(DeadlineException.class, () -> extractMilliseconds("12ms12ms", null));
}
@Test
void extractSecondsTest() {
assertEquals(12000, (long) extractSeconds("12s", null));
assertEquals(1200, (long) extractSeconds("1.2s", null));
assertEquals(1200, (long) extractSeconds("1.2m1.2s12ms", null));
assertThrows(DeadlineException.class, () -> extractSeconds("-1.2s", null));
assertThrows(DeadlineException.class, () -> extractSeconds("12s12s", null));
}
@Test
void extractMinutesTest() {
assertEquals(720000, (long) extractMinutes("12m", null));
assertEquals(72000, (long) extractMinutes("1.2m", null));
assertEquals(72000, (long) extractMinutes("1.2m1.2s12ms", null));
assertThrows(DeadlineException.class, () -> extractMinutes("-1.2m", null));
assertThrows(DeadlineException.class, () -> extractMinutes("12m12m", null));
}
@Test
void containsRelativeValuesTest() {
assertTrue(containsRelativeValues("12m", null));
assertTrue(containsRelativeValues("12s", null));
assertTrue(containsRelativeValues("12ms", null));
assertTrue(containsRelativeValues("1.2m1.2s12ms", null));
assertThrows(DeadlineException.class, () -> containsRelativeValues("-1.2s", null));
assertThrows(DeadlineException.class, () -> containsRelativeValues("12s12s", null));
assertFalse(containsRelativeValues(Instant.now().toString(), null));
assertFalse(containsRelativeValues("asd", null));
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="warn">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.rbkmoney.woody" level="ALL"/>
</configuration>