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());
+ }
+}