diff --git a/account-query/README.md b/account-query/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7c8bd539abd083ec98cbae1526e1436d327c8ed9 --- /dev/null +++ b/account-query/README.md @@ -0,0 +1,6 @@ +# PA165 Balance Service + +<p>The Balance Service, allows to view transaction history, provides a dashboard for bank employees to monitor all customers bank transactions. The system also provides a statistical module for employees, which can report total and average (per account) transactions (deposits, withdrawals, outgoing and incoming payments) in a selected date range.</p> + + + diff --git a/account-query/diagram.png b/account-query/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..fb05707fb91b8e4d27d85cb443b10541edf1bb35 Binary files /dev/null and b/account-query/diagram.png differ diff --git a/account-query/pom.xml b/account-query/pom.xml index f7ef897cf77074da1db38d9bf40e84cd0e818542..77e9c70e69d3f7b410d7dcb46d92fafee15c9fd1 100644 --- a/account-query/pom.xml +++ b/account-query/pom.xml @@ -4,6 +4,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>3.2.4</version> + <relativePath/> + </parent> + <groupId>cz.muni.pa165.banking</groupId> <artifactId>account-query</artifactId> <version>1.0-SNAPSHOT</version> @@ -12,6 +19,120 @@ <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + + <spring.version>3.2.4</spring.version> + <org.mapstruct.version>1.5.5.Final</org.mapstruct.version> </properties> + <build> + <finalName>server_generated</finalName> + + <plugins> + <plugin> + <groupId>org.openapitools</groupId> + <artifactId>openapi-generator-maven-plugin</artifactId> + <version>7.4.0</version> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> + <generatorName>spring</generatorName> + <apiPackage>cz.muni.pa165.banking.account.query</apiPackage> + <modelPackage>cz.muni.pa165.banking.account.query.dto</modelPackage> + <library>spring-boot</library> + <configOptions> + <useTags>true</useTags> + <interfaceOnly>true</interfaceOnly> + <skipDefaultInterface>true</skipDefaultInterface> + <openApiNullable>false</openApiNullable> + <documentationProvider>none</documentationProvider> + <useSpringBoot3>true</useSpringBoot3> + </configOptions> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-maven-plugin</artifactId> + <version>1.4</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>${maven-compiler-plugin.version}</version> + <configuration> + <annotationProcessorPaths> + <path> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct-processor</artifactId> + <version>${org.mapstruct.version}</version> + </path> + </annotationProcessorPaths> + <showWarnings>true</showWarnings> + <compilerArgs> + <arg>-Amapstruct.unmappedTargetPolicy=ERROR</arg> + <arg>-Amapstruct.unmappedSourcePolicy=ERROR</arg> + <arg>-Amapstruct.verbose=true</arg> + </compilerArgs> + </configuration> + </plugin> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <executable>true</executable> + </configuration> + </plugin> + <!-- run integration tests in "mvn verify" phase --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-quartz</artifactId> + </dependency> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct</artifactId> + <version>${org.mapstruct.version}</version> + </dependency> + + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <version>2.3.0</version> + </dependency> + <dependency> + <groupId>org.openapitools</groupId> + <artifactId>jackson-databind-nullable</artifactId> + <version>0.2.6</version> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-beans</artifactId> + <version>6.1.5</version> + </dependency> + </dependencies> + </project> \ No newline at end of file diff --git a/account-query/src/main/java/cz/muni/pa165/banking/Main.java b/account-query/src/main/java/cz/muni/pa165/banking/Main.java index f1b42c431db95159a9954acf5512dca812a6186a..b64e4d22b453938005cbd4402862d932ec2c4167 100644 --- a/account-query/src/main/java/cz/muni/pa165/banking/Main.java +++ b/account-query/src/main/java/cz/muni/pa165/banking/Main.java @@ -1,7 +1,11 @@ package cz.muni.pa165.banking; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication public class Main { public static void main(String[] args) { - System.out.println("Hello world!"); + SpringApplication.run(Main.class, args); } } \ No newline at end of file diff --git a/account-query/src/main/java/cz/muni/pa165/banking/application/controller/BalanceController.java b/account-query/src/main/java/cz/muni/pa165/banking/application/controller/BalanceController.java new file mode 100644 index 0000000000000000000000000000000000000000..1541bee756261982f3ab0f66b318c43059d4162f --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/application/controller/BalanceController.java @@ -0,0 +1,52 @@ +package cz.muni.pa165.banking.application.controller; + +import cz.muni.pa165.banking.account.query.CustomerServiceApi; +import cz.muni.pa165.banking.account.query.SystemServiceApi; +import cz.muni.pa165.banking.account.query.dto.Transaction; +import cz.muni.pa165.banking.account.query.dto.TransactionType; +import cz.muni.pa165.banking.application.facade.BalanceFacade; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * @author Martin Mojzis + */ +@RestController +public class BalanceController implements CustomerServiceApi, SystemServiceApi { + + private final BalanceFacade balanceFacade; + + public BalanceController(BalanceFacade balanceFacade) { + this.balanceFacade = balanceFacade; + } + + @Override + public ResponseEntity<BigDecimal> getBalance(String id) { + BigDecimal result = balanceFacade.getBalance(id); + return ResponseEntity.ok(result); + } + + @Override + public ResponseEntity<List<Transaction>> getTransactions(String id, LocalDate beginning, LocalDate end, BigDecimal minAmount, BigDecimal maxAmount, TransactionType type) { + List<Transaction> toReturn = balanceFacade.getTransactions(id, beginning, end, minAmount, maxAmount, type); + return ResponseEntity.ok(toReturn); + } + + @Override + public ResponseEntity<Void> addTransactionToBalance(String id, BigDecimal amount, UUID processId, TransactionType type) { + balanceFacade.addToBalance(id, processId, amount, type); + return ResponseEntity.ok().build(); + } + + @Override + public ResponseEntity<Void> createBalance(String id) { + balanceFacade.createNewBalance(id); + return new ResponseEntity<>(HttpStatus.CREATED); + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/application/controller/BalanceControllerEmployee.java b/account-query/src/main/java/cz/muni/pa165/banking/application/controller/BalanceControllerEmployee.java new file mode 100644 index 0000000000000000000000000000000000000000..8d4cf49b9e2debce13e46b7a5c7f83ea75c2e332 --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/application/controller/BalanceControllerEmployee.java @@ -0,0 +1,39 @@ +package cz.muni.pa165.banking.application.controller; + +import cz.muni.pa165.banking.account.query.EmployeeServiceApi; +import cz.muni.pa165.banking.account.query.dto.Transaction; +import cz.muni.pa165.banking.account.query.dto.TransactionType; +import cz.muni.pa165.banking.account.query.dto.TransactionsReport; +import cz.muni.pa165.banking.application.facade.BalanceFacade; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * @author Martin Mojzis + */ +@RestController +public class BalanceControllerEmployee implements EmployeeServiceApi { + + private final BalanceFacade balanceFacade; + + public BalanceControllerEmployee(BalanceFacade balanceFacade) { + this.balanceFacade = balanceFacade; + } + + @Override + public ResponseEntity<TransactionsReport> createReport(String id, LocalDate beginning, LocalDate end) { + TransactionsReport result = balanceFacade.getReport(id, beginning, end); + return ResponseEntity.ok(result); + } + + @Override + public ResponseEntity<List<Transaction>> getAllTransactions(LocalDate beginning, LocalDate end, + BigDecimal minAmount, BigDecimal maxAmount, TransactionType type) { + List<Transaction> result = balanceFacade.getAllTransactions(beginning, end, minAmount, maxAmount, type); + return ResponseEntity.ok(result); + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/application/exception/NotFoundAccountException.java b/account-query/src/main/java/cz/muni/pa165/banking/application/exception/NotFoundAccountException.java new file mode 100644 index 0000000000000000000000000000000000000000..f82452ba924c491cf3279094104c787bac666e4a --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/application/exception/NotFoundAccountException.java @@ -0,0 +1,11 @@ +package cz.muni.pa165.banking.application.exception; + +/** + * @author Martin Mojzis + */ +public class NotFoundAccountException extends RuntimeException{ + public NotFoundAccountException(String message) { + super(message); + } + +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/application/exception/RestApiExceptionHandler.java b/account-query/src/main/java/cz/muni/pa165/banking/application/exception/RestApiExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..a2b6023472a40b9feb804a3a79c2661054b437de --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/application/exception/RestApiExceptionHandler.java @@ -0,0 +1,19 @@ +package cz.muni.pa165.banking.application.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +/** + * @author Martin Mojzis + */ +@ControllerAdvice +public class RestApiExceptionHandler { + @ExceptionHandler(NotFoundAccountException.class) + public ResponseEntity<Object> handleNotFoundAccount(Exception e, WebRequest request){ + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/application/facade/BalanceFacade.java b/account-query/src/main/java/cz/muni/pa165/banking/application/facade/BalanceFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..b89d717c93695ce038e443d9074139c606191177 --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/application/facade/BalanceFacade.java @@ -0,0 +1,68 @@ +package cz.muni.pa165.banking.application.facade; + +import cz.muni.pa165.banking.account.query.dto.Transaction; +import cz.muni.pa165.banking.account.query.dto.TransactionType; +import cz.muni.pa165.banking.account.query.dto.TransactionsReport; +import cz.muni.pa165.banking.application.exception.NotFoundAccountException; +import cz.muni.pa165.banking.application.mapper.BalanceMapper; +import cz.muni.pa165.banking.domain.balance.service.BalanceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.UUID; + +/** + * @author Martin Mojzis + */ +@Service +public class BalanceFacade { + + private final BalanceService balanceService; + private final BalanceMapper balanceMapper; + + public BalanceFacade(BalanceService balanceService, BalanceMapper balanceMapper) { + this.balanceService = balanceService; + this.balanceMapper = balanceMapper; + } + + public void createNewBalance(String id) throws NotFoundAccountException { + balanceService.addNewBalance(id); + } + + public void addToBalance(String id, UUID processId, BigDecimal value, TransactionType type) { + balanceService.addToBalance(id, value, processId, balanceMapper.mapTypeOut(type)); + } + + public BigDecimal getBalance(String id) throws NotFoundAccountException { + return balanceService.getBalance(id); + } + + public List<Transaction> getTransactions(String id, LocalDate beginning, LocalDate end, BigDecimal minAmount, + BigDecimal maxAmount, TransactionType type) { + List<cz.muni.pa165.banking.domain.transaction.Transaction> toReturn; + toReturn = balanceService.getTransactions(id, OffsetDateTime.of(beginning, LocalTime.MIDNIGHT, ZoneOffset.UTC), OffsetDateTime.of(end, LocalTime.MIDNIGHT, ZoneOffset.UTC), + minAmount, maxAmount, balanceMapper.mapTypeOut(type)); + return toReturn.stream().map(balanceMapper::mapTransactionIn).toList(); + } + + public TransactionsReport getReport(String id, LocalDate beginning, LocalDate end) { + return balanceMapper.mapReportOut(balanceService.getReport(id, + OffsetDateTime.of(beginning, LocalTime.MIDNIGHT, ZoneOffset.UTC), + OffsetDateTime.of(end, LocalTime.MIDNIGHT, ZoneOffset.UTC))); + } + + public List<Transaction> getAllTransactions(LocalDate beginning, LocalDate end, BigDecimal minAmount, + BigDecimal maxAmount, TransactionType type) { + List<cz.muni.pa165.banking.domain.transaction.Transaction> toReturn = + balanceService.getAllTransactions(OffsetDateTime.of(beginning, LocalTime.MIDNIGHT, ZoneOffset.UTC), OffsetDateTime.of(end, LocalTime.MIDNIGHT, ZoneOffset.UTC), minAmount, maxAmount, + balanceMapper.mapTypeOut(type)); + return toReturn.stream().map(balanceMapper::mapTransactionIn).toList(); + } + +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/application/mapper/BalanceMapper.java b/account-query/src/main/java/cz/muni/pa165/banking/application/mapper/BalanceMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..be5214fe6dd63625c321ffa0b7b5d3451cb99f1f --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/application/mapper/BalanceMapper.java @@ -0,0 +1,76 @@ +package cz.muni.pa165.banking.application.mapper; + +import cz.muni.pa165.banking.account.query.dto.Transaction; +import cz.muni.pa165.banking.account.query.dto.TransactionStatistics; +import cz.muni.pa165.banking.account.query.dto.TransactionsReport; +import cz.muni.pa165.banking.domain.report.StatisticalReport; +import cz.muni.pa165.banking.domain.transaction.TransactionType; +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.NullValuePropertyMappingStrategy; + +import java.math.BigDecimal; +import java.time.ZoneOffset; +import java.util.Date; + +/** + * @author Martin Mojzis + */ +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, injectionStrategy = InjectionStrategy.CONSTRUCTOR, + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface BalanceMapper { + TransactionType mapTypeOut(cz.muni.pa165.banking.account.query.dto.TransactionType type); + cz.muni.pa165.banking.account.query.dto.TransactionType mapTypeIn(TransactionType type); + default java.util.Date mapDateIn(java.time.@jakarta.validation.Valid OffsetDateTime value) { + return new Date(value.toInstant().toEpochMilli()); + } + + default java.time.OffsetDateTime mapDate(java.util.Date date) { + return date.toInstant().atOffset(ZoneOffset.UTC); + } + + default cz.muni.pa165.banking.domain.transaction.Transaction mapTransactionOut(Transaction transaction) { + return new + cz.muni.pa165.banking.domain.transaction.Transaction(mapTypeOut(transaction.getTransactionType()), + transaction.getAmount(), transaction.getDate(), transaction.getProcessId()); + } + default Transaction mapTransactionIn(cz.muni.pa165.banking.domain.transaction.Transaction transaction){ + Transaction result = new Transaction(); + result.setAmount(transaction.getAmount()); + result.setDate(transaction.getDate()); + result.setProcessId(transaction.getProcessId()); + result.setTransactionType(mapTypeIn(transaction.getType())); + return result; + } + default TransactionStatistics mapStatisticsOut(cz.muni.pa165.banking.domain.report.TransactionStatistics statistics){ + TransactionStatistics result = new TransactionStatistics(); + result.setTransactionType(mapTypeIn(statistics.getType())); + result.setAmountOut(statistics.getAmountOut()); + result.setTimesOut(BigDecimal.valueOf(statistics.getTimesOut())); + result.setAmountIn(statistics.getAmountIn()); + result.setTimesIn(BigDecimal.valueOf(statistics.getTimesIn())); + if(statistics.getTimesOut() != 0) { + result.setAvgOut(statistics.getAmountOut().divide(new BigDecimal(statistics.getTimesOut()))); + } + else{ + result.setAvgOut(BigDecimal.ZERO); + } + if(statistics.getTimesIn() != 0) { + result.setAvgIn(statistics.getAmountIn().divide(new BigDecimal(statistics.getTimesIn()))); + } + else{ + result.setAvgIn(BigDecimal.ZERO); + } + return result; + } + default TransactionsReport mapReportOut(StatisticalReport report){ + TransactionsReport result = new TransactionsReport(); + result.setCreditAmount(mapStatisticsOut(report.getCreditAmount())); + result.setDepositAmount(mapStatisticsOut(report.getDepositAmount())); + result.setTotalAmount(mapStatisticsOut(report.getTotalAmount())); + result.setWithdrawalAmount(mapStatisticsOut(report.getWithdrawalAmount())); + result.setCrossAccountAmount(mapStatisticsOut(report.getCrossAccountAmount())); + return result; + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/application/repository/BalancesRepositoryImpl.java b/account-query/src/main/java/cz/muni/pa165/banking/application/repository/BalancesRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..23bbcdddd01f709dc9bfdfcd32b38d26efb33e3c --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/application/repository/BalancesRepositoryImpl.java @@ -0,0 +1,45 @@ +package cz.muni.pa165.banking.application.repository; + +import cz.muni.pa165.banking.domain.balance.Balance; +import cz.muni.pa165.banking.domain.balance.repository.BalancesRepository; +import org.springframework.stereotype.Repository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * @author Martin Mojzis + */ +@Repository +public class BalancesRepositoryImpl implements BalancesRepository { + + private final Map<String, Balance> mockData = new HashMap<>(); + + public BalancesRepositoryImpl() { + mockData.put("id1", new Balance("id1")); + mockData.put("id2", new Balance("id2")); + } + + //@Transactional + @Override + public Optional<Balance> findById(String id) { + if (mockData.containsKey(id)) { + return Optional.of(mockData.get(id)); + } + return Optional.empty(); + } + + @Override + public List<String> getAllIds() { + return mockData.keySet().stream().toList(); + } + + + //@Transactional + @Override + public void addBalance(String id) { + mockData.put(id, new Balance(id)); + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/application/service/BalanceServiceImpl.java b/account-query/src/main/java/cz/muni/pa165/banking/application/service/BalanceServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b8437551ccca8cbcc81fdd53ba4d765ed9966ef5 --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/application/service/BalanceServiceImpl.java @@ -0,0 +1,85 @@ +package cz.muni.pa165.banking.application.service; + +import cz.muni.pa165.banking.application.exception.NotFoundAccountException; +import cz.muni.pa165.banking.domain.balance.Balance; +import cz.muni.pa165.banking.domain.balance.repository.BalancesRepository; +import cz.muni.pa165.banking.domain.balance.service.BalanceService; +import cz.muni.pa165.banking.domain.report.StatisticalReport; +import cz.muni.pa165.banking.domain.transaction.Transaction; +import cz.muni.pa165.banking.domain.transaction.TransactionType; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * @author Martin Mojzis + */ +@Service +public class BalanceServiceImpl implements BalanceService { + + private final BalancesRepository balanceRepository; + + public BalanceServiceImpl(BalancesRepository balanceRepository) { + this.balanceRepository = balanceRepository; + } + + public Balance findById(String id) throws NotFoundAccountException { + return balanceRepository.findById(id) + .orElseThrow(() -> new NotFoundAccountException("Balance of person with id: " + id + " was not found.")); + } + + @Override + public void addNewBalance(String id) { + balanceRepository.addBalance(id); + } + + @Override + public BigDecimal getBalance(String id) throws NotFoundAccountException { + Balance balance = findById(id); + return balance.getAmount(); + } + + @Override + public List<Transaction> getTransactions(String id, OffsetDateTime from, OffsetDateTime to, BigDecimal minAmount, + BigDecimal maxAmount, TransactionType type) + throws NotFoundAccountException { + Balance balance = findById(id); + if (minAmount == null && maxAmount == null && type == null) + return balance.getData(from, to); + if (minAmount == null && maxAmount == null) + return balance.getData(from, to, type); + BigDecimal amountMax = Objects.requireNonNullElse(maxAmount, BigDecimal.valueOf(Integer.MAX_VALUE)); + BigDecimal amountMin = Objects.requireNonNullElse(minAmount, BigDecimal.valueOf(Integer.MIN_VALUE)); + if (type == null) + return balance.getData(from, to, amountMin, amountMax); + return balance.getData(from, to, amountMin, amountMax, type); + } + + @Override + public void addToBalance(String id, BigDecimal amount, UUID processID, TransactionType type) + throws NotFoundAccountException { + Balance balance = findById(id); + balance.addTransaction(amount, type, processID); + } + + @Override + public StatisticalReport getReport(String id, OffsetDateTime beginning, OffsetDateTime end) { + Balance balance = findById(id); + return balance.getReport(beginning, end); + } + + @Override + public List<Transaction> getAllTransactions(OffsetDateTime from, OffsetDateTime to, BigDecimal minAmount, + BigDecimal maxAmount, TransactionType transactionType) { + List<Transaction> result = new LinkedList<>(); + for (String id : balanceRepository.getAllIds()) { + result.addAll(getTransactions(id, from, to, minAmount, maxAmount, transactionType)); + } + return result; + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/Balance.java b/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/Balance.java new file mode 100644 index 0000000000000000000000000000000000000000..ed99c1f827a09fae852ec85ed06c31262125643a --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/Balance.java @@ -0,0 +1,89 @@ +package cz.muni.pa165.banking.domain.balance; + +import cz.muni.pa165.banking.domain.report.StatisticalReport; +import cz.muni.pa165.banking.domain.transaction.Transaction; +import cz.muni.pa165.banking.domain.transaction.TransactionType; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * @author Martin Mojzis + */ +public class Balance { + + private final String accountId; + + private BigDecimal amount; + + private final List<Transaction> transactionList; + + public Balance(String userId) { + this.amount = new BigDecimal(0); + this.transactionList = new ArrayList<>(); + this.accountId = userId; + } + + public void addTransaction(BigDecimal amount, TransactionType type, UUID processId) { + transactionList.add(new Transaction(type, amount, + OffsetDateTime.now(), processId)); + this.amount = this.amount.add(amount); + } + + public BigDecimal getAmount() { + return amount; + } + + public List<Transaction> getTransactions() { + return transactionList; + } + + public Transaction getTransaction(UUID pid) throws RuntimeException { + List<Transaction> result = transactionList.stream().filter(a -> Objects.equals(a.getProcessId(), pid)).toList(); + if (result.isEmpty()) { + throw new RuntimeException("list has no tranaction with this id"); + } + return result.get(0); + } + + public boolean transactionExists(UUID pid) { + List<Transaction> result = transactionList.stream().filter(a -> Objects.equals(a.getProcessId(), pid)).toList(); + return !result.isEmpty(); + } + + public StatisticalReport getReport(OffsetDateTime after, OffsetDateTime before) { + return new StatisticalReport(this.getData(after, before)); + } + + public List<Transaction> getData(OffsetDateTime after, OffsetDateTime before) { + return transactionList.stream().filter(a -> a.getDate().isAfter(after) && a.getDate().isBefore(before)).toList(); + } + + public List<Transaction> getData(OffsetDateTime after, OffsetDateTime before, BigDecimal amountMin, BigDecimal amountMax, TransactionType type) { + List<Transaction> result = this.getData(after, before); + return result.stream() + .filter(a -> a.getAmount().compareTo(amountMax) < 0 && a.getAmount().compareTo(amountMin) > 0 && a.getType() == type) + .toList(); + } + + public List<Transaction> getData(OffsetDateTime after, OffsetDateTime before, BigDecimal amountMin, BigDecimal amountMax) { + List<Transaction> result = this.getData(after, before); + return result.stream() + .filter(a -> a.getAmount().compareTo(amountMax) < 0 && a.getAmount().compareTo(amountMin) > 0) + .toList(); + } + public String getAccountId() { + return accountId; + } + + public List<Transaction> getData(OffsetDateTime from, OffsetDateTime to, TransactionType type) { + List<Transaction> result = this.getData(from, to); + return result.stream() + .filter(a -> a.getType() == type) + .toList(); + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/repository/BalancesRepository.java b/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/repository/BalancesRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..09a4b51cf7d1c82c0f437be0b70e829722250521 --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/repository/BalancesRepository.java @@ -0,0 +1,18 @@ +package cz.muni.pa165.banking.domain.balance.repository; + +import cz.muni.pa165.banking.domain.balance.Balance; + +import java.util.List; +import java.util.Optional; + +/** + * @author Martin Mojzis + */ +public interface BalancesRepository { + + Optional<Balance> findById(String id); + + void addBalance(String id); + + List<String> getAllIds(); +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/service/BalanceService.java b/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/service/BalanceService.java new file mode 100644 index 0000000000000000000000000000000000000000..c242a56b620bab2c0ce47ccd480578a02a2bf648 --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/domain/balance/service/BalanceService.java @@ -0,0 +1,31 @@ +package cz.muni.pa165.banking.domain.balance.service; + +import cz.muni.pa165.banking.application.exception.NotFoundAccountException; +import cz.muni.pa165.banking.domain.report.StatisticalReport; +import cz.muni.pa165.banking.domain.transaction.Transaction; +import cz.muni.pa165.banking.domain.transaction.TransactionType; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +/** + * @author Martin Mojzis + */ +public interface BalanceService { + + void addNewBalance(String id) throws NotFoundAccountException; + + BigDecimal getBalance(String id) throws NotFoundAccountException; + + List<Transaction> getTransactions(String id, OffsetDateTime from, OffsetDateTime to, BigDecimal minAmount, + BigDecimal maxAmount, TransactionType type) throws NotFoundAccountException; + + void addToBalance(String id, BigDecimal amount, UUID processID, TransactionType type) throws NotFoundAccountException; + + StatisticalReport getReport(String id, OffsetDateTime beginning, OffsetDateTime end) throws NotFoundAccountException; + + List<Transaction> getAllTransactions(OffsetDateTime from, OffsetDateTime from1, BigDecimal minAmount, + BigDecimal maxAmount, TransactionType transactionType); +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/domain/report/StatisticalReport.java b/account-query/src/main/java/cz/muni/pa165/banking/domain/report/StatisticalReport.java new file mode 100644 index 0000000000000000000000000000000000000000..db2583f8f82810795c30cc36a5499786ed81823f --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/domain/report/StatisticalReport.java @@ -0,0 +1,86 @@ +package cz.muni.pa165.banking.domain.report; + +import cz.muni.pa165.banking.domain.transaction.Transaction; +import cz.muni.pa165.banking.domain.transaction.TransactionType; + +import java.math.BigDecimal; +import java.util.List; + +/** + * @author Martin Mojzis + */ +public class StatisticalReport { + //report total and average (per account) transactions (deposits, withdrawals, outgoing and incoming payments) in a selected date range + + private final TransactionStatistics totalAmount = new TransactionStatistics(); + + private final TransactionStatistics depositAmount = new TransactionStatistics(TransactionType.DEPOSIT); + + private final TransactionStatistics withdrawalAmount = new TransactionStatistics(TransactionType.WITHDRAW); + + private final TransactionStatistics crossAccountAmount = new TransactionStatistics(TransactionType.CROSS_ACCOUNT_PAYMENT); + + private final TransactionStatistics creditAmount = new TransactionStatistics(TransactionType.CREDIT); + + private final TransactionStatistics refundAmount = new TransactionStatistics(TransactionType.REFUND); + + //maybe not needed + private BigDecimal amountMin = BigDecimal.valueOf(Double.MAX_VALUE); + + private BigDecimal amountMax = new BigDecimal(0); + + public StatisticalReport(List<Transaction> list) { + for (Transaction transaction : list) { + if (transaction.getAmount().abs().compareTo(amountMin) < 0) { + amountMin = transaction.getAmount().abs(); + } + if (transaction.getAmount().abs().compareTo(amountMax) > 0) { + amountMax = transaction.getAmount().abs(); + } + addToAmountStatistics(transaction); + } + } + + private void addToAmountStatistics(Transaction transaction) { + totalAmount.AddAmount(transaction.getAmount()); + switch (transaction.getType()){ + case CREDIT -> creditAmount.AddAmount(transaction.getAmount()); + case REFUND -> refundAmount.AddAmount(transaction.getAmount()); + case DEPOSIT -> depositAmount.AddAmount(transaction.getAmount()); + case WITHDRAW -> withdrawalAmount.AddAmount(transaction.getAmount()); + case CROSS_ACCOUNT_PAYMENT -> crossAccountAmount.AddAmount(transaction.getAmount()); + } + } + + public BigDecimal getAmountMax() { + return amountMax; + } + + public BigDecimal getAmountMin() { + return amountMin; + } + + public TransactionStatistics getCreditAmount() { + return creditAmount; + } + + public TransactionStatistics getCrossAccountAmount() { + return crossAccountAmount; + } + + public TransactionStatistics getWithdrawalAmount() { + return withdrawalAmount; + } + + public TransactionStatistics getDepositAmount() { + return depositAmount; + } + + public TransactionStatistics getTotalAmount() { + return totalAmount; + } + + public TransactionStatistics getRefundAmount() { + return refundAmount; + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/domain/report/TransactionStatistics.java b/account-query/src/main/java/cz/muni/pa165/banking/domain/report/TransactionStatistics.java new file mode 100644 index 0000000000000000000000000000000000000000..8514f8f12b91da3ff72c302c2c210e0d267e48a0 --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/domain/report/TransactionStatistics.java @@ -0,0 +1,58 @@ +package cz.muni.pa165.banking.domain.report; + +import cz.muni.pa165.banking.domain.transaction.TransactionType; + +import java.math.BigDecimal; + +/** + * @author Martin Mojzis + */ +public class TransactionStatistics { + + private TransactionType type = null; + + private BigDecimal amountIn = new BigDecimal(0); + + private BigDecimal amountOut = new BigDecimal(0); + + private Integer timesIn = 0; + + private Integer timesOut = 0; + + public TransactionStatistics(TransactionType type){ + this.type = type; + } + + public TransactionStatistics(){} + + public void AddAmount(BigDecimal amount){ + if(amount.compareTo(BigDecimal.ZERO) > 0){ + amountIn = amountIn.add(amount); + timesIn += 1; + } + else{ + amountOut = amountOut.add(amount); + timesOut += 1; + } + } + + public Integer getTimesOut() { + return timesOut; + } + + public Integer getTimesIn() { + return timesIn; + } + + public BigDecimal getAmountOut() { + return amountOut; + } + + public BigDecimal getAmountIn() { + return amountIn; + } + + public TransactionType getType() { + return type; + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/domain/transaction/Transaction.java b/account-query/src/main/java/cz/muni/pa165/banking/domain/transaction/Transaction.java new file mode 100644 index 0000000000000000000000000000000000000000..479ca7981a098d493db35f9277ef2000dec6219e --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/domain/transaction/Transaction.java @@ -0,0 +1,42 @@ +package cz.muni.pa165.banking.domain.transaction; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.UUID; + +/** + * @author Martin Mojzis + */ +public class Transaction { + + private final TransactionType type; + + private final BigDecimal amount; + + private final OffsetDateTime date; + + private final UUID processId; + + public Transaction(TransactionType type, BigDecimal amount, OffsetDateTime date, UUID processId) { + this.type = type; + this.amount = amount; + this.date = date; + this.processId = processId; + } + + public OffsetDateTime getDate() { + return date; + } + + public BigDecimal getAmount() { + return amount; + } + + public TransactionType getType() { + return type; + } + + public UUID getProcessId() { + return processId; + } +} diff --git a/account-query/src/main/java/cz/muni/pa165/banking/domain/transaction/TransactionType.java b/account-query/src/main/java/cz/muni/pa165/banking/domain/transaction/TransactionType.java new file mode 100644 index 0000000000000000000000000000000000000000..2a1fb15b87e63a153fb22f837940e0b2bf5b7e6d --- /dev/null +++ b/account-query/src/main/java/cz/muni/pa165/banking/domain/transaction/TransactionType.java @@ -0,0 +1,18 @@ +package cz.muni.pa165.banking.domain.transaction; + +/** + * @author Martin Mojzis + */ +public enum TransactionType { + + WITHDRAW, + + DEPOSIT, + + CREDIT, + + CROSS_ACCOUNT_PAYMENT, + + REFUND + +} diff --git a/account-query/src/main/resources/application.properties b/account-query/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..bafddced850ad5cb9c8b100b9daf64dae4e70202 --- /dev/null +++ b/account-query/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8081 \ No newline at end of file diff --git a/account-query/src/main/resources/openapi.yaml b/account-query/src/main/resources/openapi.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dd10e76d9e992ad34dd1d3c8caee7205cb627ffc --- /dev/null +++ b/account-query/src/main/resources/openapi.yaml @@ -0,0 +1,248 @@ +openapi: 3.0.1 +info: + title: Pa165 Project Banking Application Account Query + description: | + Hand-made OpenAPI document. + version: 1.0.0 +servers: + - url: "http://localhost:8080" +tags: + - name: CustomerService + description: Service to be used by customer of bank + - name: SystemService + description: Service to be used by other services of bank + - name: EmployeeService + description: Service to be used by employees of bank +components: + schemas: + TransactionType: + type: string + enum: [ WITHDRAW, DEPOSIT, CREDIT, CROSS_ACCOUNT_PAYMENT, REFUND ] + description: type of transaction + Transaction: + title: A transaction + description: A object representing one transaction + properties: + transactionType: + $ref: "#/components/schemas/TransactionType" + amount: + type: number + description: amount of money in transaction + example: 10 + date: + type: string + description: time when the transaction happened + format: date-time + processId: + type: string + format: uuid + description: id of transaction issued by transaction processor + + Balance: + title: A balance + description: Balance object keeping balance of ones account as well as all transactions that led to it. + required: + - id + properties: + id: + type: string + description: id of account with this balance + example: id1 + amount: + type: number + description: balance of account right now + example: 10 + transactions: + type: array + items: + $ref: '#/components/schemas/Transaction' + description: all transactions of this account in list + + TransactionStatistics: + title: statistics about one type of transaction + properties: + transactionType: + $ref: "#/components/schemas/TransactionType" + amountIn: + type: number + description: amount of money got by this type of transactions + example: 1002 + amountOut: + type: number + description: amount of money spent by this type of transactions + example: 1184 + timesIn: + type: number + description: number of times this type of transaction was used to get money + example: 12 + timesOut: + type: number + description: number of times this type of transaction was used to spend money + example: 13 + avgIn: + type: number + description: average amount of money regarding charging account + example: 120 + avgOut: + type: number + description: average amount of money regarding spending money + example: 14 + + TransactionsReport: + title: statistical report about one account + properties: + totalAmount: + $ref: '#/components/schemas/TransactionStatistics' + depositAmount: + $ref: '#/components/schemas/TransactionStatistics' + withdrawalAmount: + $ref: '#/components/schemas/TransactionStatistics' + crossAccountAmount: + $ref: '#/components/schemas/TransactionStatistics' + creditAmount: + $ref: '#/components/schemas/TransactionStatistics' + +paths: + /balance/new: + post: + tags: + - SystemService + summary: creates a balance + operationId: createBalance + parameters: + - { name: id, in: query, required: true, schema: { type: string }, description: "id of account" } + responses: + "201": + description: OK + + /balance/add: + post: + tags: + - SystemService + summary: adds transaction to existing balance + operationId: addTransactionToBalance + parameters: + - { name: id, in: query, required: true, schema: { type: string }, description: "id of account" } + - { name: amount, in: query, required: true, schema: { type: number }, description: "amount of money in transaction" } + - { name: processId, in: query, required: true, schema: { type: string, format: uuid }, description: "id of process which this transaction is part of" } + - { name: type, in: query, required: true, schema: { $ref: "#/components/schemas/TransactionType" }, description: "type of transaction" } + responses: + "200": + description: OK + "400": + description: NOK + content: + application/json: + schema: + type: string + description: reason of failure + + /balance/status: + get: + tags: + - CustomerService + - SystemService + summary: returns account balance status + operationId: getBalance + parameters: + - { name: id, in: query, required: true, schema: { type: string }, description: "id of account" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: number + description: accounts balance + "400": + description: NOK + content: + application/json: + schema: + type: string + description: reason of failure + + /balance/transactions: + get: + tags: + - CustomerService + - SystemService + summary: returns transactions of specific account for specific time interval + operationId: getTransactions + parameters: + - { name: id, in: query, required: true, schema: { type: string }, description: "id of account" } + - { name: beginning, in: query, required: true, schema: { type: string, format: date }, description: "date from which onwards the transactions happened" } + - { name: end, in: query, required: true, schema: { type: string, format: date }, description: "date before which wanted transactions happened" } + - { name: minAmount, in: query, required: false, schema: { type: number }, description: "minimal amount of money included in transaction to be reported" } + - { name: maxAmount, in: query, required: false, schema: { type: number }, description: "maximal amount of money included in transaction to be reported" } + - { name: type, in: query, required: false, schema: { $ref: "#/components/schemas/TransactionType" }, description: "type of transactiops to be included in returned ones" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Transaction" + "400": + description: NOK + content: + application/json: + schema: + type: string + description: reason of failure + + /balance/alltransactions: + get: + tags: + - EmployeeService + summary: returns all transactions for specific time interval + operationId: getAllTransactions + parameters: + - { name: beginning, in: query, required: true, schema: { type: string, format: date }, description: "date from which onwards the transactions happened" } + - { name: end, in: query, required: true, schema: { type: string, format: date }, description: "date before which wanted transactions happened" } + - { name: minAmount, in: query, required: false, schema: { type: number }, description: "minimal amount of money included in transaction to be reported" } + - { name: maxAmount, in: query, required: false, schema: { type: number }, description: "maximal amount of money included in transaction to be reported" } + - { name: type, in: query, required: false, schema: { $ref: "#/components/schemas/TransactionType" }, description: "type of transactiops to be included in returned ones" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Transaction" + "400": + description: NOK + content: + application/json: + schema: + type: string + description: reason of failure + + /balance/account/report: + get: + tags: + - EmployeeService + summary: creates a report of spending statistics for one user + operationId: createReport + parameters: + - { name: id, in: query, required: true, schema: { type: string }, description: "id of account" } + - { name: beginning, in: query, required: true, schema: { type: string, format: date }, description: "date from which onwards the transactions wanted in report happened" } + - { name: end, in: query, required: true, schema: { type: string, format: date }, description: "date before which wanted transactions wanted in report happened" } + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionsReport' + "400": + description: NOK + content: + application/json: + schema: + type: string + description: reason of failure