diff --git a/.docker/db/scripts/1_create_account_management_db.sql b/.docker/db/scripts/1_create_account_management_db.sql index b90aa18027cfa6753b677b80cd98a846c53b091f..61285b82315567a7d903faca4ba0cb7e6e5fbff2 100644 --- a/.docker/db/scripts/1_create_account_management_db.sql +++ b/.docker/db/scripts/1_create_account_management_db.sql @@ -31,6 +31,7 @@ CREATE TABLE IF NOT EXISTS scheduled_payment source_account_id BIGINT, target_account_id BIGINT, amount DECIMAL, + currency_code VARCHAR(3), recurrence_type VARCHAR(50), recurrence_payment_day INTEGER ); diff --git a/.docker/db/scripts/1_create_transaction_db.sql b/.docker/db/scripts/1_create_transaction_db.sql index 40a610c02c8d2627ff0852fd7329d86cd213e452..03191b75450e85570ebfda3f85145e222995c92d 100644 --- a/.docker/db/scripts/1_create_transaction_db.sql +++ b/.docker/db/scripts/1_create_transaction_db.sql @@ -5,7 +5,7 @@ SET SEARCH_PATH TO bank_transaction; CREATE TABLE IF NOT EXISTS proc_transaction ( - proc_uuid UUID PRIMARY KEY, + trans_uuid UUID PRIMARY KEY, acc_source VARCHAR(255), acc_target VARCHAR(255), type VARCHAR(255), @@ -14,3 +14,10 @@ CREATE TABLE IF NOT EXISTS proc_transaction detail_msg VARCHAR(255) ); +CREATE TABLE IF NOT EXISTS proc_reg +( + proc_uuid UUID PRIMARY KEY, + status_when TIMESTAMP, + status VARCHAR(255), + status_info VARCHAR(255) +); \ No newline at end of file diff --git a/.docker/db/scripts/2_generate_account_management_db.sql b/.docker/db/scripts/2_generate_account_management_db.sql index b061fe00fb6b74d78e6927a1c5e300ce08d2fcc5..2f473f8352c8b6491090bf016226cd4ae85c1684 100644 --- a/.docker/db/scripts/2_generate_account_management_db.sql +++ b/.docker/db/scripts/2_generate_account_management_db.sql @@ -20,10 +20,10 @@ VALUES (3656018305580485508, 'a5dc3241-71c9-4594-8a07-083c9c2b7b1c', 1, 1000, 0, (994, 'ACC4', 1, 1000, 1, 'CZK'); -INSERT INTO scheduled_payment (id, source_account_id, target_account_id, amount, recurrence_type, recurrence_payment_day) +INSERT INTO scheduled_payment (id, source_account_id, target_account_id, amount, currency_code, recurrence_type, recurrence_payment_day) VALUES - (991, 2, 3, 100, 0, 2), - (992, 2, 3, 200, 0, 3), - (993, 3, 2, 1200, 1, 15); + (991, 2, 3, 100, 'EUR', 0, 2), + (992, 2, 3, 200, 'EUR', 0, 3), + (993, 3, 2, 1200, 'EUR', 1, 15); diff --git a/.docker/db/scripts/2_generate_transaction_db.sql b/.docker/db/scripts/2_generate_transaction_db.sql index 25302370fb20ba0a032e5078884b7557731c478a..bf1b81cad26f8433f771ce4a4c03bc3c6067518c 100644 --- a/.docker/db/scripts/2_generate_transaction_db.sql +++ b/.docker/db/scripts/2_generate_transaction_db.sql @@ -2,7 +2,7 @@ SET SEARCH_PATH TO bank_transaction; -- INSERT starting data -INSERT INTO proc_transaction (proc_uuid, acc_source, acc_target, type, amount, curr_code, detail_msg) +INSERT INTO proc_transaction (trans_uuid, acc_source, acc_target, type, amount, curr_code, detail_msg) VALUES ('00000000-0000-0000-0000-000000000001', '123456789', '987654321', 'TRANSFER', 100.00, 'USD', 'Dummy transfer 1'), ('00000000-0000-0000-0000-000000000002', '987654321', '123456789', 'DEPOSIT', 200.00, 'EUR', 'Dummy deposit 1'), ('00000000-0000-0000-0000-000000000003', '111111111', '222222222', 'WITHDRAWAL', 300.00, 'GBP', 'Dummy withdrawal 1'), diff --git a/account-management/src/main/java/cz/muni/pa165/banking/application/controller/AccountController.java b/account-management/src/main/java/cz/muni/pa165/banking/application/controller/AccountController.java index 6df625abb9b54a7e11a41367cb1ddd971c9b04a3..dc2ed112fbc7c33a38667b730d73edf4aa1c0df5 100644 --- a/account-management/src/main/java/cz/muni/pa165/banking/application/controller/AccountController.java +++ b/account-management/src/main/java/cz/muni/pa165/banking/application/controller/AccountController.java @@ -1,16 +1,14 @@ package cz.muni.pa165.banking.application.controller; import cz.muni.pa165.banking.account.management.AccountApi; -import cz.muni.pa165.banking.account.management.dto.AccountDto; -import cz.muni.pa165.banking.account.management.dto.NewAccountDto; -import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentDto; -import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentsDto; +import cz.muni.pa165.banking.account.management.dto.*; import cz.muni.pa165.banking.application.facade.AccountFacade; -import cz.muni.pa165.banking.exception.EntityNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; +import java.time.LocalDate; + @RestController public class AccountController implements AccountApi { @@ -37,13 +35,13 @@ public class AccountController implements AccountApi { @Override public ResponseEntity<ScheduledPaymentsDto> getScheduledPayments(String accountNumber) { - ScheduledPaymentsDto payments; - try{ - payments = accountFacade.findScheduledPaymentsByNumber(accountNumber); - } - catch (EntityNotFoundException e){ - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } + ScheduledPaymentsDto payments = accountFacade.findScheduledPaymentsByNumber(accountNumber); + return ResponseEntity.ok(payments); + } + + @Override + public ResponseEntity<ScheduledPaymentsDto> getScheduledPaymentsOf(LocalDate date) { + ScheduledPaymentsDto payments = accountFacade.scheduledPaymentsOfDay(date); return ResponseEntity.ok(payments); } diff --git a/account-management/src/main/java/cz/muni/pa165/banking/application/controller/UserController.java b/account-management/src/main/java/cz/muni/pa165/banking/application/controller/UserController.java index 261ba6df0f3c6e7cd4ae6660319e6e0197829934..9c2bfb02981aefc5e2852ebc5464dd3f5f289660 100644 --- a/account-management/src/main/java/cz/muni/pa165/banking/application/controller/UserController.java +++ b/account-management/src/main/java/cz/muni/pa165/banking/application/controller/UserController.java @@ -4,7 +4,6 @@ import cz.muni.pa165.banking.account.management.UserApi; import cz.muni.pa165.banking.account.management.dto.NewUserDto; import cz.muni.pa165.banking.account.management.dto.UserDto; import cz.muni.pa165.banking.application.facade.UserFacade; -import cz.muni.pa165.banking.exception.EntityNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -25,13 +24,7 @@ public class UserController implements UserApi{ @Override public ResponseEntity<UserDto> findUserById(Long userId) { - UserDto user; - try{ - user = userFacade.findById(userId); - } - catch (EntityNotFoundException e){ - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } + UserDto user = userFacade.findById(userId); return ResponseEntity.ok(user); } diff --git a/account-management/src/main/java/cz/muni/pa165/banking/application/facade/AccountFacade.java b/account-management/src/main/java/cz/muni/pa165/banking/application/facade/AccountFacade.java index f8e1a774987685927aeaeea7f33f5023a4876cec..9b6077cd7be1b0bf72b56fa407ba3f85b2640867 100644 --- a/account-management/src/main/java/cz/muni/pa165/banking/application/facade/AccountFacade.java +++ b/account-management/src/main/java/cz/muni/pa165/banking/application/facade/AccountFacade.java @@ -1,9 +1,6 @@ package cz.muni.pa165.banking.application.facade; -import cz.muni.pa165.banking.account.management.dto.AccountDto; -import cz.muni.pa165.banking.account.management.dto.NewAccountDto; -import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentDto; -import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentsDto; +import cz.muni.pa165.banking.account.management.dto.*; import cz.muni.pa165.banking.application.mapper.DtoMapper; import cz.muni.pa165.banking.application.service.AccountService; import cz.muni.pa165.banking.domain.account.Account; @@ -12,7 +9,9 @@ import cz.muni.pa165.banking.exception.EntityNotFoundException; import cz.muni.pa165.banking.exception.UnexpectedValueException; import org.springframework.stereotype.Component; +import java.time.LocalDate; import java.util.Currency; +import java.util.List; @Component @@ -60,4 +59,13 @@ public class AccountFacade { public AccountDto findByAccountNumber(String accountNumber) { return mapper.map(accountService.findByNumber(accountNumber)); } + + public ScheduledPaymentsDto scheduledPaymentsOfDay(LocalDate date) { + List<ScheduledPayment> payments = accountService.scheduledPaymentsOfDay(date); + ScheduledPaymentsDto result = new ScheduledPaymentsDto(); + result.setScheduledPayments(payments.stream() + .map(mapper::map) + .toList()); + return result; + } } diff --git a/account-management/src/main/java/cz/muni/pa165/banking/application/mapper/DtoMapper.java b/account-management/src/main/java/cz/muni/pa165/banking/application/mapper/DtoMapper.java index 9c7eb93419aaff60a532dd77907173c90dfdf92f..088749afdc9b203eb2f7aabaa31a9fe2f5bc60f5 100644 --- a/account-management/src/main/java/cz/muni/pa165/banking/application/mapper/DtoMapper.java +++ b/account-management/src/main/java/cz/muni/pa165/banking/application/mapper/DtoMapper.java @@ -45,6 +45,8 @@ public interface DtoMapper { return result; } + ScheduledPaymentDto map(ScheduledPayment scheduledPayment); + ScheduledPaymentType map(RecurrenceType type); RecurrenceType map(ScheduledPaymentType type); diff --git a/account-management/src/main/java/cz/muni/pa165/banking/application/service/AccountService.java b/account-management/src/main/java/cz/muni/pa165/banking/application/service/AccountService.java index af6d7ec26d4a059a7ba42445e4aff873552ff721..7966ac928fa96e72dc37f10086edfbcc5c6aba97 100644 --- a/account-management/src/main/java/cz/muni/pa165/banking/application/service/AccountService.java +++ b/account-management/src/main/java/cz/muni/pa165/banking/application/service/AccountService.java @@ -6,15 +6,16 @@ import cz.muni.pa165.banking.domain.account.repository.AccountRepository; import cz.muni.pa165.banking.domain.scheduled.ScheduledPayment; import cz.muni.pa165.banking.domain.scheduled.ScheduledPaymentProjection; import cz.muni.pa165.banking.domain.scheduled.recurrence.Recurrence; +import cz.muni.pa165.banking.domain.scheduled.recurrence.RecurrenceQuerySpecificationBuilder; import cz.muni.pa165.banking.domain.scheduled.recurrence.RecurrenceType; import cz.muni.pa165.banking.domain.scheduled.repository.ScheduledPaymentRepository; import cz.muni.pa165.banking.domain.user.repository.UserRepository; import cz.muni.pa165.banking.exception.EntityNotFoundException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -74,6 +75,7 @@ public class AccountService { newScheduledPayment.setSourceAccountId(senderAccount.getId()); newScheduledPayment.setTargetAccountId(receiverAccount.getId()); newScheduledPayment.setAmount(amount); + newScheduledPayment.setCurrencyCode(senderAccount.getCurrency().getCurrencyCode()); Recurrence recurrence = new Recurrence(); recurrence.setType(recurrenceType); @@ -109,4 +111,7 @@ public class AccountService { .toList(); } + public List<ScheduledPayment> scheduledPaymentsOfDay(LocalDate date) { + return scheduledPaymentsRepository.findAll(RecurrenceQuerySpecificationBuilder.forDay(date)); + } } diff --git a/account-management/src/main/java/cz/muni/pa165/banking/domain/scheduled/ScheduledPayment.java b/account-management/src/main/java/cz/muni/pa165/banking/domain/scheduled/ScheduledPayment.java index 67aeaa580470bf646a0f7518726dce3e1f50a426..ce7404d5380bb1bbe889ee4b445d749844de8cb8 100644 --- a/account-management/src/main/java/cz/muni/pa165/banking/domain/scheduled/ScheduledPayment.java +++ b/account-management/src/main/java/cz/muni/pa165/banking/domain/scheduled/ScheduledPayment.java @@ -22,6 +22,9 @@ public class ScheduledPayment { private BigDecimal amount; + @Column(name = "currency_code") + private String currencyCode; + @Embedded @AttributeOverrides({ @AttributeOverride(name = "type", column = @Column(name = "recurrence_type")), @@ -65,6 +68,14 @@ public class ScheduledPayment { this.amount = amount; } + public String getCurrencyCode() { + return currencyCode; + } + + public void setCurrencyCode(String currencyCode) { + this.currencyCode = currencyCode; + } + public Recurrence getRecurrence() { return recurrence; } 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 index ad06ee984adec9c4f43f2ae2e5d0034b5f65fee0..815a98879dc6cb91399cff31ff0c259fc814141c 100644 --- 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 @@ -19,7 +19,7 @@ public class StatisticalReport { private final TransactionStatistics withdrawalAmount = new TransactionStatistics(TransactionType.WITHDRAW); - private final TransactionStatistics crossAccountAmount = new TransactionStatistics(TransactionType.CROSS_ACCOUNT_PAYMENT); + private final TransactionStatistics crossAccountAmount = new TransactionStatistics(TransactionType.TRANSFER); private final TransactionStatistics creditAmount = new TransactionStatistics(TransactionType.CREDIT); @@ -49,7 +49,7 @@ public class StatisticalReport { 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()); + case TRANSFER -> crossAccountAmount.AddAmount(transaction.getAmount()); } } 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 index 2a1fb15b87e63a153fb22f837940e0b2bf5b7e6d..5d0988c86783f5f2da92ae27e580ff3c7b5ba064 100644 --- 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 @@ -11,7 +11,7 @@ public enum TransactionType { CREDIT, - CROSS_ACCOUNT_PAYMENT, + TRANSFER, REFUND diff --git a/account-query/src/test/java/cz/muni/pa165/banking/domain/report/StatisticalReportTest.java b/account-query/src/test/java/cz/muni/pa165/banking/domain/report/StatisticalReportTest.java index 1a56a8f4b0b34e30a3de7c6de4a2bb432a9d48cb..1b0418837ed7bac0a9a8baa4bdf49017c4320e9b 100644 --- a/account-query/src/test/java/cz/muni/pa165/banking/domain/report/StatisticalReportTest.java +++ b/account-query/src/test/java/cz/muni/pa165/banking/domain/report/StatisticalReportTest.java @@ -21,7 +21,7 @@ class StatisticalReportTest { Transaction tr3 = new Transaction(TransactionType.WITHDRAW, BigDecimal.ONE, OffsetDateTime.now(), new UUID(2, 2)); Transaction tr5 = new Transaction(TransactionType.REFUND, BigDecimal.ONE, OffsetDateTime.now(), new UUID(2, 2)); Transaction tr6 = new Transaction(TransactionType.DEPOSIT, BigDecimal.ONE, OffsetDateTime.now(), new UUID(2, 2)); - Transaction tr7 = new Transaction(TransactionType.CROSS_ACCOUNT_PAYMENT, BigDecimal.ONE, OffsetDateTime.now(), new UUID(2, 2)); + Transaction tr7 = new Transaction(TransactionType.TRANSFER, BigDecimal.ONE, OffsetDateTime.now(), new UUID(2, 2)); report = new StatisticalReport(List.of(tr1, tr2, tr3, tr5, tr6, tr7)); } @Test @@ -59,7 +59,7 @@ class StatisticalReportTest { //Act TransactionStatistics statistics = report.getCrossAccountAmount(); //Assert - assertThat(statistics.getType()).isEqualTo(TransactionType.CROSS_ACCOUNT_PAYMENT); + assertThat(statistics.getType()).isEqualTo(TransactionType.TRANSFER); } @Test public void whenGetAllStatsThenStatsOfAllTypesReturned(){ diff --git a/m2m-banking-api/account-management-api/openapi.yaml b/m2m-banking-api/account-management-api/openapi.yaml index d2472c8698bfb7f9ae5adc9432241cdb24c67f14..18dc08b5a88034ae3478891cae54719840e15cde 100644 --- a/m2m-banking-api/account-management-api/openapi.yaml +++ b/m2m-banking-api/account-management-api/openapi.yaml @@ -129,6 +129,7 @@ components: - senderAccountNumber - receiverAccountNumber - amount + - currencyCode - type - day properties: @@ -140,6 +141,8 @@ components: type: number format: decimal description: amount of money to send + currencyCode: + type: string type: $ref: '#/components/schemas/ScheduledPaymentType' day: @@ -309,3 +312,25 @@ paths: $ref: '#/components/schemas/ScheduledPaymentDto' "400": description: NOK + + /scheduled: + get: + tags: + - Account + summary: Scheduled payments of a day + operationId: getScheduledPaymentsOf + requestBody: + required: true + content: + application/json: + schema: + type: string + format: date + nullable: false + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduledPaymentsDto' \ No newline at end of file diff --git a/m2m-banking-api/account-query-api/openapi.yaml b/m2m-banking-api/account-query-api/openapi.yaml index 344ed566183609927723e57bc97646414b7b7065..1560488aea29fa5759c3b2ffc8fde8d219eef69a 100644 --- a/m2m-banking-api/account-query-api/openapi.yaml +++ b/m2m-banking-api/account-query-api/openapi.yaml @@ -17,7 +17,7 @@ components: schemas: TransactionType: type: string - enum: [ WITHDRAW, DEPOSIT, CREDIT, CROSS_ACCOUNT_PAYMENT, REFUND ] + enum: [ WITHDRAW, DEPOSIT, CREDIT, TRANSFER, REFUND ] description: type of transaction Transaction: title: A transaction diff --git a/m2m-banking-api/transaction-api/openapi.yaml b/m2m-banking-api/transaction-api/openapi.yaml index f44f9df748c9906e8b22027be1e05fb0b00526a9..d4bd661243056aa2e18d568f31dd93d02b813a7e 100644 --- a/m2m-banking-api/transaction-api/openapi.yaml +++ b/m2m-banking-api/transaction-api/openapi.yaml @@ -62,6 +62,28 @@ paths: '404': description: Resource or process by UUID not found + /transaction/v1/scheduled/trigger: + post: + tags: + - Transaction + operationId: executeSchedulePayments + summary: Trigger scheduled payments + description: | + Manual execution of scheduled payments for a given date. + The input date is optional, if none is provided then the current day is used. + parameters: + - in: query + name: date + schema: + type: string + format: date + required: false + responses: + '200': + description: Executed + '500': + description: Failure + /transaction/v1/revert: post: tags: @@ -89,6 +111,119 @@ paths: description: Process found by UUID but not in expected state (PROCESSED) '404': description: Resource or process by UUID not found + + /process/v1/{uuid}/status: + get: + tags: + - Process + operationId: getProcessStatus + summary: Process Status + description: | + Method finds an existing process for a transaction request, returning all information about it's state + and further information regarding errors. + parameters: + - in: path + name: uuid + required: true + schema: + type: string + format: uuid + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessStatusDto' + '404': + description: Process by UUID not found + + /processes/v1/{status}: + get: + tags: + - Process + operationId: getProcessesOfState + summary: Processes in a state + description: Find a collection of Processes in a defined state + parameters: + - in: path + name: status + required: true + schema: + $ref: '#/components/schemas/StatusDto' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessStatusListDto' + + /processes/v1/resolve/threshold: + get: + tags: + - Process + operationId: getDefaultThreshold + summary: Process resolution date threshold + description: Get the default threshold used by the system to mark unresolved processes as failed + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ThresholdDto' + + /processes/v1/resolve: + get: + tags: + - Process + operationId: getUnresolvedProcesses + summary: Unresolved processes + description: | + Get a collection of unresolved processes to a given date. If no date is provided, the default threshold is used. + parameters: + - in: query + name: date + schema: + type: string + format: date + required: false + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessStatusListDto' + patch: + tags: + - Process + operationId: resolveProcesses + summary: Fail unresolved processes + description: | + Mark unresolved processes as failed. The input date marks as the threshold below which processes + in the state CREATED or PENDING shall be marked as resolved by failure. The provided date must be + before the current day. If no date is provided, the default threshold is used. + Method returns a collection of Process UUID's, which have been resolved. + parameters: + - in: query + name: date + schema: + type: string + format: date + required: false + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: string + format: uuid + components: schemas: @@ -99,6 +234,7 @@ components: - DEPOSIT - TRANSFER - SCHEDULED + - REFUND description: Enumaration defining a type for a transaction. Each type may have different certain implementations and validations. AccountDto: @@ -156,4 +292,43 @@ components: allOf: - $ref: '#/components/schemas/ProcessDto' - $ref: '#/components/schemas/TransactionDto' - \ No newline at end of file + + StatusDetailDto: + type: object + properties: + created: + type: string + format: date-time + status: + $ref: '#/components/schemas/StatusDto' + information: + type: string + + ProcessStatusDto: + type: object + properties: + identifier: + type: string + format: uuid + status: + $ref: '#/components/schemas/StatusDetailDto' + + ProcessStatusListDto: + type: object + properties: + when: + type: string + format: date-time + processes: + type: array + items: + $ref: '#/components/schemas/ProcessStatusDto' + + ThresholdDto: + type: object + properties: + message: + type: string + currentThreshold: + type: string + format: date \ No newline at end of file diff --git a/transaction-processor/.todo-list b/transaction-processor/.todo-list deleted file mode 100644 index d425db5eed299212dc9db86d756977e27c3555cf..0000000000000000000000000000000000000000 --- a/transaction-processor/.todo-list +++ /dev/null @@ -1,10 +0,0 @@ -- externalize openapi contract into separate artifact -- handler for cross-account transaction -- exchange rate API for currency conversion -- custom exception classes -- handler for exceptions -> Controller Advice -- implementation for status and rollback/revert of transaction - - -- TESTS TESTS TESTS -- review within teammates projects / branches (bude toho dost asi zejo) \ No newline at end of file diff --git a/transaction-processor/pom.xml b/transaction-processor/pom.xml index 003a95d0efb68b5d8eab782c2201b3dca314a50e..32b296ccaab1bc14c5166ab4cfe993cb093bc468 100644 --- a/transaction-processor/pom.xml +++ b/transaction-processor/pom.xml @@ -88,11 +88,6 @@ <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency> - <dependency> - <groupId>org.mapstruct</groupId> - <artifactId>mapstruct-processor</artifactId> - <version>${org.mapstruct.version}</version> - </dependency> <!-- DB --> <dependency> @@ -124,6 +119,13 @@ <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.3.0</version> </dependency> + + <!-- Test utils --> + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> @@ -247,6 +249,11 @@ </execution> </executions> </plugin> + <!-- run integration tests in "mvn verify" phase --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + </plugin> </plugins> </build> diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/AsyncMessagingConfiguration.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/AsyncMessagingConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..7146acc5621a547e260540d86fceab6af9af9b65 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/AsyncMessagingConfiguration.java @@ -0,0 +1,20 @@ +package cz.muni.pa165.banking.application.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +public class AsyncMessagingConfiguration { + + @Bean + public ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setThreadNamePrefix("Async-"); + executor.initialize(); + return executor; + } + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/ExchangeRateInitializer.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/ExchangeRateInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..8cff4df537ee02f65bfb276b16f110045be517f1 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/ExchangeRateInitializer.java @@ -0,0 +1,39 @@ +package cz.muni.pa165.banking.application.configuration; + +import cz.muni.pa165.banking.domain.money.exchange.ExchangeRateService; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Currency; +import java.util.List; + +@Component +public class ExchangeRateInitializer { + + private final Logger logger = LoggerFactory.getLogger(ExchangeRateInitializer.class); + + private final ExchangeRateService exchangeRateService; + + @Value("#{'${banking.apps.rates.initial.currencies}'.split(',')}") + private List<String> currencies; + + public ExchangeRateInitializer(ExchangeRateService exchangeRateService) { + this.exchangeRateService = exchangeRateService; + } + + @PostConstruct + void initialize() { + for (String currency : currencies) { + try { + // ignore result, just call service to trigger mechanism to cache data + exchangeRateService.getRate(Currency.getInstance(currency), Currency.getInstance(currency)); + } catch (Exception e) { + logger.warn(String.format("Initializing exchange rates for %s failed. API might be unavailable!", currency)); + } + } + } + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/controller/ProcessController.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/controller/ProcessController.java new file mode 100644 index 0000000000000000000000000000000000000000..4a77544dab2fae1e7af813b35cc5a92c74dc7a19 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/controller/ProcessController.java @@ -0,0 +1,49 @@ +package cz.muni.pa165.banking.application.controller; + +import cz.muni.pa165.banking.application.facade.ProcessFacade; +import cz.muni.pa165.banking.transaction.processor.ProcessApi; +import cz.muni.pa165.banking.transaction.processor.dto.ProcessStatusDto; +import cz.muni.pa165.banking.transaction.processor.dto.ProcessStatusListDto; +import cz.muni.pa165.banking.transaction.processor.dto.StatusDto; +import cz.muni.pa165.banking.transaction.processor.dto.ThresholdDto; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +@RestController +public class ProcessController implements ProcessApi { + + private final ProcessFacade processFacade; + + public ProcessController(ProcessFacade processFacade) { + this.processFacade = processFacade; + } + + @Override + public ResponseEntity<ThresholdDto> getDefaultThreshold() { + return ResponseEntity.ok(processFacade.getDefaultThreshold()); + } + + @Override + public ResponseEntity<ProcessStatusDto> getProcessStatus(UUID uuid) { + return ResponseEntity.ok(processFacade.getProcessStatus(uuid)); + } + + @Override + public ResponseEntity<ProcessStatusListDto> getProcessesOfState(StatusDto status) { + return ResponseEntity.ok(processFacade.getProcessesOfState(status)); + } + + @Override + public ResponseEntity<ProcessStatusListDto> getUnresolvedProcesses(LocalDate date) { + return ResponseEntity.ok(processFacade.getUnresolvedProcesses(date)); + } + + @Override + public ResponseEntity<List<UUID>> resolveProcesses(LocalDate date) { + return ResponseEntity.ok(processFacade.resolveProcesses(date)); + } +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/controller/TransactionController.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/controller/TransactionController.java index af908faf0d588d58dce85daa6baf617e1dae317c..7cf6096ce8c7deafd6d15677d472791d83afe15a 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/controller/TransactionController.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/controller/TransactionController.java @@ -1,6 +1,7 @@ package cz.muni.pa165.banking.application.controller; import cz.muni.pa165.banking.application.facade.TransactionFacade; +import cz.muni.pa165.banking.application.service.ScheduledPaymentService; import cz.muni.pa165.banking.transaction.processor.TransactionApi; import cz.muni.pa165.banking.transaction.processor.dto.ProcessDetailDto; import cz.muni.pa165.banking.transaction.processor.dto.ProcessDto; @@ -8,15 +9,19 @@ import cz.muni.pa165.banking.transaction.processor.dto.TransactionDto; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.time.LocalDate; import java.util.UUID; @RestController public class TransactionController implements TransactionApi { private final TransactionFacade facade; + + private final ScheduledPaymentService scheduledPaymentService; - public TransactionController(TransactionFacade facade) { + public TransactionController(TransactionFacade facade, ScheduledPaymentService scheduledPaymentService) { this.facade = facade; + this.scheduledPaymentService = scheduledPaymentService; } @@ -38,4 +43,12 @@ public class TransactionController implements TransactionApi { return ResponseEntity.ok(revertingProcess); } + @Override + public ResponseEntity<Void> executeSchedulePayments(LocalDate date) { + if (date == null) { + date = LocalDate.now(); + } + scheduledPaymentService.executeScheduledPayments(date); + return ResponseEntity.ok(null); + } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/facade/ProcessFacade.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/facade/ProcessFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..5dfbc2257d46c01748cfc53b8a94ac8d5914371d --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/facade/ProcessFacade.java @@ -0,0 +1,91 @@ +package cz.muni.pa165.banking.application.facade; + +import cz.muni.pa165.banking.application.service.ProcessService; +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.transaction.processor.dto.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.UUID; + +@Component +public class ProcessFacade { + + @Value("${process.resolution.threshold}") + private int thresholdDays; + + private final ProcessService service; + + public ProcessFacade(ProcessService service) { + this.service = service; + } + + public ThresholdDto getDefaultThreshold() { + return new ThresholdDto() + .message("The default threshold is set to be " + thresholdDays + " days ago.") + .currentThreshold(defaultThreshold()); + } + + public ProcessStatusDto getProcessStatus(UUID uuid) { + Process process = service.findByUuid(uuid); + return map(process); + } + + public ProcessStatusListDto getProcessesOfState(StatusDto statusDto) { + OffsetDateTime now = OffsetDateTime.now(); + Status status = Status.valueOf(statusDto.name()); + List<Process> processes = service.processesWithStatus(status); + + return new ProcessStatusListDto() + .when(now) + .processes( + processes.stream() + .map(this::map) + .toList() + ); + } + + public ProcessStatusListDto getUnresolvedProcesses(LocalDate threshold) { + OffsetDateTime now = OffsetDateTime.now(); + + if (threshold == null) { + threshold = defaultThreshold(); + } + List<Process> processes = service.unresolvedProcessesToDate(threshold); + + return new ProcessStatusListDto() + .when(now) + .processes( + processes.stream() + .map(this::map) + .toList() + ); + } + + public List<UUID> resolveProcesses(LocalDate threshold) { + if (threshold == null) { + threshold = defaultThreshold(); + } + return service.resolveProcesses(threshold); + } + + private LocalDate defaultThreshold() { + return LocalDate.now().minusDays(thresholdDays); + } + + + private ProcessStatusDto map(Process process) { + return new ProcessStatusDto() + .identifier(process.getUuid()) + .status(new StatusDetailDto() + .created(process.getWhen().atOffset(ZoneOffset.UTC)) + .status(StatusDto.valueOf(process.getStatus().name())) + .information(process.getInformation())); + } + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/mapper/DtoMapper.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/mapper/DtoMapper.java index 91672d333dce23118e744ee9bafa6b0692817b1c..61a32d648c66532bb44338244270563cc036bc22 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/mapper/DtoMapper.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/mapper/DtoMapper.java @@ -36,18 +36,18 @@ public interface DtoMapper { default ProcessDto map(Process source) { ProcessDto dto = new ProcessDto(); - dto.setIdentifier(source.uuid()); + dto.setIdentifier(source.getUuid()); dto.setStatus(map(source.getStatus())); - dto.setInfo(source.getStatusInformation()); + dto.setInfo(source.getInformation()); return dto; } default ProcessDetailDto map(Process process, Transaction transaction) { ProcessDetailDto dto = new ProcessDetailDto(); - dto.identifier(process.uuid()); + dto.identifier(process.getUuid()); dto.status(map(process.getStatus())); - dto.info(process.getStatusInformation()); + dto.info(process.getInformation()); dto.source(map(transaction.getSource())); if (transaction.getTarget() != null) { diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/MessagingService.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/MessagingService.java new file mode 100644 index 0000000000000000000000000000000000000000..fc38675999d1da2621818ef013418abbb58c478a --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/MessagingService.java @@ -0,0 +1,40 @@ +package cz.muni.pa165.banking.application.messaging; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +@Service +public class MessagingService { + + private final Logger logger = LoggerFactory.getLogger(MessagingService.class); + + private final ProcessListener processListener; + + private final Queue<String> queue = new ConcurrentLinkedQueue<>(); + + public MessagingService(ProcessListener processListener) { + this.processListener = processListener; + } + + public void addToQueue(String message) { + queue.add(message); + triggerRead(); + } + + @Async + void triggerRead() { + String message = queue.poll(); + try { + processListener.onReceived(message); + } catch (JsonProcessingException e) { + logger.error("Error while processing message"); + } + } + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessListener.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessListener.java index d6bf974e6db5935d3006487872558fd08a0d2726..896cb904bebbc898371c3c4919ac58d35340e860 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessListener.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessListener.java @@ -1,5 +1,7 @@ package cz.muni.pa165.banking.application.messaging; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import cz.muni.pa165.banking.application.service.ProcessHandlerService; import cz.muni.pa165.banking.domain.messaging.ProcessRequest; import org.springframework.stereotype.Service; @@ -7,18 +9,20 @@ import org.springframework.stereotype.Service; @Service public class ProcessListener { - private final ProcessHandlerService service; + private final ObjectMapper objectMapper; + + private final ProcessHandlerService processHandlerService; - public ProcessListener(ProcessHandlerService service) { - this.service = service; + public ProcessListener(ObjectMapper objectMapper, ProcessHandlerService processHandlerService) { + this.objectMapper = objectMapper; + this.processHandlerService = processHandlerService; } - - // TODO Milestone-2/3, add messaging RabbitMq dependencies -// @RabbitListener(queues = "${messaging.exchange:process-request}") - public void onReceived(ProcessRequest message) { + public void onReceived(String message) throws JsonProcessingException { System.out.println(message); - service.handle(message); + + ProcessRequest request = objectMapper.readValue(message, ProcessRequest.class); + processHandlerService.handle(request); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java index 44cdc381f250ed25160a6138efbbb89cf074ac8a..ff6cb7887ba981c17aa3a5b13b602b403dc14060 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import cz.muni.pa165.banking.domain.messaging.MessageProducer; import cz.muni.pa165.banking.domain.messaging.ProcessRequest; import cz.muni.pa165.banking.exception.ServerError; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Map; @@ -12,18 +11,14 @@ import java.util.Map; @Service public class ProcessProducer implements MessageProducer { - // TODO Milestone-2/3, add messaging RabbitMq dependencies -// private final RabbitTemplate template; + private final MessagingService messagingService; private final ObjectMapper mapper; - @Value("${messaging.exchange:process-request}") - private String EXCHANGE_NAME; - - public ProcessProducer(ObjectMapper mapper) { + public ProcessProducer(MessagingService messagingService, ObjectMapper mapper) { + this.messagingService = messagingService; this.mapper = mapper; } - @Override public void send(ProcessRequest data) { @@ -39,7 +34,7 @@ public class ProcessProducer implements MessageProducer { ) ); } -// template.convertAndSend(EXCHANGE_NAME, "", dataAsJsonString); + messagingService.addToQueue(dataAsJsonString); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/AccountApiProxy.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/AccountApiProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..32a821ea408d90dbc09cb12634a0bd04feecd9e7 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/AccountApiProxy.java @@ -0,0 +1,8 @@ +package cz.muni.pa165.banking.application.proxy; + +import cz.muni.pa165.banking.account.management.AccountApi; +import org.springframework.cloud.openfeign.FeignClient; + +@FeignClient(url = "${banking.apps.management.url}", name = "AccountApi") +public interface AccountApiProxy extends AccountApi { +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/Dummy.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/Dummy.java deleted file mode 100644 index f832677ac863837007b4204d933050a48432f934..0000000000000000000000000000000000000000 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/Dummy.java +++ /dev/null @@ -1,20 +0,0 @@ -package cz.muni.pa165.banking.application.proxy; - -import cz.muni.pa165.banking.account.query.CustomerServiceApi; -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class Dummy { - - @Autowired - private CustomerServiceApi customerServiceApi; - - @PostConstruct - public void test() { - var jj = customerServiceApi.getBalance("id1"); - System.out.println("jozo"); - } - -} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/ExchangeRatesApi.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/ExchangeRatesApi.java deleted file mode 100644 index 62e21b8cfc298e1426dc6df7719376b443ad0600..0000000000000000000000000000000000000000 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/ExchangeRatesApi.java +++ /dev/null @@ -1,10 +0,0 @@ -package cz.muni.pa165.banking.application.proxy; - -// some proxy for a REST API, which contains real-time data about the currency rates -// e.g. https://exchangeratesapi.io/ -// https://nordicapis.com/10-apis-for-currency-exchange-rates/ -public interface ExchangeRatesApi { - - // TODO nejake methods po vybere a nastudovani API - -} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/SystemServiceApiProxy.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/SystemServiceApiProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..abe170cf37833e1cc22f0630e7e499634a0bf119 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/SystemServiceApiProxy.java @@ -0,0 +1,8 @@ +package cz.muni.pa165.banking.application.proxy; + +import cz.muni.pa165.banking.account.query.SystemServiceApi; +import org.springframework.cloud.openfeign.FeignClient; + +@FeignClient(url = "${banking.apps.query.url}", name = "SystemServiceApi") +public interface SystemServiceApiProxy extends SystemServiceApi { +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRateResponseProcessor.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRateResponseProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..e5f4ba4daa30f50a81492d364a6dfd19a1d7cea0 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRateResponseProcessor.java @@ -0,0 +1,46 @@ +package cz.muni.pa165.banking.application.proxy.rate; + +import cz.muni.pa165.banking.exception.UnexpectedValueException; + +import java.math.BigDecimal; +import java.util.Currency; +import java.util.HashMap; +import java.util.Map; + +public class ExchangeRateResponseProcessor { + + public static Map<Currency, BigDecimal> process(Map<String, Object> response) { + if (!response.get("result").equals("success")) { + throw new UnexpectedValueException("Exchange rate API unavailable"); + } + + Map<String, Object> rates = (Map<String, Object>) response.get("conversion_rates"); + + Map<Currency, BigDecimal> result = new HashMap<>(); + for (String currencyCode : rates.keySet()) { + Currency currency; + try { + currency = Currency.getInstance(currencyCode); + } catch (Exception e) { + continue; + } + Object value = rates.get(currencyCode); + + BigDecimal rate; + if (value instanceof Integer val) { + rate = BigDecimal.valueOf(val); + } else if (value instanceof Double val) { + rate = BigDecimal.valueOf(val); + } else { + String strVal = value.toString(); + double val = Double.parseDouble(strVal); + rate = BigDecimal.valueOf(val); + } + + result.put(currency, rate); + } + + return result; + } + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRatesApi.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRatesApi.java new file mode 100644 index 0000000000000000000000000000000000000000..f95e7581bf8b50a2ecce57a636848be9eee54228 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRatesApi.java @@ -0,0 +1,16 @@ +package cz.muni.pa165.banking.application.proxy.rate; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.Map; + +// https://app.exchangerate-api.com +@FeignClient(name = "ExchangeRateApi", url = "${banking.apps.rates.url}") +public interface ExchangeRatesApi { + + @GetMapping("/latest/{currency}") + Map<String, Object> getRatesOfCurrency(@RequestParam String currency); + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryImpl.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryImpl.java index 8d13fe11d21ad2ff03b54b8c1b0f139b40fa1460..e2bfb30fa23170147295ac2b403486bb97fa7882 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryImpl.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryImpl.java @@ -2,31 +2,51 @@ package cz.muni.pa165.banking.application.repository; import cz.muni.pa165.banking.domain.process.Process; import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.exception.EntityNotFoundException; import org.springframework.stereotype.Repository; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.time.*; +import java.util.*; @Repository public class ProcessRepositoryImpl implements ProcessRepository { - // TODO until app has no DB connection -> Milestone2 - private final Map<UUID, Process> inmemoryDb = new HashMap<>(); + private final ProcessRepositoryJpa repository; + + public ProcessRepositoryImpl(ProcessRepositoryJpa repository) { + this.repository = repository; + } @Override public boolean idExists(UUID uuid) { - return inmemoryDb.containsKey(uuid); + return repository.existsById(uuid); } @Override public Process findById(UUID uuid) { - return inmemoryDb.get(uuid); + Optional<Process> process = repository.findById(uuid); + if (process.isEmpty()) { + throw new EntityNotFoundException(String.format("Process with UUID %s not found", uuid)); + } + return process.get(); } @Override public void save(Process process) { - inmemoryDb.put(process.uuid(), process); + repository.save(process); + } + + @Override + public List<Process> findProcessOfStatus(Status status) { + return repository.findAllWithStatus(status); + } + + @Override + public List<Process> findProcessesOfStatusUptoDate(Status status, LocalDate localDate) { + LocalDateTime endOfDay = localDate.atTime(LocalTime.MAX); + Instant instant = endOfDay.toInstant(ZoneOffset.UTC); + return repository.findByStatusAndDateBeforeEqual(status, instant); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryJpa.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryJpa.java new file mode 100644 index 0000000000000000000000000000000000000000..7437ca16690f211b70a32e81031d822b37016d71 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryJpa.java @@ -0,0 +1,21 @@ +package cz.muni.pa165.banking.application.repository; + +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.status.Status; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public interface ProcessRepositoryJpa extends JpaRepository<Process, UUID> { + + @Query("SELECT p FROM Process p WHERE p.currentStatus.status = :status") + List<Process> findAllWithStatus(@Param("status") Status status); + + @Query("SELECT p FROM Process p WHERE p.currentStatus.status = :status AND p.currentStatus.when <= :date") + List<Process> findByStatusAndDateBeforeEqual(@Param("status") Status status, @Param("date") Instant date); + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/AccountServiceImpl.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/AccountServiceImpl.java index a65602e495288f5f6f501cd3534608dfd332ce10..bf5f658d0c6a09be9272ac9f91633109da26b2db 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/AccountServiceImpl.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/AccountServiceImpl.java @@ -1,5 +1,9 @@ package cz.muni.pa165.banking.application.service; +import cz.muni.pa165.banking.account.management.AccountApi; +import cz.muni.pa165.banking.account.management.dto.AccountDto; +import cz.muni.pa165.banking.account.query.CustomerServiceApi; +import cz.muni.pa165.banking.account.query.SystemServiceApi; import cz.muni.pa165.banking.domain.account.Account; import cz.muni.pa165.banking.domain.remote.AccountService; import cz.muni.pa165.banking.domain.transaction.TransactionType; @@ -7,31 +11,54 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.Currency; +import java.util.Objects; import java.util.UUID; @Service public class AccountServiceImpl implements AccountService { - // TODO add proxies to other services of teammates and calls with validations - // -> Milestone2 + private final AccountApi accountApi; + private final CustomerServiceApi publicBalanceApi; + + private final SystemServiceApi privateBalanceApi; + + public AccountServiceImpl(AccountApi accountApi, CustomerServiceApi publicBalanceApi, SystemServiceApi privateBalanceApi) { + this.accountApi = accountApi; + this.publicBalanceApi = publicBalanceApi; + this.privateBalanceApi = privateBalanceApi; + } + @Override public Currency getAccountCurrency(Account account) { - return Currency.getInstance("EUR"); + AccountDto accountDto = accountApi.findByAccountNumber(account.getAccountNumber()).getBody(); + Objects.requireNonNull(accountDto); + return Currency.getInstance(accountDto.getCurrency()); } @Override public boolean isValid(Account account) { - return true; + AccountDto accountDto = accountApi.findByAccountNumber(account.getAccountNumber()).getBody(); + return accountDto != null; } @Override public boolean accountHasSufficientFunds(Account account, BigDecimal amount) { - return true; + BigDecimal currentBalance = publicBalanceApi.getBalance(account.getAccountNumber()).getBody(); + return Objects.requireNonNull(currentBalance).compareTo(amount) >= 0; } @Override - public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account, String information) { - + public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account) { + privateBalanceApi.addTransactionToBalance( + account.getAccountNumber(), + amount, + processUuid, + convertType(transactionType) + ); + } + + private cz.muni.pa165.banking.account.query.dto.TransactionType convertType(TransactionType type) { + return cz.muni.pa165.banking.account.query.dto.TransactionType.valueOf(type.name()); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ExchangeRateServiceImpl.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ExchangeRateServiceImpl.java index fb0786f00141256104b45ed72d121537d958b834..4a0c3cad66afc4f126bee51e2567ba3a30b300e9 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ExchangeRateServiceImpl.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ExchangeRateServiceImpl.java @@ -1,25 +1,34 @@ package cz.muni.pa165.banking.application.service; +import cz.muni.pa165.banking.application.proxy.rate.ExchangeRateResponseProcessor; +import cz.muni.pa165.banking.application.proxy.rate.ExchangeRatesApi; import cz.muni.pa165.banking.domain.money.exchange.ExchangeRateService; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.Currency; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Service public class ExchangeRateServiceImpl implements ExchangeRateService { - // TODO call external API containing current information about exchange rates -> Milestone2 - // either actual API containing real data, or custom 'mock' API containing static data - -// private final ExchangeRatesApi proxy; + private final ExchangeRatesApi exchangeRatesApi; + + Map<Currency, Map<Currency, BigDecimal>> exchangeRates = new ConcurrentHashMap<>(); -// public ExchangeRateServiceImpl(ExchangeRatesApi proxy) { -// this.proxy = proxy; -// } + public ExchangeRateServiceImpl(ExchangeRatesApi exchangeRatesApi) { + this.exchangeRatesApi = exchangeRatesApi; + } @Override public BigDecimal getRate(Currency base, Currency target) { - return BigDecimal.ONE; + if (!exchangeRates.containsKey(base)) { + Map<String, Object> response = exchangeRatesApi.getRatesOfCurrency(base.getCurrencyCode()); + exchangeRates.put(base, ExchangeRateResponseProcessor.process(response)); + } + + return exchangeRates.get(base).get(target); } + } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ProcessService.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ProcessService.java new file mode 100644 index 0000000000000000000000000000000000000000..bf193f1ceb45e75bd0fd2a841cad01a76802833f --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ProcessService.java @@ -0,0 +1,69 @@ +package cz.muni.pa165.banking.application.service; + +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.ProcessOperations; +import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.domain.process.status.StatusInformation; +import cz.muni.pa165.banking.exception.UnexpectedValueException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +public class ProcessService { + + private final ProcessRepository processRepository; + + public ProcessService(ProcessRepository processRepository) { + this.processRepository = processRepository; + } + + public Process findByUuid(UUID uuid) { + return processRepository.findById(uuid); + } + + @Transactional(readOnly = true) + public List<Process> processesWithStatus(Status status) { + return processRepository.findProcessOfStatus(status); + } + + @Transactional(readOnly = true) + public List<Process> unresolvedProcessesToDate(LocalDate threshold) { + List<Process> result = new ArrayList<>(); + + result.addAll(processRepository.findProcessesOfStatusUptoDate(Status.CREATED, threshold)); + result.addAll(processRepository.findProcessesOfStatusUptoDate(Status.PENDING, threshold)); + + return result; + } + + @Transactional + public List<UUID> resolveProcesses(LocalDate localDate) { + LocalDate now = LocalDate.now(); + if (!localDate.isBefore(now)) { + throw new UnexpectedValueException("Threshold should be atleast one day ago. Cannot resolve processes from today or the future"); + } + + List<Process> staleProcesses = new ArrayList<>(); + staleProcesses.addAll(processRepository.findProcessesOfStatusUptoDate(Status.CREATED, localDate)); + staleProcesses.addAll(processRepository.findProcessesOfStatusUptoDate(Status.PENDING, localDate)); + + StatusInformation resolvedInfo = new StatusInformation(Instant.now(), Status.FAILED, "Resolved stale process as FAILED by system"); + + List<UUID> resolved = new ArrayList<>(); + for (Process process : staleProcesses) { + try { + ProcessOperations.changeState(process, resolvedInfo); + processRepository.save(process); + resolved.add(process.getUuid()); + } catch (Exception ignored){} + } + + return resolved; + } +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ScheduledPaymentService.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ScheduledPaymentService.java new file mode 100644 index 0000000000000000000000000000000000000000..958ae04b3cde48317db1649a693ad5b163cb4d82 --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ScheduledPaymentService.java @@ -0,0 +1,85 @@ +package cz.muni.pa165.banking.application.service; + +import cz.muni.pa165.banking.account.management.AccountApi; +import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentDto; +import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentsDto; +import cz.muni.pa165.banking.domain.account.Account; +import cz.muni.pa165.banking.domain.messaging.MessageProducer; +import cz.muni.pa165.banking.domain.money.Money; +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.ProcessFactory; +import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; +import cz.muni.pa165.banking.domain.process.repository.ProcessTransactionRepository; +import cz.muni.pa165.banking.domain.transaction.Transaction; +import cz.muni.pa165.banking.domain.transaction.TransactionType; +import cz.muni.pa165.banking.exception.ServerError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.Currency; +import java.util.Objects; + +@Service +public class ScheduledPaymentService { + + private final Logger LOGGER = LoggerFactory.getLogger(ScheduledPaymentService.class); + + private final AccountApi accountApi; + + private final ProcessTransactionRepository processTransactionRepository; + + private final ProcessRepository processRepository; + + private final MessageProducer messageProducer; + + ScheduledPaymentService(AccountApi accountApi, + ProcessTransactionRepository processTransactionRepository, + ProcessRepository processRepository, + MessageProducer messageProducer) { + this.accountApi = accountApi; + this.processTransactionRepository = processTransactionRepository; + this.processRepository = processRepository; + this.messageProducer = messageProducer; + } + + @Scheduled(cron = "${scheduled-payments.cron.expression}") + public void autoExecute() { + LocalDate now = LocalDate.now(); + executeScheduledPayments(now); + } + + public void executeScheduledPayments(LocalDate date) { + ResponseEntity<ScheduledPaymentsDto> response = accountApi.getScheduledPaymentsOf(date); + if (!response.getStatusCode().is2xxSuccessful()) { + throw new ServerError("Call to Account Management service unsuccessful."); + } + + ProcessFactory factory = new ProcessFactory(processTransactionRepository, processRepository); + ScheduledPaymentsDto payments = Objects.requireNonNull(response.getBody()); + for (ScheduledPaymentDto payment : payments.getScheduledPayments()) { + Transaction newTransaction = transactionForScheduledPayment(payment); + Process process = factory.create(newTransaction, messageProducer); + LOGGER.info("[Create Scheduled Payment Process] %s" + process.getUuid()); + } + + LOGGER.info("Finished creating processes for scheduled payments."); + } + + private Transaction transactionForScheduledPayment(ScheduledPaymentDto payment) { + Account source = new Account(payment.getSenderAccount()); + Account target = new Account(payment.getReceiverAccount()); + Money money = new Money(payment.getAmount(), Currency.getInstance(payment.getCurrencyCode())); + return new Transaction( + source, + target, + TransactionType.SCHEDULED, + money, + "Automatic execution of scheduled payment" + ); + } + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/TransactionProcessesService.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/TransactionProcessesService.java index 193e9bb9990af42ff22dd616b0ff0a96c60c8825..c591a76735629e5410768bb638888367e30b3f54 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/TransactionProcessesService.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/TransactionProcessesService.java @@ -1,7 +1,6 @@ package cz.muni.pa165.banking.application.service; -import cz.muni.pa165.banking.application.messaging.ProcessProducer; -import cz.muni.pa165.banking.domain.money.Money; +import cz.muni.pa165.banking.domain.messaging.MessageProducer; import cz.muni.pa165.banking.domain.process.Process; import cz.muni.pa165.banking.domain.process.ProcessFactory; import cz.muni.pa165.banking.domain.process.ProcessTransaction; @@ -27,9 +26,11 @@ public class TransactionProcessesService { private final ProcessRepository processRepository; - private final ProcessProducer processProducer; + private final MessageProducer processProducer; - public TransactionProcessesService(ProcessTransactionRepository processTransactionRepository, ProcessRepository processRepository, ProcessProducer processProducer) { + public TransactionProcessesService(ProcessTransactionRepository processTransactionRepository, + ProcessRepository processRepository, + MessageProducer processProducer) { this.processTransactionRepository = processTransactionRepository; this.processRepository = processRepository; this.processProducer = processProducer; @@ -40,7 +41,7 @@ public class TransactionProcessesService { ProcessFactory factory = new ProcessFactory(processTransactionRepository, processRepository); Process process = factory.create(newTransaction, processProducer); - LOGGER.info("[Create Process] %s" + process.uuid()); + LOGGER.info("[Create Process] %s" + process.getUuid()); return process; } @@ -70,24 +71,22 @@ public class TransactionProcessesService { ProcessTransaction processTransaction = processTransactionRepository.findTransactionByProcessId(uuid); if (!processTransaction.getType().equals(TransactionType.TRANSFER) || processTransaction.getType().equals(TransactionType.SCHEDULED)) { - LOGGER.error("[Revert Process] Process " + uuid + " not of type CROSS_ACCOUNT/SCHEDULED, unable to revert"); - throw new UnexpectedValueException("Unable to revert transaction not type of CROSS_ACCOUNT or SCHEDULED!"); + LOGGER.error("[Revert Process] Process " + uuid + " not of type TRANSFER/SCHEDULED, unable to revert"); + throw new UnexpectedValueException("Unable to revert transaction not type of TRANSFER or SCHEDULED!"); } - Money original = processTransaction.getMoney(); - Money reverted = new Money(original.getAmount().negate(), original.getCurrency()); Transaction revertingTransaction = new Transaction( processTransaction.getTarget(), processTransaction.getSource(), - processTransaction.getType(), - reverted, + TransactionType.REFUND, + processTransaction.getMoney(), String.format("Admin reversal of executed %s transaction {%s}", processTransaction.getType(), uuid) ); ProcessFactory factory = new ProcessFactory(processTransactionRepository, processRepository); Process revertingProcess = factory.create(revertingTransaction, processProducer); - LOGGER.info(String.format("[Revert Process] Created new process {%s} in order to revert process {%s}", revertingProcess.uuid(), uuid)); + LOGGER.info(String.format("[Revert Process] Created new process {%s} in order to revert process {%s}", revertingProcess.getUuid(), uuid)); return revertingProcess; } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/account/Account.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/account/Account.java index 08fb772787a4ca38f2c8a3406f150da4bcaaf3cd..5be7d59855b014a9858b94c188033c86d822a6a5 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/account/Account.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/account/Account.java @@ -1,5 +1,7 @@ package cz.muni.pa165.banking.domain.account; +import java.util.Objects; + public class Account { private String accountNumber; @@ -19,4 +21,24 @@ public class Account { public void setAccountNumber(String accountNumber) { this.accountNumber = accountNumber; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Account account = (Account) o; + return Objects.equals(getAccountNumber(), account.getAccountNumber()); + } + + @Override + public int hashCode() { + return Objects.hash(getAccountNumber()); + } + + @Override + public String toString() { + return "Account{" + + "accountNumber='" + accountNumber + '\'' + + '}'; + } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/ProcessRequest.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/ProcessRequest.java index dece4563799f49937d07416f49319310d3a9adf4..f70a6996f917e6456f0c4f7ad87478139bd02037 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/ProcessRequest.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/ProcessRequest.java @@ -2,10 +2,24 @@ package cz.muni.pa165.banking.domain.messaging; import cz.muni.pa165.banking.domain.transaction.TransactionType; +import java.util.Objects; import java.util.UUID; public record ProcessRequest(UUID uuid, TransactionType type) { + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProcessRequest request = (ProcessRequest) o; + return Objects.equals(uuid, request.uuid) && type == request.type; + } + + @Override + public int hashCode() { + return Objects.hash(uuid, type); + } + @Override public String toString() { return "ProcessRequest{" + diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/money/Money.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/money/Money.java index d3382ff80c9e952180b88708516f238b0f63cfdf..e6dc93e83c15d6d735beb3616e3380b1105518bb 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/money/Money.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/money/Money.java @@ -2,6 +2,7 @@ package cz.muni.pa165.banking.domain.money; import java.math.BigDecimal; import java.util.Currency; +import java.util.Objects; public class Money { @@ -32,4 +33,25 @@ public class Money { public void setCurrency(String currencyCode) { this.currency = Currency.getInstance(currencyCode); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Money money = (Money) o; + return Objects.equals(getAmount(), money.getAmount()) && Objects.equals(getCurrency(), money.getCurrency()); + } + + @Override + public int hashCode() { + return Objects.hash(getAmount(), getCurrency()); + } + + @Override + public String toString() { + return "Money{" + + "amount=" + amount + + ", currency=" + currency + + '}'; + } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/Process.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/Process.java index 54a372aa205e15fe19e4b28fc64e59eea41c15dc..b9aae5b469307e295686230058e10daa6f590697 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/Process.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/Process.java @@ -2,43 +2,81 @@ package cz.muni.pa165.banking.domain.process; import cz.muni.pa165.banking.domain.process.status.Status; import cz.muni.pa165.banking.domain.process.status.StatusInformation; +import jakarta.persistence.Entity; import java.time.Instant; +import java.util.Objects; import java.util.UUID; -// @Entity +@Entity public class Process { - private final UUID uuid; + private UUID uuid; private StatusInformation currentStatus; - Process() { - uuid = UUID.randomUUID(); - currentStatus = new StatusInformation(Instant.now(), Status.CREATED, "Process created, waiting for processing."); + public static Process createNew() { + Process process = new Process(); + process.uuid = UUID.randomUUID(); + process.currentStatus = new StatusInformation(Instant.now(), Status.CREATED, "Process created, waiting for processing."); + + return process; } + @Deprecated // hibernate + public Process() {} + /** * Return a copy of Process UUID, ensuring the UUID is not modified or replaced. */ - public UUID uuid() { + public UUID getUuid() { return UUID.fromString(uuid.toString()); } + + @Deprecated // hibernate + public void setUuid(UUID uuid) { + this.uuid = uuid; + } public Instant getWhen() { - return currentStatus.when(); + return currentStatus.getWhen(); } public Status getStatus() { - return currentStatus.status(); + return currentStatus.getStatus(); } - public String getStatusInformation() { - return currentStatus.information(); + public String getInformation() { + return currentStatus.getInformation(); } - + + @Deprecated // hibernate + public StatusInformation getCurrentStatus() { + return currentStatus; + } + @Deprecated // hibernate void setCurrentStatus(StatusInformation information) { currentStatus = information; } - + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Process process = (Process) o; + return Objects.equals(getUuid(), process.getUuid()) && Objects.equals(getCurrentStatus(), process.getCurrentStatus()); + } + + @Override + public int hashCode() { + return Objects.hash(getUuid(), getCurrentStatus()); + } + + @Override + public String toString() { + return "Process{" + + "uuid=" + uuid + + ", currentStatus=" + currentStatus + + '}'; + } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessFactory.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessFactory.java index e59971349af79754a883328e7dce361f8bdd7a8b..66f2ff741138adfb85336467148984c02b384654 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessFactory.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessFactory.java @@ -23,7 +23,7 @@ public class ProcessFactory { public Process create(Transaction transaction, MessageProducer messageProducer) { - Process newProcess = new Process(); + Process newProcess = Process.createNew(); processRepository.save(newProcess); ProcessTransaction assignedTransaction = new ProcessTransaction( @@ -32,11 +32,11 @@ public class ProcessFactory { transaction.getType(), transaction.getMoney(), transaction.getDetail(), - newProcess.uuid() + newProcess.getUuid() ); processTransactionRepository.save(assignedTransaction); - messageProducer.send(new ProcessRequest(newProcess.uuid(), transaction.getType())); + messageProducer.send(new ProcessRequest(newProcess.getUuid(), transaction.getType())); return newProcess; } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessTransaction.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessTransaction.java index fd98d790e5c26965a11c0b6d19638520ce444ffb..44876ab7f4e390e6f33e061e35eb677cd98513cb 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessTransaction.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessTransaction.java @@ -6,6 +6,7 @@ import cz.muni.pa165.banking.domain.transaction.Transaction; import cz.muni.pa165.banking.domain.transaction.TransactionType; import jakarta.persistence.Entity; +import java.util.Objects; import java.util.UUID; @Entity @@ -36,4 +37,23 @@ public class ProcessTransaction extends Transaction { public void setUuid(UUID uuid) { this.uuid = uuid; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProcessTransaction that)) return false; + return Objects.equals(getUuid(), that.getUuid()); + } + + @Override + public int hashCode() { + return Objects.hash(getUuid()); + } + + @Override + public String toString() { + return "ProcessTransaction{" + + "uuid=" + uuid + + "} " + super.toString(); + } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/DepositHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/DepositHandler.java index aa9a66a41ee1d6487858dc5fe0e7b83caefb0c46..a6233dcd811ab6e814ef73475824e101a7533f0b 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/DepositHandler.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/DepositHandler.java @@ -27,7 +27,7 @@ class DepositHandler extends ProcessHandler { throw new UnexpectedValueException(String.format("Unable to deposit of provided currency (%s) as the account's currency is '%s'", money.getCurrency(), accountCurrency)); } - accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.DEPOSIT, money.getAmount(), account, processTransaction.getDetail()); + accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.DEPOSIT, money.getAmount(), account); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandler.java index d076f2fb03cddfd644224122c8656e08176e8c26..513795b3f840b40697ef19a3ff1ace3a0e110570 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandler.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandler.java @@ -58,11 +58,11 @@ abstract class ProcessHandler { if (process.getStatus().equals(Status.FAILED)) { throw new UnexpectedValueException( "Process already closed, ended with failure", - "Failure information: " + process.getStatusInformation() + "Failure information: " + process.getInformation() ); } if (process.getStatus().equals(Status.PROCESSED)) { - throw new UnexpectedValueException("Process already finalized, ended successfully", process.getStatusInformation()); + throw new UnexpectedValueException("Process already finalized, ended successfully", process.getInformation()); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerGateway.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerGateway.java index 5d5811c58eb8de1a929b11e9543a5487ae4ccf7c..44bc263be8d0920e8f35adc3738b5f9e5e2fb96d 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerGateway.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerGateway.java @@ -24,8 +24,9 @@ public class ProcessHandlerGateway { ProcessHandler handler = switch (type) { case WITHDRAWAL -> new WithdrawHandler(); case DEPOSIT -> new DepositHandler(); - case TRANSFER -> new CrossAccountHandler(); + case TRANSFER -> new TransferHandler(); case SCHEDULED -> new ScheduledHandler(); + case REFUND -> new RefundHandler(); }; handler.handle(processUuid, processRepository, processTransactionRepository, accountService, currencyConverter); diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/RefundHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/RefundHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..63775a546616c42231ffff4b81565d6969afe98e --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/RefundHandler.java @@ -0,0 +1,35 @@ +package cz.muni.pa165.banking.domain.process.handler; + + +import cz.muni.pa165.banking.domain.account.Account; +import cz.muni.pa165.banking.domain.money.CurrencyConverter; +import cz.muni.pa165.banking.domain.money.Money; +import cz.muni.pa165.banking.domain.process.ProcessTransaction; +import cz.muni.pa165.banking.domain.remote.AccountService; +import cz.muni.pa165.banking.domain.transaction.TransactionType; + +import java.math.BigDecimal; +import java.util.Currency; + +public class RefundHandler extends ProcessHandler { + + @Override + void evaluate(ProcessTransaction processTransaction, AccountService accountService, CurrencyConverter currencyConverter) { + Account source = processTransaction.getSource(); + validateAccount(source, accountService); + + Account target = processTransaction.getTarget(); + validateAccount(target, accountService); + + Money money = processTransaction.getMoney(); + Currency sourceAccountCurrency = accountService.getAccountCurrency(source); + BigDecimal sourceAmount = currencyConverter.convertTo(money.getCurrency(), sourceAccountCurrency, money.getAmount()); + + Currency targetAccountCurrency = accountService.getAccountCurrency(target); + BigDecimal targetAmount = currencyConverter.convertTo(money.getCurrency(), targetAccountCurrency, money.getAmount()); + targetAmount = targetAmount.multiply(BigDecimal.valueOf(-1L)); + + accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.REFUND, sourceAmount, source); + accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.REFUND, targetAmount, target); + } +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ScheduledHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ScheduledHandler.java index cdf8e167b43f2ef707b12905d9d5e5d924b79779..63cb2107ca1ad02240a64e31bb0a5d4db4b6fb4d 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ScheduledHandler.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ScheduledHandler.java @@ -14,7 +14,7 @@ import java.util.Currency; /** * Handler for transaction of type ${@link cz.muni.pa165.banking.domain.transaction.TransactionType#SCHEDULED}. * Customer may send a specified amount of money to a foreign account automatically by setting a scheduled payment. - * The implementation resembles ${@link CrossAccountHandler#evaluate} with minor variations. + * The implementation resembles ${@link TransferHandler#evaluate} with minor variations. */ public class ScheduledHandler extends ProcessHandler { @@ -46,8 +46,8 @@ public class ScheduledHandler extends ProcessHandler { targetAmount = currencyConverter.convertTo(currency, targetAccountCurrency, targetAmount); } - accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.SCHEDULED, sourceAmount, source, processTransaction.getDetail()); - accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.SCHEDULED, targetAmount, target, processTransaction.getDetail()); + accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.SCHEDULED, sourceAmount, source); + accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.SCHEDULED, targetAmount, target); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/CrossAccountHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/TransferHandler.java similarity index 91% rename from transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/CrossAccountHandler.java rename to transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/TransferHandler.java index 9dc80feb1e318d7a9472937190eee3380383cfaa..aaa1621895a7c50e27ba310ff46ec47af635fe11 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/CrossAccountHandler.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/TransferHandler.java @@ -17,7 +17,7 @@ import java.util.Currency; * not have to be the same. The amount of money is calculated via ${@link cz.muni.pa165.banking.domain.money.CurrencyConverter} * using the target account's currency. */ -public class CrossAccountHandler extends ProcessHandler { +public class TransferHandler extends ProcessHandler { @Override void evaluate(ProcessTransaction processTransaction, AccountService accountService, CurrencyConverter currencyConverter) { @@ -55,8 +55,8 @@ public class CrossAccountHandler extends ProcessHandler { targetAmount = currencyConverter.convertTo(currency, targetAccountCurrency, targetAmount); } - accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.TRANSFER, sourceAmount, source, processTransaction.getDetail()); - accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.TRANSFER, targetAmount, target, processTransaction.getDetail()); + accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.TRANSFER, sourceAmount, source); + accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.TRANSFER, targetAmount, target); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandler.java index d57c3bf9c9e0162a92977de2b8abbc4cbfc348aa..2f8ef3751acf38b41accd9aafa747831b6fd31ce 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandler.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandler.java @@ -40,7 +40,7 @@ public class WithdrawHandler extends ProcessHandler { BigDecimal calculatedAmount = money.getAmount().multiply(BigDecimal.valueOf(-1L)); - accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.WITHDRAWAL, calculatedAmount, account, processTransaction.getDetail()); + accountService.publishAccountChange(processTransaction.getUuid(), TransactionType.WITHDRAWAL, calculatedAmount, account); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/repository/ProcessRepository.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/repository/ProcessRepository.java index 12b56e54bbe890a148cbf3095ea7d3cb35f6b3ad..07a75bba7dbd6febb5906880e4fbac335ccbb9d7 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/repository/ProcessRepository.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/repository/ProcessRepository.java @@ -1,7 +1,10 @@ package cz.muni.pa165.banking.domain.process.repository; import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.status.Status; +import java.time.LocalDate; +import java.util.List; import java.util.UUID; public interface ProcessRepository { @@ -12,4 +15,8 @@ public interface ProcessRepository { void save(Process process); + List<Process> findProcessOfStatus(Status status); + + List<Process> findProcessesOfStatusUptoDate(Status status, LocalDate localDate); + } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/status/StatusInformation.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/status/StatusInformation.java index ddc7d46aefe93fe94dd396e26ff0bc911c693f28..713c83696a96c4a53a84c7382c7e8de24fe65621 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/status/StatusInformation.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/status/StatusInformation.java @@ -1,6 +1,70 @@ package cz.muni.pa165.banking.domain.process.status; +import jakarta.persistence.Embeddable; + import java.time.Instant; +import java.util.Objects; + +@Embeddable +public final class StatusInformation { + private Instant when; + private Status status; + private String information; + + public StatusInformation(Instant when, Status status, String information) { + this.when = when; + this.status = status; + this.information = information; + } + + @Deprecated // hibernate + public StatusInformation() {} + + public Instant getWhen() { + return when; + } + + public void setWhen(Instant when) { + this.when = when; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getInformation() { + return information; + } + + public void setInformation(String information) { + this.information = information; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (StatusInformation) obj; + return Objects.equals(this.when, that.when) && + Objects.equals(this.status, that.status) && + Objects.equals(this.information, that.information); + } + + @Override + public int hashCode() { + return Objects.hash(when, status, information); + } + + @Override + public String toString() { + return "StatusInformation[" + + "when=" + when + ", " + + "status=" + status + ", " + + "information=" + information + ']'; + } -public record StatusInformation(Instant when, Status status, String information) { } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/remote/AccountService.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/remote/AccountService.java index 010f2731127f4ef98981d9f4f745a227cf1aa6ff..37ebc5d9ef9464705b43946fa18e10199835cd02 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/remote/AccountService.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/remote/AccountService.java @@ -30,6 +30,6 @@ public interface AccountService { /** * Publish transaction results to update balance of account within processed transaction */ - void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account, String information); + void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account); } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/transaction/Transaction.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/transaction/Transaction.java index 35a7d1742f363bcd8753e546fbf1045b97e5b4c7..e363c572df66bd3a36dd8a4939b6818c7e846381 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/transaction/Transaction.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/transaction/Transaction.java @@ -3,6 +3,8 @@ package cz.muni.pa165.banking.domain.transaction; import cz.muni.pa165.banking.domain.account.Account; import cz.muni.pa165.banking.domain.money.Money; +import java.util.Objects; + public class Transaction { private Account source; @@ -20,7 +22,8 @@ public class Transaction { // Hibernate } - public Transaction(Account source, Account target, TransactionType type, Money amount, String detail) { + public Transaction(Account source, Account target, + TransactionType type, Money amount, String detail) { this.source = source; this.target = target; this.type = type; @@ -73,4 +76,26 @@ public class Transaction { this.detail = detail; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Transaction that)) return false; + return Objects.equals(getSource(), that.getSource()) && Objects.equals(getTarget(), that.getTarget()) && getType() == that.getType() && Objects.equals(getMoney(), that.getMoney()) && Objects.equals(getDetail(), that.getDetail()); + } + + @Override + public int hashCode() { + return Objects.hash(getSource(), getTarget(), getType(), getMoney(), getDetail()); + } + + @Override + public String toString() { + return "Transaction{" + + "source=" + source + + ", target=" + target + + ", type=" + type + + ", money=" + money + + ", detail='" + detail + '\'' + + '}'; + } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/transaction/TransactionType.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/transaction/TransactionType.java index 1158d407d47d69a9f84f45467fe6a2dba672f922..7b826bacbf8740d48f026a8145cb86c491d62670 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/transaction/TransactionType.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/transaction/TransactionType.java @@ -8,6 +8,8 @@ public enum TransactionType { TRANSFER, - SCHEDULED + SCHEDULED, + + REFUND } diff --git a/transaction-processor/src/main/resources/application.yaml b/transaction-processor/src/main/resources/application.yaml index 2dc85c373a9d07b3ebca98472350bbea9a9f0c20..19aeabac8d731275c9b84f87b86ef2103aada346 100644 --- a/transaction-processor/src/main/resources/application.yaml +++ b/transaction-processor/src/main/resources/application.yaml @@ -1,8 +1,12 @@ db: hostname: localhost -spring: - application: - name: Transaction-Processor +scheduled-payments: + cron: + expression: "00 7 * * *" +process: + resolution: + threshold: 7 + banking: apps: management: @@ -13,11 +17,19 @@ banking: host: localhost port: 8081 url: ${banking.apps.query.host}:${banking.apps.query.port} - + rates: + api-key: 5f40db3a892e886bda8a6967 + url: https://v6.exchangerate-api.com/v6/${banking.apps.rates.api-key} + initial: + currencies: CZK, EUR, USD + +spring: + application: + name: Transaction-Processor datasource: url: jdbc:postgresql://${db.hostname}:5432/banking driver-class-name: org.postgresql.Driver username: ACC_TRANSACTION password: transactionAccPasswd jpa: - mapping-resources: hibernate/Transaction.hbm.xml + mapping-resources: hibernate/Transaction.hbm.xml, hibernate/Process.hbm.xml diff --git a/transaction-processor/src/main/resources/hibernate/Process.hbm.xml b/transaction-processor/src/main/resources/hibernate/Process.hbm.xml new file mode 100644 index 0000000000000000000000000000000000000000..f810cd53ffad63338c2ec7bf3acbbd3ce8f4bb98 --- /dev/null +++ b/transaction-processor/src/main/resources/hibernate/Process.hbm.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE hibernate-mapping PUBLIC + "-//Hibernate/Hibernate Mapping DTD//EN" + "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> + +<hibernate-mapping> + <class name="cz.muni.pa165.banking.domain.process.Process" table="proc_reg"> + + <cache usage="read-write"/> + + <id name="uuid" + column="proc_uuid" + type="java.util.UUID"/> + + <component + name="currentStatus" + class="cz.muni.pa165.banking.domain.process.status.StatusInformation" + > + <property + name="when" + column="status_when" + type="java.time.Instant" + /> + <property + name="status" + column="status"> + <type name="org.hibernate.type.EnumType"> + <param name="enumClass">cz.muni.pa165.banking.domain.process.status.Status</param> + <param name="useNamed">true</param> + </type> + </property> + + <property + name="information" + column="status_info" + type="java.lang.String" + /> + </component> + + </class> + +</hibernate-mapping> \ No newline at end of file diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/application/integration/ProcessControllerIT.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/integration/ProcessControllerIT.java new file mode 100644 index 0000000000000000000000000000000000000000..c97aecfda45f64ae4984cfbb2fc1cb33e08cbf71 --- /dev/null +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/integration/ProcessControllerIT.java @@ -0,0 +1,103 @@ +package cz.muni.pa165.banking.application.integration; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.muni.pa165.banking.application.repository.ProcessRepositoryJpa; +import cz.muni.pa165.banking.application.service.ProcessService; +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.ProcessOperations; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.domain.process.status.StatusInformation; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.time.*; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureDataJpa +public class ProcessControllerIT { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ProcessService service; + + @Autowired + private ObjectMapper objectMapper; + + private static final Instant instant = LocalDateTime.of(2024, Month.APRIL, 24, 12, 0, 0).toInstant(ZoneOffset.UTC); + + @BeforeAll + public static void initDb(@Autowired ProcessRepositoryJpa repository) { + repository.save(Process.createNew()); + repository.save(Process.createNew()); + + Process processed1 = Process.createNew(); + ProcessOperations.changeState(processed1, new StatusInformation(instant, Status.PENDING, "PENDING")); + repository.save(processed1); + + Process processed2 = Process.createNew(); + ProcessOperations.changeState(processed2, new StatusInformation(instant, Status.PENDING, "PENDING")); + repository.save(processed2); + + LocalDateTime currentDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + LocalDateTime newDateTime = currentDateTime.minusDays(10); + Instant tenDaysAgo = newDateTime.toInstant(ZoneOffset.UTC); + + Process processed4 = Process.createNew(); + ProcessOperations.changeState(processed4, new StatusInformation(tenDaysAgo, Status.CREATED, "CREATED")); + repository.save(processed4); + + Process processed5 = Process.createNew(); + ProcessOperations.changeState(processed5, new StatusInformation(tenDaysAgo, Status.PENDING, "PENDING")); + repository.save(processed5); + } + + @Test + public void resolveStaleProcesses() throws Exception { + LocalDateTime currentDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + LocalDateTime newDateTime = currentDateTime.minusDays(1); + LocalDate yesterday = newDateTime.toLocalDate(); + + List<Process> staleProcesses = service.unresolvedProcessesToDate(yesterday); + for (Process process : staleProcesses) { + assertNotEquals(Status.FAILED, process.getStatus()); + } + List<UUID> staleProcessIds = staleProcesses.stream() + .map(Process::getUuid) + .toList(); + + MvcResult response = mockMvc.perform(patch("/processes/v1/resolve") + .param("date", yesterday.toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = response.getResponse().getContentAsString(); + List<UUID> resolvedProcessIds = objectMapper.readValue(jsonResponse, new TypeReference<>() {}); + assertEquals(staleProcessIds.size(), resolvedProcessIds.size()); + assertTrue(resolvedProcessIds.containsAll(staleProcessIds)); + + for (UUID uuid : resolvedProcessIds) { + Process process = service.findByUuid(uuid); + assertEquals(Status.FAILED, process.getStatus()); + assertEquals("Resolved stale process as FAILED by system", process.getInformation()); + } + } + +} diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryJpaTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryJpaTest.java new file mode 100644 index 0000000000000000000000000000000000000000..273a714cf1f48b1d72465b59d408418067d522ca --- /dev/null +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/repository/ProcessRepositoryJpaTest.java @@ -0,0 +1,94 @@ +package cz.muni.pa165.banking.application.repository; + +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.ProcessOperations; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.domain.process.status.StatusInformation; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.*; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@DataJpaTest +public class ProcessRepositoryJpaTest { + + @Autowired + private ProcessRepositoryJpa repository; + + private static final Instant instant = LocalDateTime.of(2024, Month.APRIL, 24, 12, 0, 0).toInstant(ZoneOffset.UTC); + + + @BeforeAll + public static void initDb(@Autowired ProcessRepositoryJpa repository) { + repository.save(Process.createNew()); + repository.save(Process.createNew()); + repository.save(Process.createNew()); + + Process processed1 = Process.createNew(); + ProcessOperations.changeState(processed1, new StatusInformation(instant, Status.PROCESSED, "done")); + repository.save(processed1); + + Process processed2 = Process.createNew(); + ProcessOperations.changeState(processed2, new StatusInformation(instant, Status.PROCESSED, "done")); + repository.save(processed2); + + Process processed3 = Process.createNew(); + ProcessOperations.changeState(processed3, new StatusInformation(instant, Status.PROCESSED, "done")); + repository.save(processed3); + + LocalDateTime currentDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + LocalDateTime newDateTime = currentDateTime.minusDays(1); + Instant yesterday = newDateTime.toInstant(ZoneOffset.UTC); + + Process processed4 = Process.createNew(); + ProcessOperations.changeState(processed4, new StatusInformation(yesterday, Status.PROCESSED, "done")); + repository.save(processed4); + + Process processed5 = Process.createNew(); + ProcessOperations.changeState(processed5, new StatusInformation(yesterday, Status.PROCESSED, "done")); + repository.save(processed5); + } + + @Test + public void findAllWithStatusCreated() { + List<Process> createdProcesses = repository.findAllWithStatus(Status.CREATED); + + assertEquals(3, createdProcesses.size()); + + Set<Status> statuses = createdProcesses.stream() + .map(Process::getStatus) + .collect(Collectors.toSet()); + assertEquals(1, statuses.size()); + assertTrue(statuses.contains(Status.CREATED)); + } + + @Test + public void findProcessedProcessesFromYesterday() { + LocalDateTime currentDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + LocalDateTime newDateTime = currentDateTime.minusDays(1); + Instant yesterday = newDateTime.toInstant(ZoneOffset.UTC); + + List<Process> processedYesterday = repository.findByStatusAndDateBeforeEqual(Status.PROCESSED, yesterday); + assertEquals(2, processedYesterday.size()); + } + + @Test + public void findCreatedProcessesFromYesterday() { + LocalDateTime currentDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + LocalDateTime newDateTime = currentDateTime.minusDays(1); + Instant yesterday = newDateTime.toInstant(ZoneOffset.UTC); + + List<Process> processedYesterday = repository.findByStatusAndDateBeforeEqual(Status.CREATED, yesterday); + assertEquals(0, processedYesterday.size()); + } +} diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/AccountServiceImplTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/AccountServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bf107f607d340ec80801e2f0cac54216146ab0e1 --- /dev/null +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/AccountServiceImplTest.java @@ -0,0 +1,89 @@ +package cz.muni.pa165.banking.application.service; + +import cz.muni.pa165.banking.account.management.AccountApi; +import cz.muni.pa165.banking.account.management.dto.AccountDto; +import cz.muni.pa165.banking.account.query.CustomerServiceApi; +import cz.muni.pa165.banking.account.query.SystemServiceApi; +import cz.muni.pa165.banking.domain.account.Account; +import cz.muni.pa165.banking.domain.transaction.TransactionType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AccountServiceImplTest { + + @Mock + private AccountApi accountApi; + + @Mock + private CustomerServiceApi publicBalanceApi; + + @Mock + private SystemServiceApi privateBalanceApi; + + @InjectMocks + private AccountServiceImpl accountService; + + @Test + void getAccountCurrency_shouldCallAccountApi() { + Account account = new Account("12345"); + AccountDto accountDto = new AccountDto(); + accountDto.setCurrency("USD"); + + when(accountApi.findByAccountNumber(account.getAccountNumber())).thenReturn(ResponseEntity.ok(accountDto)); + + accountService.getAccountCurrency(account); + + verify(accountApi, times(1)).findByAccountNumber(account.getAccountNumber()); + } + + @Test + void isValid_shouldCallAccountApi() { + Account account = new Account("12345"); + AccountDto accountDto = new AccountDto(); + + when(accountApi.findByAccountNumber(account.getAccountNumber())).thenReturn(ResponseEntity.ok(accountDto)); + + accountService.isValid(account); + + verify(accountApi, times(1)).findByAccountNumber(account.getAccountNumber()); + } + + @Test + void accountHasSufficientFunds_shouldCallPublicBalanceApi() { + Account account = new Account("12345"); + BigDecimal amount = BigDecimal.TEN; + + when(publicBalanceApi.getBalance(account.getAccountNumber())).thenReturn(ResponseEntity.ok(BigDecimal.ONE)); + + accountService.accountHasSufficientFunds(account, amount); + + verify(publicBalanceApi, times(1)).getBalance(account.getAccountNumber()); + } + + @Test + void publishAccountChange_shouldCallPrivateBalanceApi() { + UUID processUuid = UUID.randomUUID(); + TransactionType transactionType = TransactionType.DEPOSIT; + BigDecimal amount = BigDecimal.TEN; + Account account = new Account("12345"); + + accountService.publishAccountChange(processUuid, transactionType, amount, account); + + verify(privateBalanceApi, times(1)).addTransactionToBalance( + account.getAccountNumber(), + amount, + processUuid, + cz.muni.pa165.banking.account.query.dto.TransactionType.DEPOSIT + ); + } +} \ No newline at end of file diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/ProcessServiceTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/ProcessServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8d310cb391b8af76fda45208f88d1e6ce02afc76 --- /dev/null +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/ProcessServiceTest.java @@ -0,0 +1,87 @@ +package cz.muni.pa165.banking.application.service; + + +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.exception.UnexpectedValueException; +import org.awaitility.reflect.WhiteboxImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ProcessServiceTest { + + @Mock + private ProcessRepository processRepository; + + @InjectMocks + private ProcessService processService; + + @Test + void findByUuid_shouldCallProcessRepository() { + UUID uuid = UUID.randomUUID(); + + processService.findByUuid(uuid); + + verify(processRepository, times(1)).findById(uuid); + } + + @Test + void processesWithStatus_shouldCallProcessRepository() { + Status status = Status.CREATED; + + processService.processesWithStatus(status); + + verify(processRepository, times(1)).findProcessOfStatus(status); + } + + @Test + void unresolvedProcessesToDate_shouldCallProcessRepository() { + LocalDate threshold = LocalDate.now(); + + processService.unresolvedProcessesToDate(threshold); + + verify(processRepository, times(1)).findProcessesOfStatusUptoDate(Status.CREATED, threshold); + verify(processRepository, times(1)).findProcessesOfStatusUptoDate(Status.PENDING, threshold); + } + + @Test + void resolveProcesses_failOnInvalidThreshold() { + LocalDate localDate = LocalDate.now(); + + UnexpectedValueException e = assertThrows( + UnexpectedValueException.class, + () -> processService.resolveProcesses(localDate) + ); + + String cause = WhiteboxImpl.getInternalState(e, "cause"); + assertEquals("Threshold should be atleast one day ago. Cannot resolve processes from today or the future", cause); + } + + @Test + void resolveProcesses_shouldCallProcessRepository() { + LocalDate localDate = LocalDate.now().minusDays(2); + + Process p1 = Process.createNew(); + Process p2 = Process.createNew(); + when(processRepository.findProcessesOfStatusUptoDate(Status.CREATED, localDate)).thenReturn(List.of(p1)); + when(processRepository.findProcessesOfStatusUptoDate(Status.PENDING, localDate)).thenReturn(List.of(p2)); + + processService.resolveProcesses(localDate); + + verify(processRepository, times(2)).findProcessesOfStatusUptoDate(any(), any()); + verify(processRepository, times(1)).save(p1); + verify(processRepository, times(1)).save(p2); + } +} \ No newline at end of file diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/ScheduledPaymentServiceTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/ScheduledPaymentServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e64b6565a5a9a2cd009ef155c7e756b8b285b928 --- /dev/null +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/ScheduledPaymentServiceTest.java @@ -0,0 +1,84 @@ +package cz.muni.pa165.banking.application.service; + +import cz.muni.pa165.banking.account.management.AccountApi; +import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentDto; +import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentType; +import cz.muni.pa165.banking.account.management.dto.ScheduledPaymentsDto; +import cz.muni.pa165.banking.domain.messaging.MessageProducer; +import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; +import cz.muni.pa165.banking.domain.process.repository.ProcessTransactionRepository; +import cz.muni.pa165.banking.exception.ServerError; +import org.awaitility.reflect.WhiteboxImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ScheduledPaymentServiceTest { + + @Mock + private AccountApi accountApi; + + @Mock + private ProcessTransactionRepository processTransactionRepository; + + @Mock + private ProcessRepository processRepository; + + @Mock + private MessageProducer messageProducer; + + @InjectMocks + private ScheduledPaymentService scheduledPaymentService; + + @Test + void executeScheduledPayments_shouldCreateProcessesForScheduledPayments() { + LocalDate date = LocalDate.now(); + ScheduledPaymentsDto validScheduledPaymentsDto = new ScheduledPaymentsDto(); + + validScheduledPaymentsDto.addScheduledPaymentsItem( + new ScheduledPaymentDto(BigDecimal.ONE, "EUR", ScheduledPaymentType.MONTHLY, 1) + ); + validScheduledPaymentsDto.addScheduledPaymentsItem( + new ScheduledPaymentDto(BigDecimal.ONE, "EUR", ScheduledPaymentType.MONTHLY, 1) + ); + validScheduledPaymentsDto.addScheduledPaymentsItem( + new ScheduledPaymentDto(BigDecimal.ONE, "EUR", ScheduledPaymentType.MONTHLY, 1) + ); + + when(accountApi.getScheduledPaymentsOf(date)).thenReturn(ResponseEntity.ok(validScheduledPaymentsDto)); + + scheduledPaymentService.executeScheduledPayments(date); + + verify(processRepository, times(3)).save(any()); + verify(processTransactionRepository, times(3)).save(any()); + verify(messageProducer, times(3)).send(any()); + } + + @Test + void executeScheduledPayments_shouldThrowServerErrorForInvalidResponse() { + LocalDate date = LocalDate.now(); + + when(accountApi.getScheduledPaymentsOf(date)).thenReturn(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); + + ServerError err = assertThrows( + ServerError.class, + () -> scheduledPaymentService.executeScheduledPayments(date) + ); + String cause = WhiteboxImpl.getInternalState(err, "cause"); + assertEquals("Call to Account Management service unsuccessful.", cause); + + verify(processRepository, never()).save(any()); + } + +} \ No newline at end of file diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/TransactionProcessesServiceTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/TransactionProcessesServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6014746d3982a47b94226418c3bcafa6f05e7aa9 --- /dev/null +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/application/service/TransactionProcessesServiceTest.java @@ -0,0 +1,95 @@ +package cz.muni.pa165.banking.application.service; + +import cz.muni.pa165.banking.domain.account.Account; +import cz.muni.pa165.banking.domain.messaging.MessageProducer; +import cz.muni.pa165.banking.domain.money.Money; +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.ProcessOperations; +import cz.muni.pa165.banking.domain.process.ProcessTransaction; +import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; +import cz.muni.pa165.banking.domain.process.repository.ProcessTransactionRepository; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.domain.process.status.StatusInformation; +import cz.muni.pa165.banking.domain.transaction.Transaction; +import cz.muni.pa165.banking.domain.transaction.TransactionType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Currency; +import java.util.UUID; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TransactionProcessesServiceTest { + + @Mock + private ProcessTransactionRepository processTransactionRepository; + + @Mock + private ProcessRepository processRepository; + + @Mock + private MessageProducer processProducer; + + @InjectMocks + private TransactionProcessesService transactionProcessesService; + + @Test + void createProcessForTransaction_shouldCreateProcess() { + Transaction newTransaction = new Transaction(); + + transactionProcessesService.createProcessForTransaction(newTransaction); + + verify(processRepository, times(1)).save(any()); + verify(processTransactionRepository, times(1)).save(any()); + verify(processProducer, times(1)).send(any()); + } + + @Test + void findProcess_shouldCallProcessRepository() { + UUID uuid = UUID.randomUUID(); + + transactionProcessesService.findProcess(uuid); + + verify(processRepository, times(1)).findById(uuid); + } + + @Test + void findProcessTransaction_shouldCallProcessTransactionRepository() { + UUID uuid = UUID.randomUUID(); + + transactionProcessesService.findProcessTransaction(uuid); + + verify(processTransactionRepository, times(1)).findTransactionByProcessId(uuid); + } + + @Test + void revertProcess_shouldCreateRevertingProcess() { + UUID uuid = UUID.randomUUID(); + Process process = Process.createNew(); + ProcessOperations.changeState(process, new StatusInformation(Instant.now(), Status.PROCESSED, "done")); + ProcessTransaction processTransaction = new ProcessTransaction(); + processTransaction.setUuid(process.getUuid()); + processTransaction.setSource(new Account("123")); + processTransaction.setTarget(new Account("321")); + processTransaction.setType(TransactionType.TRANSFER); + processTransaction.setMoney(new Money(BigDecimal.ONE, Currency.getInstance("EUR"))); + Transaction revertingTransaction = new Transaction(); + + when(processRepository.findById(uuid)).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(uuid)).thenReturn(processTransaction); + + transactionProcessesService.revertProcess(uuid); + + verify(processRepository, times(1)).findById(any()); + verify(processTransactionRepository, times(1)).findTransactionByProcessId(any()); + verify(processRepository, times(1)).save(any()); + verify(processProducer, times(1)).send(any()); + } +} \ No newline at end of file diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessFactoryTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessFactoryTest.java index a1264aa501c86cd84b55ac2ec095a412123b2988..7994b660d5350a67863b10acb6465e44abb3b0f3 100644 --- a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessFactoryTest.java +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessFactoryTest.java @@ -95,7 +95,7 @@ class ProcessFactoryTest { verify(processTransactionRepository).save(argumentCaptor.capture()); ProcessTransaction captured = argumentCaptor.getValue(); - assertEquals(createdProcess.uuid(), captured.getUuid()); + assertEquals(createdProcess.getUuid(), captured.getUuid()); } @@ -103,23 +103,20 @@ class ProcessFactoryTest { void transactionRequestCreated() { ProcessTransactionRepository processTransactionRepository = mock(ProcessTransactionRepository.class); ProcessRepository processRepository = mock(ProcessRepository.class); - MessageProducer messageProducer = new MessageProducerSpy(); + MessageProducerSpy messageProducer = new MessageProducerSpy(); ProcessFactory processFactory = new ProcessFactory(processTransactionRepository, processRepository); Process createdProcess = processFactory.create(transaction, messageProducer); - UUID sentUuid = WhiteboxImpl.getInternalState(messageProducer, "receivedUuid"); - TransactionType sentType = WhiteboxImpl.getInternalState(messageProducer, "receivedType"); - - assertEquals(createdProcess.uuid(), sentUuid); - assertEquals(TransactionType.DEPOSIT, sentType); + assertEquals(createdProcess.getUuid(), messageProducer.receivedUuid); + assertEquals(TransactionType.DEPOSIT, messageProducer.receivedType); } - private class MessageProducerSpy implements MessageProducer { + private static class MessageProducerSpy implements MessageProducer { - private UUID receivedUuid; + public UUID receivedUuid; - private TransactionType receivedType; + public TransactionType receivedType; @Override public void send(ProcessRequest data) { diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessMock.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessMock.java index 8cbf9dee8a40fe311c5263311d9621267869694b..143d073017059023b096c261eafb6440749b067d 100644 --- a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessMock.java +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessMock.java @@ -1,9 +1,23 @@ package cz.muni.pa165.banking.domain.process; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.domain.process.status.StatusInformation; + +import java.time.Instant; +import java.util.UUID; + public class ProcessMock extends Process { + private final UUID uuid; + public ProcessMock() { super(); + this.uuid = UUID.randomUUID(); + this.setCurrentStatus(new StatusInformation(Instant.now(), Status.CREATED, "Process created, waiting for processing.")); } + @Override + public UUID getUuid() { + return this.uuid; + } } diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessOperationsTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessOperationsTest.java index 7505ecb9373589a9515a3e13f57a8ddb55e742da..61593a14e6f039ade5c177f81e79e3ffcb0e1601 100644 --- a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessOperationsTest.java +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/ProcessOperationsTest.java @@ -12,7 +12,7 @@ class ProcessOperationsTest { @Test void changeState() { - Process process = new Process(); + Process process = Process.createNew(); StatusInformation newStatus = new StatusInformation( Instant.now(), @@ -22,8 +22,8 @@ class ProcessOperationsTest { ProcessOperations.changeState(process, newStatus); - assertEquals(newStatus.when(), process.getWhen()); - assertEquals(newStatus.status(), process.getStatus()); - assertEquals(newStatus.information(), process.getStatusInformation()); + assertEquals(newStatus.getWhen(), process.getWhen()); + assertEquals(newStatus.getStatus(), process.getStatus()); + assertEquals(newStatus.getInformation(), process.getInformation()); } } \ No newline at end of file diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/DepositHandlerTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/DepositHandlerTest.java index 4bcddcc4a4a03b8533243e5b8a995d9d9ae39efc..da39d6016439a52512a3777f8494620de632044a 100644 --- a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/DepositHandlerTest.java +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/DepositHandlerTest.java @@ -31,27 +31,25 @@ import static org.mockito.Mockito.when; class DepositHandlerTest { private boolean published = false; - - private static Account account; + private static Process process; - private static ProcessTransaction processTransaction; private static ProcessRepository processRepository; private static ProcessTransactionRepository processTransactionRepository; private static ProcessHandler depositHandler; private static CurrencyConverter converter; @BeforeAll - static void init() { - account = new Account("ACC"); + static void init() { + Account account = new Account("ACC"); process = new ProcessMock(); - processTransaction = new ProcessTransaction(account, null, TransactionType.DEPOSIT, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); + ProcessTransaction processTransaction = new ProcessTransaction(account, null, TransactionType.DEPOSIT, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); depositHandler = new DepositHandler(); processRepository = mock(ProcessRepository.class); processTransactionRepository = mock(ProcessTransactionRepository.class); converter = new CurrencyConverterStub(null); - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); } @BeforeEach @@ -65,7 +63,7 @@ class DepositHandlerTest { assertThrows( EntityNotFoundException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) ); assertEquals(Status.FAILED, process.getStatus()); } @@ -76,7 +74,7 @@ class DepositHandlerTest { assertThrows( UnexpectedValueException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) ); assertEquals(Status.FAILED, process.getStatus()); } @@ -84,7 +82,7 @@ class DepositHandlerTest { @Test void depositMoneySuccessful() { AccountService accountService = new AccountServiceStub(true, Currency.getInstance("EUR")); - depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter); + depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter); assertEquals(Status.PROCESSED, process.getStatus()); assertTrue(published); @@ -129,7 +127,7 @@ class DepositHandlerTest { } @Override - public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account, String information) { + public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account) { published = true; } } diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerTest.java index fc420748d9e08a0c2f557c1f8b3b364ef55ba088..52263512cd53695dcc8ffcad06b42d8c74006272 100644 --- a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerTest.java +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerTest.java @@ -51,7 +51,7 @@ class ProcessHandlerTest { ProcessOperations.changeState(process, new StatusInformation(null, Status.FAILED, "N/A")); assertThrows( UnexpectedValueException.class, - () -> genericHandler.handle(process.uuid(), processRepository, null, null, null) + () -> genericHandler.handle(process.getUuid(), processRepository, null, null, null) ); } @@ -62,17 +62,17 @@ class ProcessHandlerTest { ProcessOperations.changeState(process, new StatusInformation(null, Status.PROCESSED, "N/A")); assertThrows( UnexpectedValueException.class, - () -> genericHandler.handle(process.uuid(), processRepository, null, null, null) + () -> genericHandler.handle(process.getUuid(), processRepository, null, null, null) ); } @Test void processNotLinkedToTransactionRequest() { Process process = createProcess(); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(null); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(null); assertThrows( EntityNotFoundException.class, - () -> genericHandler.handle(process.uuid(), processRepository, processTransactionRepository, null, null) + () -> genericHandler.handle(process.getUuid(), processRepository, processTransactionRepository, null, null) ); } @@ -90,7 +90,7 @@ class ProcessHandlerTest { assertThrows( RuntimeException.class, - () -> failingHandler.handle(process.uuid(), processRepository, processTransactionRepository, null, null) + () -> failingHandler.handle(process.getUuid(), processRepository, processTransactionRepository, null, null) ); assertEquals(Status.FAILED, process.getStatus()); @@ -101,20 +101,20 @@ class ProcessHandlerTest { Process process = createProcess(); ofProcess(process); - genericHandler.handle(process.uuid(), processRepository, processTransactionRepository, null, null); + genericHandler.handle(process.getUuid(), processRepository, processTransactionRepository, null, null); assertEquals(Status.PROCESSED, process.getStatus()); } private Process createProcess() { Process process = new ProcessMock(); - when(processRepository.findById(process.uuid())).thenReturn(process); + when(processRepository.findById(process.getUuid())).thenReturn(process); return process; } private ProcessTransaction ofProcess(Process process) { - ProcessTransaction result = new ProcessTransaction(new Account("ACC_1"), null, TransactionType.DEPOSIT, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(result); + ProcessTransaction result = new ProcessTransaction(new Account("ACC_1"), null, TransactionType.DEPOSIT, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(result); return result; } } \ No newline at end of file diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/ScheduledHandlerTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/ScheduledHandlerTest.java index 24b1013c4dc887699f8e18b10a2fa3a3618382a7..802a490381e0c9cbe6695c3c6d2975106f1fe919 100644 --- a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/ScheduledHandlerTest.java +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/ScheduledHandlerTest.java @@ -51,14 +51,14 @@ class ScheduledHandlerTest { void nonexistingFirstAccountValidation() { AccountService accountService = new AccountServiceStub(false, false,null, false); Process process = new ProcessMock(); - ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); assertThrows( EntityNotFoundException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) ); assertEquals(Status.FAILED, process.getStatus()); } @@ -67,14 +67,14 @@ class ScheduledHandlerTest { void twoSameAccountsValidation() { AccountService accountService = new AccountServiceStub(true, false,null, false); Process process = new ProcessMock(); - ProcessTransaction processTransaction = new ProcessTransaction(account1, account1, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account1, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); assertThrows( UnexpectedValueException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) ); assertEquals(Status.FAILED, process.getStatus()); } @@ -83,14 +83,14 @@ class ScheduledHandlerTest { void nonexistingSecondAccountValidation() { AccountService accountService = new AccountServiceStub(true, false,null, false); Process process = new ProcessMock(); - ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); assertThrows( EntityNotFoundException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) ); assertEquals(Status.FAILED, process.getStatus()); } @@ -99,12 +99,12 @@ class ScheduledHandlerTest { void crossAccountTransactionSuccessful() { AccountService accountService = new AccountServiceStub(true, true, null, true); Process process = new ProcessMock(); - ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); - depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter); + depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter); assertEquals(Status.PROCESSED, process.getStatus()); assertTrue(published1); @@ -157,7 +157,7 @@ class ScheduledHandlerTest { } @Override - public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account, String information) { + public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account) { if (account.equals(account1)) { published1 = true; } else { diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/CrossAccountHandlerTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/TransferHandlerTest.java similarity index 74% rename from transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/CrossAccountHandlerTest.java rename to transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/TransferHandlerTest.java index 2551bd4a91df78468d6ea6f7cb846545a19f1704..357e351209f18c0b39c46b19d9dfb32b0249d108 100644 --- a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/CrossAccountHandlerTest.java +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/TransferHandlerTest.java @@ -1,208 +1,207 @@ -package cz.muni.pa165.banking.domain.process.handler; - -import cz.muni.pa165.banking.domain.account.Account; -import cz.muni.pa165.banking.domain.money.CurrencyConverter; -import cz.muni.pa165.banking.domain.money.Money; -import cz.muni.pa165.banking.domain.money.exchange.ExchangeRateService; -import cz.muni.pa165.banking.domain.process.Process; -import cz.muni.pa165.banking.domain.process.ProcessMock; -import cz.muni.pa165.banking.domain.process.ProcessOperations; -import cz.muni.pa165.banking.domain.process.ProcessTransaction; -import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; -import cz.muni.pa165.banking.domain.process.repository.ProcessTransactionRepository; -import cz.muni.pa165.banking.domain.process.status.Status; -import cz.muni.pa165.banking.domain.process.status.StatusInformation; -import cz.muni.pa165.banking.domain.remote.AccountService; -import cz.muni.pa165.banking.domain.transaction.TransactionType; -import cz.muni.pa165.banking.exception.EntityNotFoundException; -import cz.muni.pa165.banking.exception.UnexpectedValueException; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.Currency; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class CrossAccountHandlerTest { - - private boolean published1 = false; - private boolean published2 = false; - - private static Account account1; - private static Account account2; - private static Process process; - private static ProcessTransaction processTransaction; - private static ProcessRepository processRepository; - private static ProcessTransactionRepository processTransactionRepository; - private static ProcessHandler depositHandler; - private static CurrencyConverter converter; - - @BeforeAll - static void init() { - account1 = new Account("ACC1"); - account2 = new Account("ACC2"); - process = new ProcessMock(); - processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); - depositHandler = new CrossAccountHandler(); - processRepository = mock(ProcessRepository.class); - processTransactionRepository = mock(ProcessTransactionRepository.class); - converter = new CurrencyConverterStub(null); - - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); - } - - @BeforeEach - void prepare() { - ProcessOperations.changeState(process, new StatusInformation(null, Status.CREATED, null)); - } - - @Test - void nonexistingFirstAccountValidation() { - AccountService accountService = new AccountServiceStub(false, false,null, false); - - assertThrows( - EntityNotFoundException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) - ); - assertEquals(Status.FAILED, process.getStatus()); - } - - - @Test - void nonexistingSecondAccountValidation() { - AccountService accountService = new AccountServiceStub(true, false,null, false); - - assertThrows( - EntityNotFoundException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) - ); - assertEquals(Status.FAILED, process.getStatus()); - } - - - @Test - void negativeAmountInTransaction() { - AccountService accountService = new AccountServiceStub(true, true, null, false); - Process process = new ProcessMock(); - ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE.negate(), Currency.getInstance("EUR")), "", process.uuid()); - - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); - - assertThrows( - UnexpectedValueException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) - ); - assertEquals(Status.FAILED, process.getStatus()); - } - - @Test - void zeroAmountInTransaction() { - AccountService accountService = new AccountServiceStub(true, true, null, false); - Process process = new ProcessMock(); - ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ZERO, Currency.getInstance("EUR")), "", process.uuid()); - - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); - - assertThrows( - UnexpectedValueException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) - ); - assertEquals(Status.FAILED, process.getStatus()); - } - - @Test - void insufficientBalanceForTransaction() { - AccountService accountService = new AccountServiceStub(true, true, null, false); - Process process = new ProcessMock(); - ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); - - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); - - assertThrows( - UnexpectedValueException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) - ); - assertEquals(Status.FAILED, process.getStatus()); - } - - @Test - void crossAccountTransactionSuccessful() { - AccountService accountService = new AccountServiceStub(true, true, null, true); - Process process = new ProcessMock(); - ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); - - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); - - depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter); - - assertEquals(Status.PROCESSED, process.getStatus()); - assertTrue(published1); - assertTrue(published2); - } - - - private static class CurrencyConverterStub extends CurrencyConverter { - - public CurrencyConverterStub(ExchangeRateService exchangeRateApi) { - super(exchangeRateApi); - } - - @Override - public BigDecimal convertTo(Currency source, Currency target, BigDecimal amount) { - return amount; - } - } - - private class AccountServiceStub implements AccountService { - - private final boolean isValid1; - private final boolean isValid2; - private final Currency currency; - private final boolean sufficientFunds; - - private AccountServiceStub(boolean isValid1, boolean isValid2, Currency currency, boolean sufficientFunds) { - this.isValid1 = isValid1; - this.isValid2 = isValid2; - this.currency = currency; - this.sufficientFunds = sufficientFunds; - } - - @Override - public Currency getAccountCurrency(Account account) { - return currency; - } - - @Override - public boolean isValid(Account account) { - if (account.equals(account1)) { - return isValid1; - } - return isValid2; - } - - @Override - public boolean accountHasSufficientFunds(Account account, BigDecimal amount) { - return sufficientFunds; - } - - @Override - public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account, String information) { - if (account.equals(account1)) { - published1 = true; - } else { - published2 = true; - } - } - } - +package cz.muni.pa165.banking.domain.process.handler; + +import cz.muni.pa165.banking.domain.account.Account; +import cz.muni.pa165.banking.domain.money.CurrencyConverter; +import cz.muni.pa165.banking.domain.money.Money; +import cz.muni.pa165.banking.domain.money.exchange.ExchangeRateService; +import cz.muni.pa165.banking.domain.process.Process; +import cz.muni.pa165.banking.domain.process.ProcessMock; +import cz.muni.pa165.banking.domain.process.ProcessOperations; +import cz.muni.pa165.banking.domain.process.ProcessTransaction; +import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; +import cz.muni.pa165.banking.domain.process.repository.ProcessTransactionRepository; +import cz.muni.pa165.banking.domain.process.status.Status; +import cz.muni.pa165.banking.domain.process.status.StatusInformation; +import cz.muni.pa165.banking.domain.remote.AccountService; +import cz.muni.pa165.banking.domain.transaction.TransactionType; +import cz.muni.pa165.banking.exception.EntityNotFoundException; +import cz.muni.pa165.banking.exception.UnexpectedValueException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.Currency; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TransferHandlerTest { + + private boolean published1 = false; + private boolean published2 = false; + + private static Account account1; + private static Account account2; + private static Process process; + private static ProcessRepository processRepository; + private static ProcessTransactionRepository processTransactionRepository; + private static ProcessHandler depositHandler; + private static CurrencyConverter converter; + + @BeforeAll + static void init() { + account1 = new Account("ACC1"); + account2 = new Account("ACC2"); + process = new ProcessMock(); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); + depositHandler = new TransferHandler(); + processRepository = mock(ProcessRepository.class); + processTransactionRepository = mock(ProcessTransactionRepository.class); + converter = new CurrencyConverterStub(null); + + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); + } + + @BeforeEach + void prepare() { + ProcessOperations.changeState(process, new StatusInformation(null, Status.CREATED, null)); + } + + @Test + void nonexistingFirstAccountValidation() { + AccountService accountService = new AccountServiceStub(false, false,null, false); + + assertThrows( + EntityNotFoundException.class, + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) + ); + assertEquals(Status.FAILED, process.getStatus()); + } + + + @Test + void nonexistingSecondAccountValidation() { + AccountService accountService = new AccountServiceStub(true, false,null, false); + + assertThrows( + EntityNotFoundException.class, + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) + ); + assertEquals(Status.FAILED, process.getStatus()); + } + + + @Test + void negativeAmountInTransaction() { + AccountService accountService = new AccountServiceStub(true, true, null, false); + Process process = new ProcessMock(); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE.negate(), Currency.getInstance("EUR")), "", process.getUuid()); + + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); + + assertThrows( + UnexpectedValueException.class, + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) + ); + assertEquals(Status.FAILED, process.getStatus()); + } + + @Test + void zeroAmountInTransaction() { + AccountService accountService = new AccountServiceStub(true, true, null, false); + Process process = new ProcessMock(); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ZERO, Currency.getInstance("EUR")), "", process.getUuid()); + + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); + + assertThrows( + UnexpectedValueException.class, + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) + ); + assertEquals(Status.FAILED, process.getStatus()); + } + + @Test + void insufficientBalanceForTransaction() { + AccountService accountService = new AccountServiceStub(true, true, null, false); + Process process = new ProcessMock(); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); + + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); + + assertThrows( + UnexpectedValueException.class, + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) + ); + assertEquals(Status.FAILED, process.getStatus()); + } + + @Test + void crossAccountTransactionSuccessful() { + AccountService accountService = new AccountServiceStub(true, true, null, true); + Process process = new ProcessMock(); + ProcessTransaction processTransaction = new ProcessTransaction(account1, account2, TransactionType.TRANSFER, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); + + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); + + depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter); + + assertEquals(Status.PROCESSED, process.getStatus()); + assertTrue(published1); + assertTrue(published2); + } + + + private static class CurrencyConverterStub extends CurrencyConverter { + + public CurrencyConverterStub(ExchangeRateService exchangeRateApi) { + super(exchangeRateApi); + } + + @Override + public BigDecimal convertTo(Currency source, Currency target, BigDecimal amount) { + return amount; + } + } + + private class AccountServiceStub implements AccountService { + + private final boolean isValid1; + private final boolean isValid2; + private final Currency currency; + private final boolean sufficientFunds; + + private AccountServiceStub(boolean isValid1, boolean isValid2, Currency currency, boolean sufficientFunds) { + this.isValid1 = isValid1; + this.isValid2 = isValid2; + this.currency = currency; + this.sufficientFunds = sufficientFunds; + } + + @Override + public Currency getAccountCurrency(Account account) { + return currency; + } + + @Override + public boolean isValid(Account account) { + if (account.equals(account1)) { + return isValid1; + } + return isValid2; + } + + @Override + public boolean accountHasSufficientFunds(Account account, BigDecimal amount) { + return sufficientFunds; + } + + @Override + public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account) { + if (account.equals(account1)) { + published1 = true; + } else { + published2 = true; + } + } + } + } \ No newline at end of file diff --git a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandlerTest.java b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandlerTest.java index 66dffd2694ae5ab84e2b57f3ab4ccd2a7adb16ef..10b13732189324ceaacbc04632e2644a8c7db682 100644 --- a/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandlerTest.java +++ b/transaction-processor/src/test/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandlerTest.java @@ -32,9 +32,7 @@ class WithdrawHandlerTest { private boolean published = false; - private static Account account; private static Process process; - private static ProcessTransaction processTransaction; private static ProcessRepository processRepository; private static ProcessTransactionRepository processTransactionRepository; private static ProcessHandler depositHandler; @@ -42,16 +40,16 @@ class WithdrawHandlerTest { @BeforeAll static void init() { - account = new Account("ACC"); + Account account = new Account("ACC"); process = new ProcessMock(); - processTransaction = new ProcessTransaction(account, null, TransactionType.WITHDRAWAL, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.uuid()); + ProcessTransaction processTransaction = new ProcessTransaction(account, null, TransactionType.WITHDRAWAL, new Money(BigDecimal.ONE, Currency.getInstance("EUR")), "", process.getUuid()); depositHandler = new WithdrawHandler(); processRepository = mock(ProcessRepository.class); processTransactionRepository = mock(ProcessTransactionRepository.class); converter = new CurrencyConverterStub(null); - when(processRepository.findById(process.uuid())).thenReturn(process); - when(processTransactionRepository.findTransactionByProcessId(process.uuid())).thenReturn(processTransaction); + when(processRepository.findById(process.getUuid())).thenReturn(process); + when(processTransactionRepository.findTransactionByProcessId(process.getUuid())).thenReturn(processTransaction); } @BeforeEach @@ -65,7 +63,7 @@ class WithdrawHandlerTest { assertThrows( EntityNotFoundException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) ); assertEquals(Status.FAILED, process.getStatus()); } @@ -76,7 +74,7 @@ class WithdrawHandlerTest { assertThrows( UnexpectedValueException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) ); assertEquals(Status.FAILED, process.getStatus()); } @@ -87,7 +85,7 @@ class WithdrawHandlerTest { assertThrows( UnexpectedValueException.class, - () -> depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter) + () -> depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter) ); assertEquals(Status.FAILED, process.getStatus()); } @@ -96,7 +94,7 @@ class WithdrawHandlerTest { void withdrawTransactionSuccessful() { AccountService accountService = new AccountServiceStub(true, Currency.getInstance("EUR"), true); - depositHandler.handle(process.uuid(), processRepository, processTransactionRepository, accountService, converter); + depositHandler.handle(process.getUuid(), processRepository, processTransactionRepository, accountService, converter); assertEquals(Status.PROCESSED, process.getStatus()); assertTrue(published); @@ -143,7 +141,7 @@ class WithdrawHandlerTest { } @Override - public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account, String information) { + public void publishAccountChange(UUID processUuid, TransactionType transactionType, BigDecimal amount, Account account) { published = true; } } diff --git a/transaction-processor/src/test/resources/application.properties b/transaction-processor/src/test/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..af598f20e5222b5689c61d4f36ff29ca5174aacb --- /dev/null +++ b/transaction-processor/src/test/resources/application.properties @@ -0,0 +1,6 @@ +spring.h2.console.enabled=true +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect \ No newline at end of file