From 44c3657ec29541e528967598c12c729adbfb5004 Mon Sep 17 00:00:00 2001 From: Baikov Dmitrii <44803026+D-Baykov@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:23:32 +0300 Subject: [PATCH] TD-955: Add implementation (#1) --- .github/settings.yml | 2 + .github/workflows/build.yml | 10 + .github/workflows/deploy.yml | 15 + .gitignore | 83 ++++ pom.xml | 385 ++++++++++++++++++ renovate.json | 4 + .../liminator/LiminatorApplication.java | 16 + .../liminator/config/ApplicationConfig.java | 14 + ...reateLimitRequestToLimitDataConverter.java | 22 + ...ntLimitValuesToLimitResponseConverter.java | 30 ++ .../converter/OperationConverter.java | 9 + .../impl/OperationConverterImpl.java | 24 ++ .../empayre/liminator/dao/AbstractDao.java | 23 ++ .../com/empayre/liminator/dao/CommonDao.java | 8 + .../liminator/dao/LimitContextDao.java | 8 + .../empayre/liminator/dao/LimitDataDao.java | 13 + .../empayre/liminator/dao/OperationDao.java | 21 + .../dao/impl/LimitContextDaoImpl.java | 37 ++ .../liminator/dao/impl/LimitDataDaoImpl.java | 52 +++ .../liminator/dao/impl/OperationDaoImpl.java | 143 +++++++ .../exception/BusinessException.java | 7 + .../liminator/exception/DaoException.java | 23 ++ .../exception/NotFoundException.java | 7 + .../exception/SerializationException.java | 7 + .../handler/CommitLimitAmountHandler.java | 44 ++ .../liminator/handler/CreateLimitHandler.java | 68 ++++ .../handler/GetLimitAmountHandler.java | 28 ++ .../empayre/liminator/handler/Handler.java | 8 + .../handler/HoldLimitAmountHandler.java | 55 +++ .../handler/RollbackLimitAmountHandler.java | 39 ++ .../empayre/liminator/model/LimitValue.java | 13 + .../liminator/service/LiminatorService.java | 59 +++ .../service/LimitsGettingService.java | 37 ++ .../servlet/LiminatorServiceServlet.java | 30 ++ .../liminator/util/LimitDataUtils.java | 29 ++ src/main/resources/application.yml | 97 +++++ .../checkstyle/checkstyle-suppressions.xml | 12 + .../db/migration/V1_1__add_base_tables.sql | 44 ++ .../config/PostgresqlJooqSpringBootITest.java | 16 + .../config/PostgresqlSpringBootITest.java | 16 + .../com/empayre/liminator/dao/DaoTests.java | 143 +++++++ .../service/LiminatorServiceTest.java | 97 +++++ 42 files changed, 1798 insertions(+) create mode 100644 .github/settings.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 renovate.json create mode 100644 src/main/java/com/empayre/liminator/LiminatorApplication.java create mode 100644 src/main/java/com/empayre/liminator/config/ApplicationConfig.java create mode 100644 src/main/java/com/empayre/liminator/converter/CreateLimitRequestToLimitDataConverter.java create mode 100644 src/main/java/com/empayre/liminator/converter/CurrentLimitValuesToLimitResponseConverter.java create mode 100644 src/main/java/com/empayre/liminator/converter/OperationConverter.java create mode 100644 src/main/java/com/empayre/liminator/converter/impl/OperationConverterImpl.java create mode 100644 src/main/java/com/empayre/liminator/dao/AbstractDao.java create mode 100644 src/main/java/com/empayre/liminator/dao/CommonDao.java create mode 100644 src/main/java/com/empayre/liminator/dao/LimitContextDao.java create mode 100644 src/main/java/com/empayre/liminator/dao/LimitDataDao.java create mode 100644 src/main/java/com/empayre/liminator/dao/OperationDao.java create mode 100644 src/main/java/com/empayre/liminator/dao/impl/LimitContextDaoImpl.java create mode 100644 src/main/java/com/empayre/liminator/dao/impl/LimitDataDaoImpl.java create mode 100644 src/main/java/com/empayre/liminator/dao/impl/OperationDaoImpl.java create mode 100644 src/main/java/com/empayre/liminator/exception/BusinessException.java create mode 100644 src/main/java/com/empayre/liminator/exception/DaoException.java create mode 100644 src/main/java/com/empayre/liminator/exception/NotFoundException.java create mode 100644 src/main/java/com/empayre/liminator/exception/SerializationException.java create mode 100644 src/main/java/com/empayre/liminator/handler/CommitLimitAmountHandler.java create mode 100644 src/main/java/com/empayre/liminator/handler/CreateLimitHandler.java create mode 100644 src/main/java/com/empayre/liminator/handler/GetLimitAmountHandler.java create mode 100644 src/main/java/com/empayre/liminator/handler/Handler.java create mode 100644 src/main/java/com/empayre/liminator/handler/HoldLimitAmountHandler.java create mode 100644 src/main/java/com/empayre/liminator/handler/RollbackLimitAmountHandler.java create mode 100644 src/main/java/com/empayre/liminator/model/LimitValue.java create mode 100644 src/main/java/com/empayre/liminator/service/LiminatorService.java create mode 100644 src/main/java/com/empayre/liminator/service/LimitsGettingService.java create mode 100644 src/main/java/com/empayre/liminator/servlet/LiminatorServiceServlet.java create mode 100644 src/main/java/com/empayre/liminator/util/LimitDataUtils.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/checkstyle/checkstyle-suppressions.xml create mode 100644 src/main/resources/db/migration/V1_1__add_base_tables.sql create mode 100644 src/test/java/com/empayre/liminator/config/PostgresqlJooqSpringBootITest.java create mode 100644 src/test/java/com/empayre/liminator/config/PostgresqlSpringBootITest.java create mode 100644 src/test/java/com/empayre/liminator/dao/DaoTests.java create mode 100644 src/test/java/com/empayre/liminator/service/LiminatorServiceTest.java diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..9267e7d --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,2 @@ +# These settings are synced to GitHub by https://probot.github.io/apps/settings/ +_extends: .github diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4af9d57 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,10 @@ +name: Build Maven Artifact + +on: + pull_request: + branches: + - '**' + +jobs: + build: + uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v3 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..385233c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,15 @@ +name: Deploy Docker Image + +on: + push: + branches: + - 'master' + - 'main' + - 'epic/**' + +jobs: + build-and-deploy: + uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v3 + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + mm-webhook-url: ${{ secrets.MATTERMOST_WEBHOOK_URL }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2e2da2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# Created by .ignore support plugin (hsz.mobi) +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +### JetBrains template +# 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 + +## File-based project format: +*.iws +*.ipr +*.iml + +## Plugin-specific files: + +# 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 +### Java template +*.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 + + +# OSX +*.DS_Store +.AppleDouble +.LSOverride + +# TestContainers +.testcontainers-* \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..edcfa74 --- /dev/null +++ b/pom.xml @@ -0,0 +1,385 @@ + + + 4.0.0 + + + dev.vality + service-parent-pom + 3.0.2 + + + + liminator + 1.0.0 + jar + + liminator + Service for interation with the dominant data + + + ${env.REGISTRY} + 8022 + 8023 + ${server.port} ${management.port} + jdbc:postgresql://localhost:5432/liminator + postgres + postgres + liminator + lim + jdbc:postgresql://localhost:5432/liminator + 5432 + ./src/main/resources/checkstyle/checkstyle-suppressions.xml + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.hibernate + hibernate-validator + + + org.yaml + snakeyaml + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.kafka + spring-kafka + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework + spring-web + 6.1.6 + + + org.springframework + spring-webmvc + 6.1.6 + + + + + jakarta.servlet + jakarta.servlet-api + + + org.postgresql + postgresql + + + com.zaxxer + HikariCP + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + + + org.jooq + jooq + 3.19.11 + + + net.logstash.logback + logstash-logback-encoder + 7.4 + + + com.fasterxml.jackson.core + jackson-databind + + + org.projectlombok + lombok + provided + + + com.github.ben-manes.caffeine + caffeine + + + org.apache.commons + commons-lang3 + + + net.javacrumbs.shedlock + shedlock-provider-jdbc-template + 4.46.0 + + + net.javacrumbs.shedlock + shedlock-spring + 4.46.0 + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-registry-prometheus + + + org.yaml + snakeyaml + 2.0 + + + software.amazon.msk + aws-msk-iam-auth + 2.0.3 + + + + + dev.vality + db-common-lib + + + dev.vality + liminator-proto + 1.2-63529e6 + + + dev.vality.woody + woody-thrift + ${woody.version} + + + dev.vality + limiter-proto + 1.33-31de59b + + + dev.vality + shared-resources + ${shared-resources.version} + + + dev.vality.geck + serializer + 1.0.1 + + + dev.vality.geck + filter + 1.0.1 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + dev.vality + testcontainers-annotations + 2.0.3 + test + + + org.awaitility + awaitility + 4.2.0 + test + + + + + + + ${project.build.directory}/maven-shared-archive-resources + ${project.build.directory} + + Dockerfile + + true + + + ${project.build.directory}/maven-shared-archive-resources + true + + Dockerfile + + + + src/main/resources + true + + + + + org.springframework.boot + spring-boot-maven-plugin + + + dev.vality.maven.plugins + pg-embedded-plugin + 2.0.0 + + ${local.pg.port} + ${db.name} + + ${db.schema} + + + + + PG_server_start + initialize + + start + + + + PG_server_stop + generate-resources + + stop + + + + + + org.flywaydb + flyway-maven-plugin + + ${local.pg.url} + ${db.user} + ${db.password} + + ${db.schema} + + + + + migrate + initialize + + migrate + + + + + + org.postgresql + postgresql + 42.7.3 + + + + + org.jooq + jooq-codegen-maven + + + org.postgresql.Driver + ${local.pg.url} + ${db.user} + ${db.password} + + + + true + true + true + true + + + org.jooq.meta.postgres.PostgresDatabase + .* + + schema_version|.*func + + ${db.schema} + + + com.empayre.liminator.domain + target/generated-sources/ + + + + + + gen-src + initialize + + generate + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Dfile.encoding=UTF-8 + + + + org.apache.maven.plugins + maven-remote-resources-plugin + 3.1.0 + + + org.apache.maven.shared + maven-filtering + 3.3.1 + + + + + dev.vality:shared-resources:${shared-resources.version} + + false + false + + + + + process + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + p12 + + + + + + + diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..a20bfd6 --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["local>valitydev/.github:renovate-config"] +} diff --git a/src/main/java/com/empayre/liminator/LiminatorApplication.java b/src/main/java/com/empayre/liminator/LiminatorApplication.java new file mode 100644 index 0000000..91f8872 --- /dev/null +++ b/src/main/java/com/empayre/liminator/LiminatorApplication.java @@ -0,0 +1,16 @@ +package com.empayre.liminator; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@ServletComponentScan +@SpringBootApplication(scanBasePackages = {"com.empayre.liminator"}) +public class LiminatorApplication { + + public static void main(String[] args) { + SpringApplication.run(LiminatorApplication.class, args); + } +} diff --git a/src/main/java/com/empayre/liminator/config/ApplicationConfig.java b/src/main/java/com/empayre/liminator/config/ApplicationConfig.java new file mode 100644 index 0000000..87755c3 --- /dev/null +++ b/src/main/java/com/empayre/liminator/config/ApplicationConfig.java @@ -0,0 +1,14 @@ +package com.empayre.liminator.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ApplicationConfig { + + @Bean + public ObjectMapper mapper() { + return new ObjectMapper(); + } +} diff --git a/src/main/java/com/empayre/liminator/converter/CreateLimitRequestToLimitDataConverter.java b/src/main/java/com/empayre/liminator/converter/CreateLimitRequestToLimitDataConverter.java new file mode 100644 index 0000000..1777cbf --- /dev/null +++ b/src/main/java/com/empayre/liminator/converter/CreateLimitRequestToLimitDataConverter.java @@ -0,0 +1,22 @@ +package com.empayre.liminator.converter; + +import com.empayre.liminator.domain.tables.pojos.LimitData; +import dev.vality.liminator.CreateLimitRequest; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Component +public class CreateLimitRequestToLimitDataConverter implements Converter { + + @Override + public LimitData convert(CreateLimitRequest request) { + LimitData data = new LimitData(); + data.setName(request.getLimitName()); + data.setCreatedAt(LocalDate.now()); + data.setWtime(LocalDateTime.now()); + return data; + } +} diff --git a/src/main/java/com/empayre/liminator/converter/CurrentLimitValuesToLimitResponseConverter.java b/src/main/java/com/empayre/liminator/converter/CurrentLimitValuesToLimitResponseConverter.java new file mode 100644 index 0000000..ead25ba --- /dev/null +++ b/src/main/java/com/empayre/liminator/converter/CurrentLimitValuesToLimitResponseConverter.java @@ -0,0 +1,30 @@ +package com.empayre.liminator.converter; + +import com.empayre.liminator.model.LimitValue; +import dev.vality.liminator.LimitResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Component +public class CurrentLimitValuesToLimitResponseConverter implements Converter, List> { + + @Override + public List convert(List values) { + if (values == null) { + log.info("Received LimitValues array is empty"); + return new ArrayList<>(); + } + return values.stream() + .map(limitValue -> new LimitResponse( + limitValue.getLimitName(), + limitValue.getCommitValue(), + limitValue.getHoldValue()) + ) + .toList(); + } +} diff --git a/src/main/java/com/empayre/liminator/converter/OperationConverter.java b/src/main/java/com/empayre/liminator/converter/OperationConverter.java new file mode 100644 index 0000000..73e66f5 --- /dev/null +++ b/src/main/java/com/empayre/liminator/converter/OperationConverter.java @@ -0,0 +1,9 @@ +package com.empayre.liminator.converter; + +import com.empayre.liminator.domain.tables.pojos.Operation; +import dev.vality.liminator.LimitRequest; + +public interface OperationConverter { + + Operation convert(LimitRequest request, Long limitId); +} diff --git a/src/main/java/com/empayre/liminator/converter/impl/OperationConverterImpl.java b/src/main/java/com/empayre/liminator/converter/impl/OperationConverterImpl.java new file mode 100644 index 0000000..ef70c72 --- /dev/null +++ b/src/main/java/com/empayre/liminator/converter/impl/OperationConverterImpl.java @@ -0,0 +1,24 @@ +package com.empayre.liminator.converter.impl; + +import com.empayre.liminator.converter.OperationConverter; +import com.empayre.liminator.domain.enums.OperationState; +import com.empayre.liminator.domain.tables.pojos.Operation; +import dev.vality.liminator.LimitRequest; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class OperationConverterImpl implements OperationConverter { + + @Override + public Operation convert(LimitRequest request, Long limitId) { + Operation operation = new Operation(); + operation.setLimitId(limitId); + operation.setOperationId(request.getOperationId()); + operation.setAmount(request.getValue()); + operation.setCreatedAt(LocalDateTime.now()); + operation.setState(OperationState.HOLD); + return operation; + } +} diff --git a/src/main/java/com/empayre/liminator/dao/AbstractDao.java b/src/main/java/com/empayre/liminator/dao/AbstractDao.java new file mode 100644 index 0000000..bfe5382 --- /dev/null +++ b/src/main/java/com/empayre/liminator/dao/AbstractDao.java @@ -0,0 +1,23 @@ +package com.empayre.liminator.dao; + +import lombok.Getter; +import org.jooq.Configuration; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DSL; +import org.jooq.impl.DefaultConfiguration; + +import javax.sql.DataSource; + +@Getter +public abstract class AbstractDao { + + private final DSLContext dslContext; + + public AbstractDao(DataSource dataSource) { + Configuration configuration = new DefaultConfiguration(); + configuration.set(dataSource); + configuration.set(SQLDialect.POSTGRES); + this.dslContext = DSL.using(configuration); + } +} diff --git a/src/main/java/com/empayre/liminator/dao/CommonDao.java b/src/main/java/com/empayre/liminator/dao/CommonDao.java new file mode 100644 index 0000000..be18e92 --- /dev/null +++ b/src/main/java/com/empayre/liminator/dao/CommonDao.java @@ -0,0 +1,8 @@ +package com.empayre.liminator.dao; + +import com.empayre.liminator.exception.DaoException; + +public interface CommonDao { + + Long save(T domainObject) throws DaoException; +} diff --git a/src/main/java/com/empayre/liminator/dao/LimitContextDao.java b/src/main/java/com/empayre/liminator/dao/LimitContextDao.java new file mode 100644 index 0000000..247e717 --- /dev/null +++ b/src/main/java/com/empayre/liminator/dao/LimitContextDao.java @@ -0,0 +1,8 @@ +package com.empayre.liminator.dao; + +import com.empayre.liminator.domain.tables.pojos.LimitContext; + +public interface LimitContextDao extends CommonDao { + + LimitContext getLimitContext(Long limitId); +} diff --git a/src/main/java/com/empayre/liminator/dao/LimitDataDao.java b/src/main/java/com/empayre/liminator/dao/LimitDataDao.java new file mode 100644 index 0000000..924ef65 --- /dev/null +++ b/src/main/java/com/empayre/liminator/dao/LimitDataDao.java @@ -0,0 +1,13 @@ +package com.empayre.liminator.dao; + +import com.empayre.liminator.domain.tables.pojos.LimitData; + +import java.util.Collection; +import java.util.List; + +public interface LimitDataDao extends CommonDao { + + LimitData get(String limitName); + + List get(Collection limitNames); +} diff --git a/src/main/java/com/empayre/liminator/dao/OperationDao.java b/src/main/java/com/empayre/liminator/dao/OperationDao.java new file mode 100644 index 0000000..c614033 --- /dev/null +++ b/src/main/java/com/empayre/liminator/dao/OperationDao.java @@ -0,0 +1,21 @@ +package com.empayre.liminator.dao; + +import com.empayre.liminator.domain.tables.pojos.Operation; +import com.empayre.liminator.model.LimitValue; + +import java.util.List; + +public interface OperationDao extends CommonDao { + + void saveBatch(List operations); + + Operation get(Long id); + + List getCurrentLimitValue(List limitNames); + + List getCurrentLimitValue(List limitNames, String operationId); + + int commit(List operationIds); + + int rollback(List operationIds); +} diff --git a/src/main/java/com/empayre/liminator/dao/impl/LimitContextDaoImpl.java b/src/main/java/com/empayre/liminator/dao/impl/LimitContextDaoImpl.java new file mode 100644 index 0000000..6b73701 --- /dev/null +++ b/src/main/java/com/empayre/liminator/dao/impl/LimitContextDaoImpl.java @@ -0,0 +1,37 @@ +package com.empayre.liminator.dao.impl; + +import com.empayre.liminator.dao.AbstractDao; +import com.empayre.liminator.dao.LimitContextDao; +import com.empayre.liminator.domain.tables.pojos.LimitContext; +import com.empayre.liminator.exception.DaoException; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; + +import static com.empayre.liminator.domain.Tables.LIMIT_CONTEXT; + +@Component +public class LimitContextDaoImpl extends AbstractDao implements LimitContextDao { + + public LimitContextDaoImpl(DataSource dataSource) { + super(dataSource); + } + + @Override + public Long save(LimitContext limitContext) throws DaoException { + return getDslContext() + .insertInto(LIMIT_CONTEXT) + .set(getDslContext().newRecord(LIMIT_CONTEXT, limitContext)) + .returning(LIMIT_CONTEXT.ID) + .fetchOne() + .getId(); + } + + @Override + public LimitContext getLimitContext(Long limitId) { + return getDslContext() + .selectFrom(LIMIT_CONTEXT) + .where(LIMIT_CONTEXT.LIMIT_ID.eq(limitId)) + .fetchOneInto(LimitContext.class); + } +} diff --git a/src/main/java/com/empayre/liminator/dao/impl/LimitDataDaoImpl.java b/src/main/java/com/empayre/liminator/dao/impl/LimitDataDaoImpl.java new file mode 100644 index 0000000..7c8123d --- /dev/null +++ b/src/main/java/com/empayre/liminator/dao/impl/LimitDataDaoImpl.java @@ -0,0 +1,52 @@ +package com.empayre.liminator.dao.impl; + +import com.empayre.liminator.dao.AbstractDao; +import com.empayre.liminator.dao.LimitDataDao; +import com.empayre.liminator.domain.tables.pojos.LimitData; +import com.empayre.liminator.exception.DaoException; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import static com.empayre.liminator.domain.Tables.LIMIT_DATA; + +@Component +public class LimitDataDaoImpl extends AbstractDao implements LimitDataDao { + + public LimitDataDaoImpl(DataSource dataSource) { + super(dataSource); + } + + @Override + public Long save(LimitData limitData) throws DaoException { + return getDslContext() + .insertInto(LIMIT_DATA) + .set(getDslContext().newRecord(LIMIT_DATA, limitData)) + .onConflict(LIMIT_DATA.NAME) + .doUpdate() + .set(LIMIT_DATA.WTIME, LocalDateTime.now()) + .returning(LIMIT_DATA.ID) + .fetchOne() + .getId(); + } + + @Override + public LimitData get(String limitName) { + return getDslContext() + .selectFrom(LIMIT_DATA) + .where(LIMIT_DATA.NAME.equal(limitName)) + .fetchOneInto(LimitData.class); + } + + @Override + public List get(Collection limitNames) { + return getDslContext() + .selectFrom(LIMIT_DATA) + .where(LIMIT_DATA.NAME.in(limitNames)) + .fetchInto(LimitData.class); + } +} diff --git a/src/main/java/com/empayre/liminator/dao/impl/OperationDaoImpl.java b/src/main/java/com/empayre/liminator/dao/impl/OperationDaoImpl.java new file mode 100644 index 0000000..87ec9fe --- /dev/null +++ b/src/main/java/com/empayre/liminator/dao/impl/OperationDaoImpl.java @@ -0,0 +1,143 @@ +package com.empayre.liminator.dao.impl; + +import com.empayre.liminator.dao.AbstractDao; +import com.empayre.liminator.dao.OperationDao; +import com.empayre.liminator.domain.enums.OperationState; +import com.empayre.liminator.domain.tables.pojos.Operation; +import com.empayre.liminator.domain.tables.records.OperationRecord; +import com.empayre.liminator.exception.DaoException; +import com.empayre.liminator.model.LimitValue; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.util.List; +import java.util.stream.Collectors; + +import static com.empayre.liminator.domain.Tables.OPERATION; +import static org.jooq.impl.DSL.raw; +import static org.jooq.impl.DSL.val; + +@Component +public class OperationDaoImpl extends AbstractDao implements OperationDao { + + public OperationDaoImpl(DataSource dataSource) { + super(dataSource); + } + + private static final String DELIMITER = " ,"; + + @Override + public Long save(Operation operation) throws DaoException { + return getDslContext() + .insertInto(OPERATION) + .set(getDslContext().newRecord(OPERATION, operation)) + .returning(OPERATION.ID) + .fetchOne() + .getId(); + } + + @Override + public Operation get(Long id) { + return getDslContext() + .selectFrom(OPERATION) + .where(OPERATION.ID.eq(id)) + .fetchOneInto(Operation.class); + } + + @Override + public void saveBatch(List operations) { + List records = operations.stream() + .map(operation -> getDslContext().newRecord(OPERATION, operation)) + .toList(); + getDslContext().batchInsert(records).execute(); + } + + @Override + public int commit(List operationIds) { + return updateStateForHoldOperation(operationIds, OperationState.COMMIT); + } + + @Override + public int rollback(List operationIds) { + return updateStateForHoldOperation(operationIds, OperationState.ROLLBACK); + } + + private int updateStateForHoldOperation(List operationIds, OperationState state) { + return getDslContext() + .update(OPERATION) + .set(OPERATION.STATE, state) + .where(OPERATION.OPERATION_ID.in(operationIds)) + .and(OPERATION.STATE.eq(OperationState.HOLD)) + .execute(); + } + + @Override + public List getCurrentLimitValue(List limitNames) { + String sql = """ + with hold_data as ( + select ld.id, ld.name, coalesce(sum(ops.amount), 0) as hold_value + from lim.limit_data as ld + left join lim.operation as ops + on ops.limit_id = ld.id and ops.state = 'HOLD' + where ld.name in ({0}) + group by ld.id, ld.name + ), commit_data as ( + select ld.id, ld.name, coalesce(sum(ops.amount), 0) as commit_value + from lim.limit_data as ld + left join lim.operation as ops + on ops.limit_id = ld.id and ops.state = 'COMMIT' + where ld.name in ({0}) + group by ld.id, ld.name + ) + + select cd.name as limit_name, cd.commit_value, hd.hold_value + from commit_data as cd + join hold_data as hd on cd.id = hd.id; + """; + return getDslContext() + .resultQuery(sql, raw(arrayToString(limitNames))) + .fetchInto(LimitValue.class); + } + + @Override + public List getCurrentLimitValue(List limitNames, String operationId) { + String sql = """ + with operation_timestamp as ( + select created_at + from lim.operation + where operation_id = {0} + ), hold_data as ( + select ld.id, ld.name, coalesce(sum(ops.amount), 0) as hold_amount + from lim.limit_data as ld + left join lim.operation as ops + on ops.limit_id = ld.id + and ops.created_at <= (select created_at from operation_timestamp limit 1) + and ops.state = 'HOLD' + where ld.name in ({1}) + group by ld.id, ld.name + ), commit_data as ( + select ld.id, ld.name, coalesce(sum(ops.amount), 0) as commit_amount + from lim.limit_data as ld + left join lim.operation as ops + on ops.limit_id = ld.id + and ops.created_at <= (select created_at from operation_timestamp limit 1) + and ops.state = 'COMMIT' + where ld.name in ({1}) + group by ld.id, ld.name + ) + + select cd.name as limit_name, cd.commit_amount, hd.hold_amount + from commit_data as cd + join hold_data as hd on cd.id = hd.id; + """; + return getDslContext() + .resultQuery(sql, val(operationId), raw(arrayToString(limitNames))) + .fetchInto(LimitValue.class); + } + + private static String arrayToString(List strings) { + return strings.stream() + .map(limit -> "'%s'".formatted(limit)) + .collect(Collectors.joining(DELIMITER)); + } +} diff --git a/src/main/java/com/empayre/liminator/exception/BusinessException.java b/src/main/java/com/empayre/liminator/exception/BusinessException.java new file mode 100644 index 0000000..effc16e --- /dev/null +++ b/src/main/java/com/empayre/liminator/exception/BusinessException.java @@ -0,0 +1,7 @@ +package com.empayre.liminator.exception; + +public class BusinessException extends RuntimeException { + public BusinessException(String message) { + super(message); + } +} diff --git a/src/main/java/com/empayre/liminator/exception/DaoException.java b/src/main/java/com/empayre/liminator/exception/DaoException.java new file mode 100644 index 0000000..4470426 --- /dev/null +++ b/src/main/java/com/empayre/liminator/exception/DaoException.java @@ -0,0 +1,23 @@ +package com.empayre.liminator.exception; + +public class DaoException extends RuntimeException { + public DaoException() { + super(); + } + + public DaoException(String message) { + super(message); + } + + public DaoException(String message, Throwable cause) { + super(message, cause); + } + + public DaoException(Throwable cause) { + super(cause); + } + + protected DaoException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/empayre/liminator/exception/NotFoundException.java b/src/main/java/com/empayre/liminator/exception/NotFoundException.java new file mode 100644 index 0000000..80dc1c1 --- /dev/null +++ b/src/main/java/com/empayre/liminator/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package com.empayre.liminator.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/empayre/liminator/exception/SerializationException.java b/src/main/java/com/empayre/liminator/exception/SerializationException.java new file mode 100644 index 0000000..05a3521 --- /dev/null +++ b/src/main/java/com/empayre/liminator/exception/SerializationException.java @@ -0,0 +1,7 @@ +package com.empayre.liminator.exception; + +public class SerializationException extends RuntimeException { + public SerializationException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/empayre/liminator/handler/CommitLimitAmountHandler.java b/src/main/java/com/empayre/liminator/handler/CommitLimitAmountHandler.java new file mode 100644 index 0000000..1f66d26 --- /dev/null +++ b/src/main/java/com/empayre/liminator/handler/CommitLimitAmountHandler.java @@ -0,0 +1,44 @@ +package com.empayre.liminator.handler; + +import com.empayre.liminator.dao.OperationDao; +import com.empayre.liminator.service.LimitsGettingService; +import dev.vality.liminator.LimitRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CommitLimitAmountHandler implements Handler, Boolean> { + + private final OperationDao operationDao; + private final LimitsGettingService limitsGettingService; + + private static final String LOG_PREFIX = "COMMIT"; + + @Override + @Transactional + public Boolean handle(List requestList) throws TException { + if (CollectionUtils.isEmpty(requestList)) { + log.warn("[{}] Received LimitRequest list is empty", LOG_PREFIX); + return true; + } + limitsGettingService.get(requestList, LOG_PREFIX); + + List operationsIds = requestList.stream() + .map(request -> request.getOperationId()) + .toList(); + int updatedRowsCount = operationDao.commit(operationsIds); + if (updatedRowsCount != operationsIds.size()) { + log.warn("[{}] Count of updated rows ({}) is not equal to the count of source commit operations ({})", + LOG_PREFIX, updatedRowsCount, operationsIds.size()); + } + return true; + } +} diff --git a/src/main/java/com/empayre/liminator/handler/CreateLimitHandler.java b/src/main/java/com/empayre/liminator/handler/CreateLimitHandler.java new file mode 100644 index 0000000..71d8bcb --- /dev/null +++ b/src/main/java/com/empayre/liminator/handler/CreateLimitHandler.java @@ -0,0 +1,68 @@ +package com.empayre.liminator.handler; + +import com.empayre.liminator.dao.LimitContextDao; +import com.empayre.liminator.dao.LimitDataDao; +import com.empayre.liminator.domain.tables.pojos.LimitContext; +import com.empayre.liminator.domain.tables.pojos.LimitData; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.liminator.CreateLimitRequest; +import dev.vality.liminator.LimitResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CreateLimitHandler implements Handler { + + private final ObjectMapper mapper; + private final Converter createLimitRequestToLimitDataConverter; + private final LimitDataDao limitDataDao; + private final LimitContextDao limitContextDao; + + private static final String LOG_PREFIX = "CREATE"; + private static final String EMPTY_JSON = "{}"; + + @Transactional + @Override + public LimitResponse handle(CreateLimitRequest request) throws TException { + String limitName = request.getLimitName(); + LimitData existedLimitData = limitDataDao.get(limitName); + if (existedLimitData != null) { + log.info("[{}] Limit {} already exists", LOG_PREFIX, limitName); + return new LimitResponse(limitName, 0, 0); + } + + LimitData limitData = createLimitRequestToLimitDataConverter.convert(request); + Long limitId = limitDataDao.save(limitData); + if (request.context != null) { + limitContextDao.save(convertToLimitContext(limitId, request.context)); + } + return new LimitResponse(limitName, 0, 0); + } + + private LimitContext convertToLimitContext(Long limitId, Map contextMap) { + LimitContext context = new LimitContext(); + context.setLimitId(limitId); + context.setContext(getContextString(contextMap)); + context.setWtime(LocalDateTime.now()); + return context; + } + + private String getContextString(Map contextMap) { + try { + return mapper.writeValueAsString(contextMap); + } catch (JsonProcessingException e) { + log.error("[{}] ContextJSON processing exception", LOG_PREFIX, e); + return EMPTY_JSON; + } + } +} diff --git a/src/main/java/com/empayre/liminator/handler/GetLimitAmountHandler.java b/src/main/java/com/empayre/liminator/handler/GetLimitAmountHandler.java new file mode 100644 index 0000000..e674fff --- /dev/null +++ b/src/main/java/com/empayre/liminator/handler/GetLimitAmountHandler.java @@ -0,0 +1,28 @@ +package com.empayre.liminator.handler; + +import com.empayre.liminator.dao.OperationDao; +import com.empayre.liminator.model.LimitValue; +import dev.vality.liminator.LimitResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GetLimitAmountHandler implements Handler, List> { + + private final OperationDao operationDao; + private final Converter, List> currentLimitValuesToLimitResponseConverter; + + @Override + @Transactional + public List handle(List limitIdNames) throws TException { + return currentLimitValuesToLimitResponseConverter.convert(operationDao.getCurrentLimitValue(limitIdNames)); + } +} diff --git a/src/main/java/com/empayre/liminator/handler/Handler.java b/src/main/java/com/empayre/liminator/handler/Handler.java new file mode 100644 index 0000000..6f36d05 --- /dev/null +++ b/src/main/java/com/empayre/liminator/handler/Handler.java @@ -0,0 +1,8 @@ +package com.empayre.liminator.handler; + +import org.apache.thrift.TException; + +public interface Handler { + + R handle(T source) throws TException; +} diff --git a/src/main/java/com/empayre/liminator/handler/HoldLimitAmountHandler.java b/src/main/java/com/empayre/liminator/handler/HoldLimitAmountHandler.java new file mode 100644 index 0000000..7455db1 --- /dev/null +++ b/src/main/java/com/empayre/liminator/handler/HoldLimitAmountHandler.java @@ -0,0 +1,55 @@ +package com.empayre.liminator.handler; + +import com.empayre.liminator.converter.OperationConverter; +import com.empayre.liminator.dao.OperationDao; +import com.empayre.liminator.domain.tables.pojos.LimitData; +import com.empayre.liminator.domain.tables.pojos.Operation; +import com.empayre.liminator.model.LimitValue; +import com.empayre.liminator.service.LimitsGettingService; +import com.empayre.liminator.util.LimitDataUtils; +import dev.vality.liminator.LimitRequest; +import dev.vality.liminator.LimitResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class HoldLimitAmountHandler implements Handler, List> { + + private final OperationDao operationDao; + private final LimitsGettingService limitsGettingService; + private final Converter, List> currentLimitValuesToLimitResponseConverter; + private final OperationConverter operationConverter; + + private static final String LOG_PREFIX = "HOLD"; + + @Transactional + @Override + public List handle(List requestList) throws TException { + if (CollectionUtils.isEmpty(requestList)) { + return new ArrayList<>(); + } + List limitData = limitsGettingService.get(requestList, LOG_PREFIX); + Map limitNamesMap = LimitDataUtils.createLimitNamesMap(limitData); + List operations = convertToOperation(requestList, limitNamesMap); + operationDao.saveBatch(operations); + List limitNames = LimitDataUtils.getLimitNames(requestList); + return currentLimitValuesToLimitResponseConverter.convert(operationDao.getCurrentLimitValue(limitNames)); + } + + private List convertToOperation(List requestList, Map limitNamesMap) { + return requestList.stream() + .map(request -> operationConverter.convert(request, limitNamesMap.get(request.getLimitName()))) + .toList(); + } +} diff --git a/src/main/java/com/empayre/liminator/handler/RollbackLimitAmountHandler.java b/src/main/java/com/empayre/liminator/handler/RollbackLimitAmountHandler.java new file mode 100644 index 0000000..2d3f20e --- /dev/null +++ b/src/main/java/com/empayre/liminator/handler/RollbackLimitAmountHandler.java @@ -0,0 +1,39 @@ +package com.empayre.liminator.handler; + +import com.empayre.liminator.dao.OperationDao; +import com.empayre.liminator.service.LimitsGettingService; +import dev.vality.liminator.LimitRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RollbackLimitAmountHandler implements Handler, Boolean> { + + private final OperationDao operationDao; + private final LimitsGettingService limitsGettingService; + + private static final String LOG_PREFIX = "ROLLBACK"; + + @Override + @Transactional + public Boolean handle(List requestList) throws TException { + limitsGettingService.get(requestList, LOG_PREFIX); + + List operationsIds = requestList.stream() + .map(request -> request.getOperationId()) + .toList(); + int updatedRowsCount = operationDao.rollback(operationsIds); + if (updatedRowsCount != operationsIds.size()) { + log.warn("[{}] Count of updated rows ({}) is not equal to the count of source rollback operations ({})", + LOG_PREFIX, updatedRowsCount, operationsIds.size()); + } + return true; + } +} diff --git a/src/main/java/com/empayre/liminator/model/LimitValue.java b/src/main/java/com/empayre/liminator/model/LimitValue.java new file mode 100644 index 0000000..0a44982 --- /dev/null +++ b/src/main/java/com/empayre/liminator/model/LimitValue.java @@ -0,0 +1,13 @@ +package com.empayre.liminator.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class LimitValue { + + private String limitName; + private Long commitValue; + private Long holdValue; +} diff --git a/src/main/java/com/empayre/liminator/service/LiminatorService.java b/src/main/java/com/empayre/liminator/service/LiminatorService.java new file mode 100644 index 0000000..e9e85e3 --- /dev/null +++ b/src/main/java/com/empayre/liminator/service/LiminatorService.java @@ -0,0 +1,59 @@ +package com.empayre.liminator.service; + +import com.empayre.liminator.handler.Handler; +import dev.vality.liminator.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LiminatorService implements LiminatorServiceSrv.Iface { + + private final Handler createLimitHandler; + private final Handler, List> holdLimitAmountHandler; + private final Handler, Boolean> commitLimitAmountHandler; + private final Handler, Boolean> rollbackLimitAmountHandler; + private final Handler, List> getLimitAmountHandler; + + @Override + public LimitResponse create(CreateLimitRequest createLimitRequest) throws DuplicateLimitName, TException { + return createLimitHandler.handle(createLimitRequest); + } + + @Override + public List hold(List list) throws LimitNotFound, TException { + return holdLimitAmountHandler.handle(list); + } + + @Override + public boolean commit(List list) throws LimitNotFound, TException { + try { + commitLimitAmountHandler.handle(list); + return true; + } catch (Exception ex) { + log.error("Commit execution exception. Request list: {}", list, ex); + return false; + } + } + + @Override + public boolean rollback(List list) throws LimitNotFound, TException { + try { + rollbackLimitAmountHandler.handle(list); + return true; + } catch (Exception ex) { + log.error("Commit execution exception. Request list: {}", list, ex); + return false; + } + } + + @Override + public List get(List limitNames) throws LimitNotFound, TException { + return getLimitAmountHandler.handle(limitNames); + } +} diff --git a/src/main/java/com/empayre/liminator/service/LimitsGettingService.java b/src/main/java/com/empayre/liminator/service/LimitsGettingService.java new file mode 100644 index 0000000..85f87e4 --- /dev/null +++ b/src/main/java/com/empayre/liminator/service/LimitsGettingService.java @@ -0,0 +1,37 @@ +package com.empayre.liminator.service; + +import com.empayre.liminator.dao.LimitDataDao; +import com.empayre.liminator.domain.tables.pojos.LimitData; +import com.empayre.liminator.util.LimitDataUtils; +import dev.vality.liminator.LimitNotFound; +import dev.vality.liminator.LimitRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.thrift.TException; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class LimitsGettingService { + + private final LimitDataDao limitDataDao; + + public List get(List requestList, String source) throws TException { + List limitNames = LimitDataUtils.getLimitNames(requestList); + List limitData = limitDataDao.get(limitNames); + if (CollectionUtils.isEmpty(limitData)) { + log.error("[{}] Limits not found: {}", source, limitNames); + throw new LimitNotFound(); + } + if (limitData.size() != limitNames.size()) { + log.error("[{}] Received limit ({}) size is not equal to expected ({}). " + + "Probably one of limits doesn't exist", source, limitData.size(), limitNames.size()); + throw new LimitNotFound(); + } + return limitData; + } +} diff --git a/src/main/java/com/empayre/liminator/servlet/LiminatorServiceServlet.java b/src/main/java/com/empayre/liminator/servlet/LiminatorServiceServlet.java new file mode 100644 index 0000000..3f2f926 --- /dev/null +++ b/src/main/java/com/empayre/liminator/servlet/LiminatorServiceServlet.java @@ -0,0 +1,30 @@ +package com.empayre.liminator.servlet; + +import dev.vality.liminator.LiminatorServiceSrv; +import dev.vality.woody.thrift.impl.http.THServiceBuilder; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebServlet; +import java.io.IOException; + +@WebServlet("/liminator/v1") +public class LiminatorServiceServlet extends GenericServlet { + + private Servlet thriftServlet; + + @Autowired + private LiminatorServiceSrv.Iface requestHandler; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + thriftServlet = new THServiceBuilder() + .build(LiminatorServiceSrv.Iface.class, requestHandler); + } + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + thriftServlet.service(req, res); + } +} diff --git a/src/main/java/com/empayre/liminator/util/LimitDataUtils.java b/src/main/java/com/empayre/liminator/util/LimitDataUtils.java new file mode 100644 index 0000000..465a6d9 --- /dev/null +++ b/src/main/java/com/empayre/liminator/util/LimitDataUtils.java @@ -0,0 +1,29 @@ +package com.empayre.liminator.util; + +import com.empayre.liminator.domain.tables.pojos.LimitData; +import dev.vality.liminator.LimitRequest; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class LimitDataUtils { + + public static List getLimitNames(List requestList) { + return requestList.stream() + .map(LimitRequest::getLimitName) + .distinct() + .toList(); + } + + public static Map createLimitNamesMap(List limitData) { + Map map = new HashMap<>(); + for (LimitData data : limitData) { + map.put(data.getName(), data.getId()); + } + return map; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..b7ccee4 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,97 @@ +server: + port: "${server.port}" + +spring: + application: + name: "${project.name}" + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: org.postgresql.Driver + url: "${db.url}" + username: '${db.user}' + password: "${db.password}" + hikari: + idle-timeout: 30000 + maximum-pool-size: 10 + data-source-properties: + reWriteBatchedInserts: true + flyway: + schemas: lim + jooq: + sql-dialect: Postgres + kafka: + bootstrap-servers: localhost:9092 + client-id: dominant_warehouse + consumer: + enable-auto-commit: false + auto-offset-reset: earliest + max-poll-records: 20 + properties: + max.poll.interval.ms: 30000 + session.timeout.ms: 30000 + +service: + default: + limit: 20 + +management: + server: + port: "${management.port}" + metrics: + export: + prometheus: + enabled: false + tags: + application: "${project.name}" + endpoint: + health: + probes: + enabled: true + show-details: always + metrics: + enabled: true + prometheus: + enabled: true + endpoints: + web: + exposure: + include: health,info,prometheus + +kafka: + consumer: + group-id: "Liminator-Listener" + party-management-concurrency: 7 + wallet-concurrency: 7 + identity-concurrency: 7 + topics: + party-management: + id: mg-events-party + enabled: false + consumer.group-id: "LiminatorListenerPartyManagement" + identity: + id: mg-events-ff-identity + enabled: false + wallet: + id: mg-events-ff-wallet + enabled: false + +dmt: + url: http://dominant:8022/v1/domain/repository + networkTimeout: 5000 + polling: + delay: 100 + maxQuerySize: 10 + enabled: false + +cache: + party-shop: + size: 10000 + expire: + after: + sec: 600 + +testcontainers: + postgresql: + tag: '11.4' + kafka: + tag: '6.2.0' diff --git a/src/main/resources/checkstyle/checkstyle-suppressions.xml b/src/main/resources/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 0000000..329c0c0 --- /dev/null +++ b/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/migration/V1_1__add_base_tables.sql b/src/main/resources/db/migration/V1_1__add_base_tables.sql new file mode 100644 index 0000000..35c1a43 --- /dev/null +++ b/src/main/resources/db/migration/V1_1__add_base_tables.sql @@ -0,0 +1,44 @@ +CREATE SCHEMA IF NOT EXISTS lim; + + +CREATE TABLE lim.limit_data +( + id bigserial NOT NULL, + name character varying NOT NULL, + created_at date NOT NULL, + wtime timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, + CONSTRAINT limit_data_pkey PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX limit_data_unq_idx ON lim.limit_data USING btree (name); + + + +CREATE TABLE lim.limit_context +( + id bigserial NOT NULL, + limit_id bigint NOT NULL, + context character varying, + wtime timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, + CONSTRAINT limit_context_pkey PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX limit_context_unq_idx ON lim.limit_context USING btree (limit_id); + + + +CREATE TYPE lim.operation_state AS ENUM ('HOLD', 'COMMIT', 'ROLLBACK'); + +CREATE TABLE lim.operation +( + id bigserial NOT NULL, + limit_id bigint NOT NULL, + operation_id character varying NOT NULL, + state lim.operation_state NOT NULL, + amount bigint NOT NULL, + created_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, + CONSTRAINT operation_pkey PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX operation_unq_idx ON lim.operation USING btree (limit_id, operation_id); +CREATE INDEX operation_idx ON lim.operation USING btree (limit_id, state, created_at, operation_id); diff --git a/src/test/java/com/empayre/liminator/config/PostgresqlJooqSpringBootITest.java b/src/test/java/com/empayre/liminator/config/PostgresqlJooqSpringBootITest.java new file mode 100644 index 0000000..084f01b --- /dev/null +++ b/src/test/java/com/empayre/liminator/config/PostgresqlJooqSpringBootITest.java @@ -0,0 +1,16 @@ +package com.empayre.liminator.config; + +import dev.vality.testcontainers.annotations.postgresql.PostgresqlTestcontainerSingleton; +import org.springframework.boot.test.autoconfigure.jooq.JooqTest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@PostgresqlTestcontainerSingleton +@JooqTest +public @interface PostgresqlJooqSpringBootITest { +} diff --git a/src/test/java/com/empayre/liminator/config/PostgresqlSpringBootITest.java b/src/test/java/com/empayre/liminator/config/PostgresqlSpringBootITest.java new file mode 100644 index 0000000..c6eddd9 --- /dev/null +++ b/src/test/java/com/empayre/liminator/config/PostgresqlSpringBootITest.java @@ -0,0 +1,16 @@ +package com.empayre.liminator.config; + +import dev.vality.testcontainers.annotations.DefaultSpringBootTest; +import dev.vality.testcontainers.annotations.postgresql.PostgresqlTestcontainerSingleton; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@PostgresqlTestcontainerSingleton +@DefaultSpringBootTest +public @interface PostgresqlSpringBootITest { +} diff --git a/src/test/java/com/empayre/liminator/dao/DaoTests.java b/src/test/java/com/empayre/liminator/dao/DaoTests.java new file mode 100644 index 0000000..c06f9d6 --- /dev/null +++ b/src/test/java/com/empayre/liminator/dao/DaoTests.java @@ -0,0 +1,143 @@ +package com.empayre.liminator.dao; + +import com.empayre.liminator.config.PostgresqlSpringBootITest; +import com.empayre.liminator.domain.enums.OperationState; +import com.empayre.liminator.domain.tables.pojos.LimitContext; +import com.empayre.liminator.domain.tables.pojos.LimitData; +import com.empayre.liminator.domain.tables.pojos.Operation; +import com.empayre.liminator.model.LimitValue; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@PostgresqlSpringBootITest +public class DaoTests { + + @Autowired + private LimitDataDao limitDataDao; + + @Autowired + private LimitContextDao limitContextDao; + + @Autowired + private OperationDao operationDao; + + @Test + public void limitDataDaoTest() { + List limitNames = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + String limitName = "Limit-" + i; + limitDataDao.save(new LimitData(null, limitName, LocalDate.now(), LocalDateTime.now())); + limitNames.add(limitName); + } + + List limitDataList = limitDataDao.get(limitNames); + assertEquals(limitNames.size(), limitDataList.size()); + } + + @Test + public void limitContextDaoTest() { + LimitContext limitContext = new LimitContext(); + long limitId = 123L; + limitContext.setLimitId(limitId); + limitContext.setContext("{\"provider\":\"test\"}"); + limitContextDao.save(limitContext); + LimitContext result = limitContextDao.getLimitContext(limitId); + assertNotNull(result); + } + + @Test + public void operationDaoTest() { + List limitIdsList = new ArrayList<>(); + List limitNamesList = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + String limitName = "Limit-odc-1-" + i; + Long limitId = limitDataDao.save(new LimitData(null, limitName, LocalDate.now(), LocalDateTime.now())); + limitIdsList.add(limitId); + limitNamesList.add(limitName); + } + List operations = new ArrayList<>(); + for (Long limitId : limitIdsList) { + for (int i = 0; i < 5; i++) { + operations.add(createOperation(limitId, "Operation-odc-1-%s-%s".formatted(limitId, i))); + } + } + operationDao.saveBatch(operations); + + List currentLimitValue = operationDao.getCurrentLimitValue(limitNamesList); + assertEquals(10, currentLimitValue.size()); + currentLimitValue.forEach(value -> assertEquals(0, value.getCommitValue())); + currentLimitValue.forEach(value -> assertNotEquals(0, value.getHoldValue())); + + List commitOperations = operations.subList(0, 20).stream() + .map(Operation::getOperationId) + .toList(); + operationDao.commit(commitOperations); + List rollbackOperations = operations.subList(20, 35).stream() + .map(Operation::getOperationId) + .toList(); + operationDao.rollback(rollbackOperations); + + List limitValuesAfterChanges = operationDao.getCurrentLimitValue(limitNamesList); + List limitValuesWithCommitData = limitValuesAfterChanges.stream() + .filter(value -> value.getCommitValue() > 0 && value.getHoldValue() == 0) + .toList(); + assertEquals(4, limitValuesWithCommitData.size()); + + List limitValuesWithHoldData = limitValuesAfterChanges.stream() + .filter(value -> value.getCommitValue() == 0 && value.getHoldValue() > 0) + .toList(); + assertEquals(3, limitValuesWithHoldData.size()); + + List limitValuesWithoutData = limitValuesAfterChanges.stream() + .filter(value -> value.getCommitValue() == 0 && value.getHoldValue() == 0) + .toList(); + assertEquals(3, limitValuesWithoutData.size()); + } + + @Test + public void operationDaoCurrentLimitWithOperationIdTest() { + String limitName = "Limit-odc-2"; + Long limitId = limitDataDao.save(new LimitData(null, limitName, LocalDate.now(), LocalDateTime.now())); + List operations = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Operation operation = createOperation( + limitId, + "Operation-odc-2-%s-%s".formatted(limitId, i), + LocalDateTime.now().minusMinutes(11L - i)); + operationDao.save(operation); + operations.add(operation); + } + + List valuesForFifthOperation = + operationDao.getCurrentLimitValue(List.of(limitName), operations.get(2).getOperationId()); + LimitValue limitValue = valuesForFifthOperation.get(0); + assertEquals(300, limitValue.getHoldValue()); + + valuesForFifthOperation = + operationDao.getCurrentLimitValue(List.of(limitName), operations.get(5).getOperationId()); + assertEquals(600, valuesForFifthOperation.get(0).getHoldValue()); + } + + private Operation createOperation(Long limitId, String operationId) { + return createOperation(limitId, operationId, LocalDateTime.now()); + } + + private Operation createOperation(Long limitId, String operationId, LocalDateTime createdAt) { + Operation operation = new Operation(); + operation.setLimitId(limitId); + operation.setOperationId(operationId); + operation.setState(OperationState.HOLD); + operation.setAmount(100L); + operation.setCreatedAt(createdAt); + return operation; + } +} diff --git a/src/test/java/com/empayre/liminator/service/LiminatorServiceTest.java b/src/test/java/com/empayre/liminator/service/LiminatorServiceTest.java new file mode 100644 index 0000000..f606e42 --- /dev/null +++ b/src/test/java/com/empayre/liminator/service/LiminatorServiceTest.java @@ -0,0 +1,97 @@ +package com.empayre.liminator.service; + +import com.empayre.liminator.config.PostgresqlSpringBootITest; +import dev.vality.liminator.CreateLimitRequest; +import dev.vality.liminator.LimitRequest; +import dev.vality.liminator.LimitResponse; +import org.apache.thrift.TException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@PostgresqlSpringBootITest +public class LiminatorServiceTest { + + @Autowired + private LiminatorService liminatorService; + + @Test + void createLimitTest() throws TException { + String limitName = "TestLimitCreate"; + CreateLimitRequest request = new CreateLimitRequest() + .setLimitName(limitName); + + LimitResponse response = liminatorService.create(request); + assertEquals(limitName, response.getLimitName()); + assertEquals(0, response.getHoldValue()); + assertEquals(0, response.getCommitValue()); + } + + @Test + void holdValueTest() throws TException { + String limitName = "TestLimitHold"; + CreateLimitRequest createRequest = new CreateLimitRequest() + .setLimitName(limitName); + liminatorService.create(createRequest); + + String operationId = "OpHold"; + LimitRequest holdRequest = new LimitRequest() + .setLimitName(limitName) + .setOperationId(operationId) + .setValue(500L); + List holdResponse = liminatorService.hold(List.of(holdRequest)); + assertEquals(1, holdResponse.size()); + LimitResponse response = holdResponse.get(0); + assertEquals(500, response.getHoldValue()); + assertEquals(0, response.getCommitValue()); + assertEquals(limitName, response.getLimitName()); + } + + @Test + void commitValueTest() throws TException { + String limitName = "TestLimitCommit"; + CreateLimitRequest createRequest = new CreateLimitRequest() + .setLimitName(limitName); + liminatorService.create(createRequest); + + String operationId = "OpComit"; + LimitRequest holdRequest = new LimitRequest() + .setLimitName(limitName) + .setOperationId(operationId) + .setValue(500L); + liminatorService.hold(List.of(holdRequest)); + assertTrue(liminatorService.commit(List.of(holdRequest))); + + List limitResponses = liminatorService.get(List.of(limitName)); + assertEquals(1, limitResponses.size()); + assertEquals(0, limitResponses.get(0).getHoldValue()); + assertEquals(500, limitResponses.get(0).getCommitValue()); + assertEquals(limitName, limitResponses.get(0).getLimitName()); + } + + @Test + void rollbackValueTest() throws TException { + String limitName = "TestLimitRollback"; + CreateLimitRequest createRequest = new CreateLimitRequest() + .setLimitName(limitName); + liminatorService.create(createRequest); + + String operationId = "Op-112"; + LimitRequest holdRequest = new LimitRequest() + .setLimitName(limitName) + .setOperationId(operationId) + .setValue(500L); + liminatorService.hold(List.of(holdRequest)); + assertTrue(liminatorService.rollback(List.of(holdRequest))); + + List limitResponses = liminatorService.get(List.of(limitName)); + assertEquals(1, limitResponses.size()); + assertEquals(0, limitResponses.get(0).getHoldValue()); + assertEquals(0, limitResponses.get(0).getCommitValue()); + assertEquals(limitName, limitResponses.get(0).getLimitName()); + } +}