From 29770dbe1e3c824133ec76134c12f7766157852b Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 23 Feb 2024 03:23:10 -0300 Subject: [PATCH 1/4] fix: adjusting error statement with 402 code --- .../rinha/controllers/CustomerController.java | 24 ++++++++++++------- .../github/rinha/services/AccountService.java | 1 + 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/github/rinha/controllers/CustomerController.java b/src/main/java/com/github/rinha/controllers/CustomerController.java index fece3dd..2c8c850 100644 --- a/src/main/java/com/github/rinha/controllers/CustomerController.java +++ b/src/main/java/com/github/rinha/controllers/CustomerController.java @@ -20,21 +20,27 @@ public CustomerController(AccountService accountService) { @GetMapping("/clientes/{id}/extrato") public Mono> getExtratoByClienteId(@PathVariable int id) { - return Mono.just(id) - .filterWhen(accountService::isValidCustomerId) - .flatMap(accountService::findStatementByCustomerId) + if (id < 1 || id > 5) { + return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); + } + + return accountService.findStatementByCustomerId(id) .map(ResponseEntity::ok) - .defaultIfEmpty(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); + .onErrorReturn(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); } @PostMapping("/clientes/{id}/transacoes") public Mono> transacionar(@PathVariable int id, @RequestBody TransactionRequest transaction) { - return Mono.just(id) - .filterWhen(accountService::isValidCustomerId) - .flatMap(unused -> accountService.isTransactionValid(transaction)) - .flatMap(clientId -> accountService.updateBalanceAndInsertTransaction(id, transaction.parseValueToInt(), transaction.tipo(), transaction.descricao())) + if (id < 1 || id > 5) { + return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); + } + + if (!transaction.isRequestValid()) { + return Mono.just(ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build()); + } + + return accountService.updateBalanceAndInsertTransaction(id, transaction.parseValueToInt(), transaction.tipo(), transaction.descricao()) .map(ResponseEntity::ok) - .switchIfEmpty(Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build())) .onErrorReturn(ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build()); } } diff --git a/src/main/java/com/github/rinha/services/AccountService.java b/src/main/java/com/github/rinha/services/AccountService.java index 77823ba..e25c8e5 100644 --- a/src/main/java/com/github/rinha/services/AccountService.java +++ b/src/main/java/com/github/rinha/services/AccountService.java @@ -23,6 +23,7 @@ public AccountService(AccountPersistence accountPersistence) { this.accountPersistence = accountPersistence; } + @Transactional(readOnly = true) public Mono findStatementByCustomerId(int id) { Customer customer = this.accountPersistence.findCustomerById(id); List transactions = this.accountPersistence.findTransactionsByCustomerId(id); From 0dd2cc3c6d8e566e70cd1fd899328f4eb3b7b8e3 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 23 Feb 2024 18:58:10 -0300 Subject: [PATCH 2/4] feat: improvement code and fix one bug that show incorrect balance --- pom.xml | 7 - script.sql | 18 +- .../rinha/controllers/AccountController.java | 52 ++++ .../rinha/controllers/CustomerController.java | 46 ---- .../java/com/github/rinha/domain/Account.java | 44 ++++ .../java/com/github/rinha/domain/Balance.java | 62 +++++ .../com/github/rinha/domain/Customer.java | 50 ---- .../com/github/rinha/domain/Statement.java | 41 +++ .../com/github/rinha/domain/Transact.java | 78 ++++++ .../com/github/rinha/domain/Transaction.java | 100 ++++---- .../github/rinha/domain/dtos/BalanceDTO.java | 21 -- .../github/rinha/domain/dtos/CustomerDTO.java | 4 - .../rinha/domain/dtos/StatementDTO.java | 13 - .../rinha/domain/dtos/TransactionRequest.java | 41 --- .../domain/dtos/TransactionResponse.java | 4 - .../exceptions/UnprocessableException.java | 5 - .../github/rinha/domain/utils/TimeUtils.java | 33 +++ .../rinha/persistence/AccountPersistence.java | 100 -------- .../github/rinha/services/AccountService.java | 89 ++++--- .../com/github/rinha/utils/ConstantsUtil.java | 18 -- .../com/github/rinha/AccountServiceTest.java | 233 ++++++++---------- .../github/rinha/CustomerControllerTest.java | 194 +++++++-------- 22 files changed, 612 insertions(+), 641 deletions(-) create mode 100644 src/main/java/com/github/rinha/controllers/AccountController.java delete mode 100644 src/main/java/com/github/rinha/controllers/CustomerController.java create mode 100644 src/main/java/com/github/rinha/domain/Account.java create mode 100644 src/main/java/com/github/rinha/domain/Balance.java delete mode 100644 src/main/java/com/github/rinha/domain/Customer.java create mode 100644 src/main/java/com/github/rinha/domain/Statement.java create mode 100644 src/main/java/com/github/rinha/domain/Transact.java delete mode 100644 src/main/java/com/github/rinha/domain/dtos/BalanceDTO.java delete mode 100644 src/main/java/com/github/rinha/domain/dtos/CustomerDTO.java delete mode 100644 src/main/java/com/github/rinha/domain/dtos/StatementDTO.java delete mode 100644 src/main/java/com/github/rinha/domain/dtos/TransactionRequest.java delete mode 100644 src/main/java/com/github/rinha/domain/dtos/TransactionResponse.java create mode 100644 src/main/java/com/github/rinha/domain/utils/TimeUtils.java delete mode 100644 src/main/java/com/github/rinha/persistence/AccountPersistence.java delete mode 100644 src/main/java/com/github/rinha/utils/ConstantsUtil.java diff --git a/pom.xml b/pom.xml index a61513c..ae59018 100644 --- a/pom.xml +++ b/pom.xml @@ -21,13 +21,6 @@ org.springframework.boot spring-boot-starter-webflux - - - com.fasterxml.jackson.core - jackson-core - 2.16.1 - - org.springframework.boot spring-boot-devtools diff --git a/script.sql b/script.sql index cd0667a..5784bff 100644 --- a/script.sql +++ b/script.sql @@ -1,22 +1,24 @@ -CREATE TABLE IF NOT EXISTS CUSTOMER +CREATE TABLE IF NOT EXISTS account ( id SERIAL PRIMARY KEY, max_limit INT NOT NULL, balance INT NOT NULL ); -CREATE TABLE IF NOT EXISTS CUSTOMER_TRANSACTION +CREATE TABLE IF NOT EXISTS transactions ( id SERIAL PRIMARY KEY, - id_customer INTEGER NOT NULL REFERENCES CUSTOMER, + id_account INTEGER NOT NULL REFERENCES account, amount INT NOT NULL, kind VARCHAR(1) NOT NULL, description VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL ); -INSERT INTO CUSTOMER (max_limit, balance) VALUES (100000, 0); -INSERT INTO CUSTOMER (max_limit, balance) VALUES (80000, 0); -INSERT INTO CUSTOMER (max_limit, balance) VALUES (1000000, 0); -INSERT INTO CUSTOMER (max_limit, balance) VALUES (10000000, 0); -INSERT INTO CUSTOMER (max_limit, balance) VALUES (500000, 0); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_account_transaction_created_at ON transactions (created_at); + +INSERT INTO account (max_limit, balance) VALUES (100000, 0); +INSERT INTO account (max_limit, balance) VALUES (80000, 0); +INSERT INTO account (max_limit, balance) VALUES (1000000, 0); +INSERT INTO account (max_limit, balance) VALUES (10000000, 0); +INSERT INTO account (max_limit, balance) VALUES (500000, 0); \ No newline at end of file diff --git a/src/main/java/com/github/rinha/controllers/AccountController.java b/src/main/java/com/github/rinha/controllers/AccountController.java new file mode 100644 index 0000000..1de8330 --- /dev/null +++ b/src/main/java/com/github/rinha/controllers/AccountController.java @@ -0,0 +1,52 @@ +package com.github.rinha.controllers; + +import com.github.rinha.domain.Account; +import com.github.rinha.domain.Statement; +import com.github.rinha.domain.Transact; +import com.github.rinha.services.AccountService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +public class AccountController { + + private final AccountService accountService; + + public AccountController(AccountService accountService) { + this.accountService = accountService; + } + + @GetMapping("/clientes/{id}/extrato") + public Mono> retrieveStatement(@PathVariable int id) { + if (id < 1 || id > 5) { + return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); + } + + return accountService.retrieveStatement(id) + .map(ResponseEntity::ok) + .onErrorReturn(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); + } + + @PostMapping("/clientes/{id}/transacoes") + public Mono> transact(@PathVariable int id, + @RequestBody Transact transact) { + if (id < 1 || id > 5) { + return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); + } + + if (!transact.isRequestValid()) { + return Mono.just(ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build()); + } + + return accountService.processTransaction(id, transact.parseValueToInt(), transact.getKind(), + transact.getDescription()) + .map(ResponseEntity::ok) + .onErrorReturn(ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build()); + } +} diff --git a/src/main/java/com/github/rinha/controllers/CustomerController.java b/src/main/java/com/github/rinha/controllers/CustomerController.java deleted file mode 100644 index 2c8c850..0000000 --- a/src/main/java/com/github/rinha/controllers/CustomerController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.rinha.controllers; - -import com.github.rinha.domain.dtos.CustomerDTO; -import com.github.rinha.domain.dtos.StatementDTO; -import com.github.rinha.domain.dtos.TransactionRequest; -import com.github.rinha.services.AccountService; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import reactor.core.publisher.Mono; - -@RestController -public class CustomerController { - - private final AccountService accountService; - - public CustomerController(AccountService accountService) { - this.accountService = accountService; - } - - @GetMapping("/clientes/{id}/extrato") - public Mono> getExtratoByClienteId(@PathVariable int id) { - if (id < 1 || id > 5) { - return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); - } - - return accountService.findStatementByCustomerId(id) - .map(ResponseEntity::ok) - .onErrorReturn(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); - } - - @PostMapping("/clientes/{id}/transacoes") - public Mono> transacionar(@PathVariable int id, @RequestBody TransactionRequest transaction) { - if (id < 1 || id > 5) { - return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); - } - - if (!transaction.isRequestValid()) { - return Mono.just(ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build()); - } - - return accountService.updateBalanceAndInsertTransaction(id, transaction.parseValueToInt(), transaction.tipo(), transaction.descricao()) - .map(ResponseEntity::ok) - .onErrorReturn(ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build()); - } -} diff --git a/src/main/java/com/github/rinha/domain/Account.java b/src/main/java/com/github/rinha/domain/Account.java new file mode 100644 index 0000000..e2d82bf --- /dev/null +++ b/src/main/java/com/github/rinha/domain/Account.java @@ -0,0 +1,44 @@ +package com.github.rinha.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Account { + + @JsonProperty("limite") + private int maxLimit; + + @JsonProperty("saldo") + private int balance; + + public Account() { + } + + public Account(int maxLimit, int balance) { + this.maxLimit = maxLimit; + this.balance = balance; + } + + public boolean hasEnoughBalance(int valor) { + return this.balance + this.maxLimit <= valor; + } + + public int getMaxLimit() { + return maxLimit; + } + + public void setMaxLimit(int maxLimit) { + this.maxLimit = maxLimit; + } + + public int getBalance() { + return balance; + } + + public void setBalance(int balance) { + this.balance = balance; + } +} diff --git a/src/main/java/com/github/rinha/domain/Balance.java b/src/main/java/com/github/rinha/domain/Balance.java new file mode 100644 index 0000000..e454c86 --- /dev/null +++ b/src/main/java/com/github/rinha/domain/Balance.java @@ -0,0 +1,62 @@ +package com.github.rinha.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.rinha.domain.utils.TimeUtils; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Balance { + + @JsonProperty("total") + private int amount; + + @JsonProperty("data_extrato") + private String date; + + @JsonProperty("limite") + private int limit; + + public Balance() { + } + + public Balance(int amount, int limit) { + this.amount = amount; + this.date = TimeUtils.nowUTCToString(); + this.limit = limit; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public int getLimit() { + return limit; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + @Override + public String toString() { + return "{" + + "\"amount\"=" + amount + + ", \"date\"='" + date + '\'' + + ", \"limit\"=" + limit + + '}'; + } +} diff --git a/src/main/java/com/github/rinha/domain/Customer.java b/src/main/java/com/github/rinha/domain/Customer.java deleted file mode 100644 index 640801d..0000000 --- a/src/main/java/com/github/rinha/domain/Customer.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.rinha.domain; - -public class Customer { - - private int id; - private int maxLimit; - private int balance; - - public Customer() { - } - - public Customer(int maxLimit, int balance) { - this.maxLimit = maxLimit; - this.balance = balance; - } - - public Customer(int id, int maxLimit, int balance) { - this.id = id; - this.maxLimit = maxLimit; - this.balance = balance; - } - - public boolean hasEnoughBalance(int valor) { - return this.balance + this.maxLimit <= valor; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public int getMaxLimit() { - return maxLimit; - } - - public void setMaxLimit(int maxLimit) { - this.maxLimit = maxLimit; - } - - public int getBalance() { - return balance; - } - - public void setBalance(int balance) { - this.balance = balance; - } -} diff --git a/src/main/java/com/github/rinha/domain/Statement.java b/src/main/java/com/github/rinha/domain/Statement.java new file mode 100644 index 0000000..7cbd6d3 --- /dev/null +++ b/src/main/java/com/github/rinha/domain/Statement.java @@ -0,0 +1,41 @@ +package com.github.rinha.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Statement { + + @JsonProperty("saldo") + private Balance saldo; + + @JsonProperty("ultimas_transacoes") + private List ultimasTransacoes; + + public Statement() { + } + + public Statement(Balance saldo, List ultimasTransacoes) { + this.saldo = saldo; + this.ultimasTransacoes = ultimasTransacoes; + } + + public Balance getSaldo() { + return saldo; + } + + public void setSaldo(Balance saldo) { + this.saldo = saldo; + } + + public List getUltimasTransacoes() { + return ultimasTransacoes; + } + + public void setUltimasTransacoes(List ultimasTransacoes) { + this.ultimasTransacoes = ultimasTransacoes; + } +} diff --git a/src/main/java/com/github/rinha/domain/Transact.java b/src/main/java/com/github/rinha/domain/Transact.java new file mode 100644 index 0000000..a4f2f83 --- /dev/null +++ b/src/main/java/com/github/rinha/domain/Transact.java @@ -0,0 +1,78 @@ +package com.github.rinha.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.logging.log4j.util.Strings; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Transact { + + @JsonProperty("valor") + private String value; + + @JsonProperty("tipo") + private String kind; + + @JsonProperty("descricao") + private String description; + + public Transact() { + } + + public Transact(String value, String kind, String description) { + this.value = value; + this.kind = kind; + this.description = description; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isValueValid() { + try { + return Strings.isNotBlank(this.value) && Integer.parseInt(this.value) > 0; + } catch (NumberFormatException e) { + return false; + } + } + + public boolean isTypeValid() { + return Strings.isNotBlank(this.kind) && (this.kind.equals("c") || this.kind.equals("d")); + } + + public boolean isDescriptionValid() { + return Strings.isNotBlank(this.description) && this.description.length() <= 10; + } + + public int parseValueToInt() { + return Integer.parseInt(this.value); + } + + public boolean isRequestValid() { + return this.isValueValid() && this.isTypeValid() && this.isDescriptionValid(); + } + +} diff --git a/src/main/java/com/github/rinha/domain/Transaction.java b/src/main/java/com/github/rinha/domain/Transaction.java index 71868c3..5e61acf 100644 --- a/src/main/java/com/github/rinha/domain/Transaction.java +++ b/src/main/java/com/github/rinha/domain/Transaction.java @@ -1,74 +1,64 @@ package com.github.rinha.domain; -import java.time.LocalDateTime; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public class Transaction { - private int id; + @JsonProperty("valor") + private int amount; - private Customer customer; + @JsonProperty("tipo") + private String kind; - private int value; + @JsonProperty("descricao") + private String description; - private String type; + @JsonProperty("realizada_em") + private String createdAt; - private String description; + public Transaction() { + } - private LocalDateTime createdAt; + public Transaction(int amount, String kind, String description, String createdAt) { + this.amount = amount; + this.kind = kind; + this.description = description; + this.createdAt = createdAt; + } - public int getId() { - return id; - } + public int getAmount() { + return amount; + } - public void setId(int id) { - this.id = id; - } + public void setAmount(int amount) { + this.amount = amount; + } - public Customer getCliente() { - return customer; - } + public String getKind() { + return kind; + } - public void setCliente(Customer customer) { - this.customer = customer; - } + public void setKind(String kind) { + this.kind = kind; + } - public Customer getCustomer() { - return customer; - } + public String getDescription() { + return description; + } - public void setCustomer(Customer customer) { - this.customer = customer; - } + public void setDescription(String description) { + this.description = description; + } - public int getValue() { - return value; - } + public String getCreatedAt() { + return createdAt; + } - public void setValue(int value) { - this.value = value; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } } diff --git a/src/main/java/com/github/rinha/domain/dtos/BalanceDTO.java b/src/main/java/com/github/rinha/domain/dtos/BalanceDTO.java deleted file mode 100644 index c3b7a10..0000000 --- a/src/main/java/com/github/rinha/domain/dtos/BalanceDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.rinha.domain.dtos; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.rinha.utils.ConstantsUtil; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; - -public record BalanceDTO(int total, @JsonProperty("data_extrato") String dataExtrato, int limite) { - - public BalanceDTO(int total, int limite) { - this(total, formatDataExtrato(), limite); - } - - private static String formatDataExtrato() { - return LocalDateTime.now() - .atZone(ZoneId.of(ConstantsUtil.TIME_ZONE)) - .format(DateTimeFormatter.ofPattern(ConstantsUtil.DATE_FORMAT)); - } -} diff --git a/src/main/java/com/github/rinha/domain/dtos/CustomerDTO.java b/src/main/java/com/github/rinha/domain/dtos/CustomerDTO.java deleted file mode 100644 index bef082a..0000000 --- a/src/main/java/com/github/rinha/domain/dtos/CustomerDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.rinha.domain.dtos; - -public record CustomerDTO(int limite, int saldo) { -} diff --git a/src/main/java/com/github/rinha/domain/dtos/StatementDTO.java b/src/main/java/com/github/rinha/domain/dtos/StatementDTO.java deleted file mode 100644 index 5d7fe1a..0000000 --- a/src/main/java/com/github/rinha/domain/dtos/StatementDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.rinha.domain.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.github.rinha.domain.Customer; - -import java.util.List; - -public record StatementDTO(BalanceDTO saldo, @JsonInclude(JsonInclude.Include.NON_NULL) List ultimas_transacoes) { - - public StatementDTO(Customer customer, List transacoes) { - this(new BalanceDTO(customer.getBalance(), customer.getMaxLimit()), transacoes); - } -} diff --git a/src/main/java/com/github/rinha/domain/dtos/TransactionRequest.java b/src/main/java/com/github/rinha/domain/dtos/TransactionRequest.java deleted file mode 100644 index af6f341..0000000 --- a/src/main/java/com/github/rinha/domain/dtos/TransactionRequest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.rinha.domain.dtos; - -import com.github.rinha.utils.ConstantsUtil; -import org.apache.logging.log4j.util.Strings; - -public record TransactionRequest(String valor, String tipo, String descricao) { - - public boolean isValid() { - return this.valor != null && this.tipo != null && this.descricao != null; - } - - public boolean isValueValid() { - if(Strings.isBlank(this.valor)) return false; - - try { - return Integer.parseInt(this.valor) > 0; - } catch (NumberFormatException e) { - return false; - } - } - - public boolean isTypeValid() { - if(Strings.isBlank(this.tipo)) return false; - - return this.tipo.equals(ConstantsUtil.TRANSACTION_TYPE_CREDIT) || this.tipo.equals(ConstantsUtil.TRANSACTION_TYPE_DEBIT); - } - - public boolean isDescriptionValid() { - if (Strings.isBlank(this.descricao)) return false; - - return this.descricao.length() <= 10; - } - - public boolean isRequestValid() { - return this.isValid() && this.isValueValid() && this.isTypeValid() && this.isDescriptionValid(); - } - - public int parseValueToInt() { - return Integer.parseInt(this.valor); - } -} diff --git a/src/main/java/com/github/rinha/domain/dtos/TransactionResponse.java b/src/main/java/com/github/rinha/domain/dtos/TransactionResponse.java deleted file mode 100644 index c0a8fc6..0000000 --- a/src/main/java/com/github/rinha/domain/dtos/TransactionResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.rinha.domain.dtos; - -public record TransactionResponse(int valor, String tipo, String descricao, String realizada_em) { -} diff --git a/src/main/java/com/github/rinha/domain/exceptions/UnprocessableException.java b/src/main/java/com/github/rinha/domain/exceptions/UnprocessableException.java index 917a7ae..c3e1936 100644 --- a/src/main/java/com/github/rinha/domain/exceptions/UnprocessableException.java +++ b/src/main/java/com/github/rinha/domain/exceptions/UnprocessableException.java @@ -1,10 +1,5 @@ package com.github.rinha.domain.exceptions; -import com.github.rinha.utils.ConstantsUtil; - public class UnprocessableException extends RuntimeException { - public UnprocessableException() { - super(ConstantsUtil.INVALID_REQUEST); - } } diff --git a/src/main/java/com/github/rinha/domain/utils/TimeUtils.java b/src/main/java/com/github/rinha/domain/utils/TimeUtils.java new file mode 100644 index 0000000..dff97d1 --- /dev/null +++ b/src/main/java/com/github/rinha/domain/utils/TimeUtils.java @@ -0,0 +1,33 @@ +package com.github.rinha.domain.utils; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class TimeUtils { + + private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"; + + private static final String TIME_ZONE = "UTC"; + + private TimeUtils() { + throw new IllegalStateException("Utility class"); + } + + public static String formatToString(Timestamp timestamp) { + return timestamp.toLocalDateTime() + .atZone(ZoneId.of(TIME_ZONE)) + .format(DateTimeFormatter.ofPattern(DATE_FORMAT)); + } + + public static String nowUTCToString() { + return LocalDateTime.now() + .atZone(ZoneId.of("UTC")) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ")); + } + + public static Timestamp now() { + return Timestamp.valueOf(LocalDateTime.now().atZone(ZoneId.of(TIME_ZONE)).toLocalDateTime()); + } +} diff --git a/src/main/java/com/github/rinha/persistence/AccountPersistence.java b/src/main/java/com/github/rinha/persistence/AccountPersistence.java deleted file mode 100644 index 6e0e67e..0000000 --- a/src/main/java/com/github/rinha/persistence/AccountPersistence.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.github.rinha.persistence; - -import com.github.rinha.domain.Customer; -import com.github.rinha.domain.dtos.TransactionResponse; -import com.github.rinha.utils.ConstantsUtil; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; - -import javax.sql.DataSource; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.List; - -@Service -public class AccountPersistence { - - private static final String CUSTOMER_ID_SQL = "id"; - private static final String CUSTOMER_MAX_LIMIT_SQL = "max_limit"; - private static final String CUSTOMER_BALANCE_SQL = "balance"; - - private static final String TRANSACTION_VALUE_SQL = "amount"; - private static final String TRANSACTION_TYPE_SQL = "kind"; - private static final String TRANSACTION_DESCRIPTION_SQL = "description"; - private static final String TRANSACTION_DATE_SQL = "created_at"; - - private static final String FIND_CUSTOMER_BY_ID_QUERY = "SELECT id, max_limit, balance FROM CUSTOMER WHERE id = ?"; - private static final String FIND_CUSTOMER_BY_ID_FOR_UPDATE_QUERY = "SELECT id, max_limit, balance FROM CUSTOMER WHERE id = ? FOR UPDATE"; - private static final String UPDATE_CUSTOMER_BALANCE_QUERY = "UPDATE CUSTOMER SET balance = ? WHERE id = ?"; - private static final String INSERT_TRANSACTION_QUERY = "INSERT INTO CUSTOMER_TRANSACTION (id_customer, amount, kind, description, created_at) VALUES (?, ?, ?, ?, ?)"; - private static final String FIND_LAST_10_TRANSACTION_BY_CUSTOMER_ID_QUERY = "SELECT amount, kind, description, created_at FROM CUSTOMER_TRANSACTION WHERE id_customer = ? ORDER BY created_at DESC LIMIT 10"; - private static final String EXIST_CUSTOMER_BY_ID_QUERY = "SELECT EXISTS(SELECT 1 FROM CUSTOMER WHERE id = ?)"; - - private final JdbcTemplate jdbcTemplate; - - public AccountPersistence(DataSource dataSource) { - this.jdbcTemplate = new JdbcTemplate(dataSource); - } - - public Customer findCustomerById(int id) { - return this.jdbcTemplate.queryForObject( - FIND_CUSTOMER_BY_ID_QUERY, - new Object[]{id}, - (rs, rowNum) -> new Customer( - rs.getInt(CUSTOMER_ID_SQL), - rs.getInt(CUSTOMER_MAX_LIMIT_SQL), - rs.getInt(CUSTOMER_BALANCE_SQL) - ) - ); - } - - public List findTransactionsByCustomerId(int id) { - return this.jdbcTemplate.query(FIND_LAST_10_TRANSACTION_BY_CUSTOMER_ID_QUERY, new Object[]{id}, - (rs, rowNum) -> new TransactionResponse( - rs.getInt(TRANSACTION_VALUE_SQL), - rs.getString(TRANSACTION_TYPE_SQL), - rs.getString(TRANSACTION_DESCRIPTION_SQL), - formatDateToString(rs.getTimestamp(TRANSACTION_DATE_SQL)) - ) - ); - } - - private static String formatDateToString(Timestamp timestamp) { - return timestamp.toLocalDateTime() - .atZone(ZoneId.of(ConstantsUtil.TIME_ZONE)) - .format(DateTimeFormatter.ofPattern(ConstantsUtil.DATE_FORMAT)); - } - - public void updateBalanceAndInsertTransaction(int customerId, int newBalance, int value, String transactionType, String description) { - insertTransaction(customerId, value, transactionType, description); - updateBalance(customerId, newBalance); - } - - public Customer findCustomerForUpdate(int id) { - return this.jdbcTemplate.queryForObject(FIND_CUSTOMER_BY_ID_FOR_UPDATE_QUERY, new Object[]{id}, - (rs, rowNum) -> new Customer( - rs.getInt(CUSTOMER_ID_SQL), - rs.getInt(CUSTOMER_MAX_LIMIT_SQL), - rs.getInt(CUSTOMER_BALANCE_SQL) - ) - ); - } - - private void updateBalance(int id, int balance) { - this.jdbcTemplate.update(UPDATE_CUSTOMER_BALANCE_QUERY, balance, id); - } - - private void insertTransaction(int id, int value, String transactionType, String description) { - this.jdbcTemplate.update(INSERT_TRANSACTION_QUERY, - id, value, transactionType, description, - Timestamp.valueOf(LocalDateTime.now().atZone(ZoneId.of(ConstantsUtil.TIME_ZONE)).toLocalDateTime())); - } - - public boolean existsCustomerById(int id) { - return this.jdbcTemplate.queryForObject(EXIST_CUSTOMER_BY_ID_QUERY, new Object[]{id}, - (rs, rowNum) -> rs.getBoolean(1) - ); - } -} diff --git a/src/main/java/com/github/rinha/services/AccountService.java b/src/main/java/com/github/rinha/services/AccountService.java index e25c8e5..6dea207 100644 --- a/src/main/java/com/github/rinha/services/AccountService.java +++ b/src/main/java/com/github/rinha/services/AccountService.java @@ -1,54 +1,75 @@ package com.github.rinha.services; -import com.github.rinha.domain.Customer; -import com.github.rinha.domain.dtos.CustomerDTO; -import com.github.rinha.domain.dtos.StatementDTO; -import com.github.rinha.domain.dtos.TransactionRequest; -import com.github.rinha.domain.dtos.TransactionResponse; +import com.github.rinha.domain.Account; +import com.github.rinha.domain.Balance; +import com.github.rinha.domain.Statement; +import com.github.rinha.domain.Transaction; import com.github.rinha.domain.exceptions.UnprocessableException; -import com.github.rinha.persistence.AccountPersistence; -import com.github.rinha.utils.ConstantsUtil; +import com.github.rinha.domain.utils.TimeUtils; +import java.util.ArrayList; +import java.util.List; +import javax.sql.DataSource; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Mono; -import java.util.List; - @Service public class AccountService { - private final AccountPersistence accountPersistence; + private final JdbcTemplate jdbcTemplate; - public AccountService(AccountPersistence accountPersistence) { - this.accountPersistence = accountPersistence; - } + public AccountService(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } - @Transactional(readOnly = true) - public Mono findStatementByCustomerId(int id) { - Customer customer = this.accountPersistence.findCustomerById(id); - List transactions = this.accountPersistence.findTransactionsByCustomerId(id); + public Mono retrieveStatement(int id) { + Balance account = new Balance(); + List transactions = new ArrayList<>(); - return Mono.just(new StatementDTO(customer, transactions)); - } + this.jdbcTemplate.query( + "SELECT c.max_limit, c.balance, t.amount, t.kind, t.description, t.created_at FROM account as c LEFT JOIN transactions as t ON t.id_account = c.id WHERE c.id = ? ORDER BY t.created_at DESC LIMIT 10", + new Object[]{id}, + (rs, rowNum) -> { + if (account.getLimit() == 0) { + account.setLimit(rs.getInt("max_limit")); + account.setAmount(rs.getInt("balance")); + } + if (rs.getInt("amount") != 0) { + transactions.add( + new Transaction(rs.getInt("amount"), rs.getString("kind"), rs.getString("description"), + rs.getString("created_at"))); + } + return null; + }); - @Transactional - public Mono updateBalanceAndInsertTransaction(int id, int value, String type, String description) { - Customer customer = this.accountPersistence.findCustomerForUpdate(id); - if (type.equals(ConstantsUtil.TRANSACTION_TYPE_DEBIT) && customer.hasEnoughBalance(value)) { - return Mono.error(UnprocessableException::new); - } + return Mono.just(new Statement(account, transactions)); + } - int newBalance = type.equals(ConstantsUtil.TRANSACTION_TYPE_DEBIT) ? customer.getBalance() - value : customer.getBalance() + value; - this.accountPersistence.updateBalanceAndInsertTransaction(id, newBalance, value, type, description); + @Transactional + public Mono processTransaction(int id, int value, String type, String description) { + Account account = this.jdbcTemplate.queryForObject( + "SELECT id, max_limit, balance FROM account WHERE id = ? FOR UPDATE", new Object[]{id}, + (rs, rowNum) -> new Account(rs.getInt("max_limit"), rs.getInt("balance"))); - return Mono.just(new CustomerDTO(customer.getMaxLimit(), newBalance)); + if (type.equals("d") && account.hasEnoughBalance(value)) { + return Mono.error(UnprocessableException::new); } - public Mono isValidCustomerId(int id) { - return Mono.just(this.accountPersistence.existsCustomerById(id)); - } + int newBalance = type.equals("d") ? account.getBalance() - value : account.getBalance() + value; + + this.jdbcTemplate.update( + "WITH ins AS ( " + + " INSERT INTO transactions (id_account, amount, kind, description, created_at) VALUES (?, ?, ?, ?, ?) " + + " RETURNING id_account " + + ") " + + "UPDATE account c " + + "SET balance = ? " + + "FROM ins " + + "WHERE c.id = ins.id_account", + id, value, type, description, TimeUtils.now(), newBalance); + + return Mono.just(new Account(account.getMaxLimit(), newBalance)); + } - public Mono isTransactionValid(TransactionRequest transaction) { - return transaction.isRequestValid() ? Mono.just(true): Mono.error(UnprocessableException::new); - } } diff --git a/src/main/java/com/github/rinha/utils/ConstantsUtil.java b/src/main/java/com/github/rinha/utils/ConstantsUtil.java deleted file mode 100644 index 11d5cd8..0000000 --- a/src/main/java/com/github/rinha/utils/ConstantsUtil.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.rinha.utils; - -public final class ConstantsUtil { - - private ConstantsUtil() { - throw new IllegalStateException("Utility class"); - } - - public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"; - - public static final String TIME_ZONE = "UTC"; - - public static final String INVALID_REQUEST = "Invalid request"; - - public static final String TRANSACTION_TYPE_DEBIT = "d"; - - public static final String TRANSACTION_TYPE_CREDIT = "c"; -} diff --git a/src/test/java/com/github/rinha/AccountServiceTest.java b/src/test/java/com/github/rinha/AccountServiceTest.java index 6dbfde8..7bbda50 100644 --- a/src/test/java/com/github/rinha/AccountServiceTest.java +++ b/src/test/java/com/github/rinha/AccountServiceTest.java @@ -1,134 +1,109 @@ package com.github.rinha; -import com.github.rinha.domain.Customer; -import com.github.rinha.domain.dtos.CustomerDTO; -import com.github.rinha.domain.dtos.StatementDTO; -import com.github.rinha.domain.dtos.TransactionRequest; -import com.github.rinha.domain.dtos.TransactionResponse; -import com.github.rinha.domain.exceptions.UnprocessableException; -import com.github.rinha.persistence.AccountPersistence; -import com.github.rinha.services.AccountService; -import org.junit.jupiter.api.DisplayName; -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.mock.mockito.MockBean; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.util.List; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.when; - -@DisplayName("Account Service Unit Test") -@ContextConfiguration(classes = {AccountService.class}) -@ExtendWith(SpringExtension.class) +//@DisplayName("Account Service Unit Test") +//@ContextConfiguration(classes = {AccountService.class}) +//@ExtendWith(SpringExtension.class) class AccountServiceTest { - @Autowired - private AccountService accountService; - - @MockBean - private AccountPersistence accountPersistence; - - @Test - void shouldBeFindStatementByCustomerId() { - // Given - when(this.accountPersistence.findCustomerById(anyInt())).thenReturn(new Customer(1, 10, 10)); - when(this.accountPersistence.findTransactionsByCustomerId(anyInt())).thenReturn(List.of(new TransactionResponse(1, "c", "test", "2024-02-19"))); - - - // When - Mono statement = this.accountService.findStatementByCustomerId(1); - - // Then - StepVerifier.create(statement) - .expectNextMatches(s -> s.saldo().limite() == 10 && s.saldo().total() == 10 && s.ultimas_transacoes().size() == 1) - .verifyComplete(); - } - - @Test - void shouldBeUpdateBalanceAndInsertTransaction() { - // Given - when(this.accountPersistence.findCustomerForUpdate(anyInt())).thenReturn(new Customer(1, 10, 10)); - doNothing().when(this.accountPersistence).updateBalanceAndInsertTransaction(anyInt(), anyInt(), anyInt(), anyString(), anyString()); - - // When - Mono customer = this.accountService.updateBalanceAndInsertTransaction(1, 1, "c", "test"); - - // Then - StepVerifier.create(customer) - .expectNextMatches(c -> c.limite() == 10 && c.saldo() == 11) - .verifyComplete(); - } - - @Test - void shouldValidateBalanceOfCustomer() { - // Given - when(this.accountPersistence.findCustomerForUpdate(anyInt())).thenReturn(new Customer(1, 10, 10)); - doNothing().when(this.accountPersistence).updateBalanceAndInsertTransaction(anyInt(), anyInt(), anyInt(), anyString(), anyString()); - - // When - Mono customer = this.accountService.updateBalanceAndInsertTransaction(1, 10000, "d", "test"); - - // Then - StepVerifier.create(customer) - .expectErrorMatches(throwable -> throwable instanceof UnprocessableException) - .verify(); - } - - @Test - void shouldVerifyCustomerId() { - // Given - when(this.accountPersistence.existsCustomerById(anyInt())).thenReturn(true); - - // When - Mono isCustomerValid = this.accountService.isValidCustomerId(1); - - // Then - StepVerifier.create(isCustomerValid) - .expectNextMatches(b -> b.equals(true)) - .verifyComplete(); - } - - @Test - void shouldVerifyIfIsInvalidCustomerId() { - // Given - when(this.accountPersistence.existsCustomerById(anyInt())).thenReturn(false); - - // When - Mono isCustomerValid = this.accountService.isValidCustomerId(1); - - // Then - StepVerifier.create(isCustomerValid) - .expectNextMatches(b -> b.equals(false)) - .verifyComplete(); - } - - @Test - void shouldVerifyIfIsTransactionValid() { - // When - Mono isTransactionValid = this.accountService.isTransactionValid(new TransactionRequest("1", "c", "test")); - - // Then - StepVerifier.create(isTransactionValid) - .expectNextMatches(b -> b.equals(true)) - .verifyComplete(); - } - - @Test - void shouldVerifyIfIsTransactionInvalid() { - // When - Mono isTransactionValid = this.accountService.isTransactionValid(new TransactionRequest("c", "c", "test")); - - // Then - StepVerifier.create(isTransactionValid) - .expectErrorMatches(throwable -> throwable instanceof UnprocessableException) - .verify(); - } +// @Autowired +// private AccountService accountService; +// +// @MockBean +// private AccountPersistence accountPersistence; +// +// @Test +// void shouldBeFindStatementByCustomerId() { +// // Given +// when(this.accountPersistence.findCustomerById(anyInt())).thenReturn(new Customer(1, 10, 10)); +// when(this.accountPersistence.findTransactionsByCustomerId(anyInt())).thenReturn(List.of(new TransactionResponse(1, "c", "test", "2024-02-19"))); +// +// +// // When +// Mono statement = this.accountService.retrieveStatement(1); +// +// // Then +// StepVerifier.create(statement) +// .expectNextMatches(s -> s.saldo().limite() == 10 && s.saldo().total() == 10 && s.ultimas_transacoes().size() == 1) +// .verifyComplete(); +// } +// +// @Test +// void shouldBeUpdateBalanceAndInsertTransaction() { +// // Given +// when(this.accountPersistence.findCustomerForUpdate(anyInt())).thenReturn(new Customer(1, 10, 10)); +// doNothing().when(this.accountPersistence).updateBalanceAndInsertTransaction(anyInt(), anyInt(), anyInt(), anyString(), anyString()); +// +// // When +// Mono customer = this.accountService.processTransaction(1, 1, "c", "test"); +// +// // Then +// StepVerifier.create(customer) +// .expectNextMatches(c -> c.limite() == 10 && c.saldo() == 11) +// .verifyComplete(); +// } +// +// @Test +// void shouldValidateBalanceOfCustomer() { +// // Given +// when(this.accountPersistence.findCustomerForUpdate(anyInt())).thenReturn(new Customer(1, 10, 10)); +// doNothing().when(this.accountPersistence).updateBalanceAndInsertTransaction(anyInt(), anyInt(), anyInt(), anyString(), anyString()); +// +// // When +// Mono customer = this.accountService.processTransaction(1, 10000, "d", "test"); +// +// // Then +// StepVerifier.create(customer) +// .expectErrorMatches(throwable -> throwable instanceof UnprocessableException) +// .verify(); +// } +// +// @Test +// void shouldVerifyCustomerId() { +// // Given +// when(this.accountPersistence.existsCustomerById(anyInt())).thenReturn(true); +// +// // When +// Mono isCustomerValid = this.accountService.isValidCustomerId(1); +// +// // Then +// StepVerifier.create(isCustomerValid) +// .expectNextMatches(b -> b.equals(true)) +// .verifyComplete(); +// } +// +// @Test +// void shouldVerifyIfIsInvalidCustomerId() { +// // Given +// when(this.accountPersistence.existsCustomerById(anyInt())).thenReturn(false); +// +// // When +// Mono isCustomerValid = this.accountService.isValidCustomerId(1); +// +// // Then +// StepVerifier.create(isCustomerValid) +// .expectNextMatches(b -> b.equals(false)) +// .verifyComplete(); +// } +// +// @Test +// void shouldVerifyIfIsTransactionValid() { +// // When +// Mono isTransactionValid = this.accountService.isTransactionValid(new TransactionRequest("1", "c", "test")); +// +// // Then +// StepVerifier.create(isTransactionValid) +// .expectNextMatches(b -> b.equals(true)) +// .verifyComplete(); +// } +// +// @Test +// void shouldVerifyIfIsTransactionInvalid() { +// // When +// Mono isTransactionValid = this.accountService.isTransactionValid(new TransactionRequest("c", "c", "test")); +// +// // Then +// StepVerifier.create(isTransactionValid) +// .expectErrorMatches(throwable -> throwable instanceof UnprocessableException) +// .verify(); +// } } diff --git a/src/test/java/com/github/rinha/CustomerControllerTest.java b/src/test/java/com/github/rinha/CustomerControllerTest.java index 74afc86..32a006f 100644 --- a/src/test/java/com/github/rinha/CustomerControllerTest.java +++ b/src/test/java/com/github/rinha/CustomerControllerTest.java @@ -1,111 +1,93 @@ package com.github.rinha; -import com.github.rinha.controllers.CustomerController; -import com.github.rinha.domain.dtos.*; -import com.github.rinha.services.AccountService; -import org.junit.jupiter.api.DisplayName; -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 reactor.core.publisher.Mono; - -import java.util.List; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@DisplayName("Customer Controller Unit Test") -@ExtendWith(MockitoExtension.class) +//@DisplayName("Customer Controller Unit Test") +//@ExtendWith(MockitoExtension.class) public class CustomerControllerTest { - @Mock - private AccountService accountService; - - @InjectMocks - private CustomerController controller; - - @Test - void shouldBeRetrieveStatement() { - // Given - when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); - when(accountService.findStatementByCustomerId(anyInt())).thenReturn(Mono.just(new StatementDTO(new BalanceDTO(10, 10), List.of(new TransactionResponse(1, "c", "test", "2024-02-19"))))); - - // When and Then - controller.getExtratoByClienteId(123) - .doOnNext(response -> { - assert response.getStatusCodeValue() == HttpStatus.OK.value(); - verify(accountService, times(1)).isValidCustomerId(123); - verify(accountService, times(1)).findStatementByCustomerId(123); - }) - .block(); - } - - @Test - void shouldBeReturnNotFoundWhenCustomerIdIsInvalidForStatement() { - // Given - when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(false)); - - // When and Then - controller.getExtratoByClienteId(123) - .doOnNext(response -> { - assert response.getStatusCodeValue() == HttpStatus.NOT_FOUND.value(); - }) - .block(); - } - - @Test - void shouldBeCreateTransaction() { - // Given - TransactionRequest transaction = new TransactionRequest("100", "c", "test"); - - when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); - when(accountService.isTransactionValid(transaction)).thenReturn(Mono.just(true)); - when(accountService.updateBalanceAndInsertTransaction(anyInt(), anyInt(), anyString(), anyString())) - .thenReturn(Mono.just(new CustomerDTO(123, 10))); - - // When and Then - controller.transacionar(123, transaction) - .doOnNext(response -> { - assert response.getStatusCodeValue() == HttpStatus.OK.value(); - verify(accountService, times(1)).isValidCustomerId(123); - verify(accountService, times(1)).isTransactionValid(transaction); - verify(accountService, times(1)).updateBalanceAndInsertTransaction(123, transaction.parseValueToInt(), transaction.tipo(), transaction.descricao()); - }) - .block(); - } - - @Test - void shouldBeReturnNotFoundWhenCustomerIdIsInvalid() { - // Given - TransactionRequest transaction = new TransactionRequest("100", "c", "test"); - - when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(false)); - - // When and Then - controller.transacionar(123, transaction) - .doOnNext(response -> { - assert response.getStatusCodeValue() == HttpStatus.NOT_FOUND.value(); - }) - .block(); - } - - @Test - void shouldBeReturnUnprocessableEntityWhenTransactionIsInvalid() { - // Given - TransactionRequest transaction = new TransactionRequest("100", "c", "test"); - - when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); - when(accountService.isTransactionValid(transaction)).thenReturn(Mono.just(false)); - - // When and Then - controller.transacionar(123, transaction) - .doOnNext(response -> { - assert response.getStatusCodeValue() == HttpStatus.UNPROCESSABLE_ENTITY.value(); - }) - .block(); - } +// @Mock +// private AccountService accountService; +// +// @InjectMocks +// private CustomerController controller; +// +// @Test +// void shouldBeRetrieveStatement() { +// // Given +// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); +// when(accountService.retrieveStatement(anyInt())).thenReturn(Mono.just(new StatementDTO(new BalanceDTO(10, 10), List.of(new TransactionResponse(1, "c", "test", "2024-02-19"))))); +// +// // When and Then +// controller.getExtratoByClienteId(123) +// .doOnNext(response -> { +// assert response.getStatusCodeValue() == HttpStatus.OK.value(); +// verify(accountService, times(1)).isValidCustomerId(123); +// verify(accountService, times(1)).retrieveStatement(123); +// }) +// .block(); +// } +// +// @Test +// void shouldBeReturnNotFoundWhenCustomerIdIsInvalidForStatement() { +// // Given +// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(false)); +// +// // When and Then +// controller.getExtratoByClienteId(123) +// .doOnNext(response -> { +// assert response.getStatusCodeValue() == HttpStatus.NOT_FOUND.value(); +// }) +// .block(); +// } +// +// @Test +// void shouldBeCreateTransaction() { +// // Given +// TransactionRequest transaction = new TransactionRequest("100", "c", "test"); +// +// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); +// when(accountService.isTransactionValid(transaction)).thenReturn(Mono.just(true)); +// when(accountService.processTransaction(anyInt(), anyInt(), anyString(), anyString())) +// .thenReturn(Mono.just(new CustomerDTO(123, 10))); +// +// // When and Then +// controller.transacionar(123, transaction) +// .doOnNext(response -> { +// assert response.getStatusCodeValue() == HttpStatus.OK.value(); +// verify(accountService, times(1)).isValidCustomerId(123); +// verify(accountService, times(1)).isTransactionValid(transaction); +// verify(accountService, times(1)).processTransaction(123, transaction.parseValueToInt(), transaction.tipo(), transaction.descricao()); +// }) +// .block(); +// } +// +// @Test +// void shouldBeReturnNotFoundWhenCustomerIdIsInvalid() { +// // Given +// TransactionRequest transaction = new TransactionRequest("100", "c", "test"); +// +// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(false)); +// +// // When and Then +// controller.transacionar(123, transaction) +// .doOnNext(response -> { +// assert response.getStatusCodeValue() == HttpStatus.NOT_FOUND.value(); +// }) +// .block(); +// } +// +// @Test +// void shouldBeReturnUnprocessableEntityWhenTransactionIsInvalid() { +// // Given +// TransactionRequest transaction = new TransactionRequest("100", "c", "test"); +// +// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); +// when(accountService.isTransactionValid(transaction)).thenReturn(Mono.just(false)); +// +// // When and Then +// controller.transacionar(123, transaction) +// .doOnNext(response -> { +// assert response.getStatusCodeValue() == HttpStatus.UNPROCESSABLE_ENTITY.value(); +// }) +// .block(); +// } } From 50405c646ccd1155754186b83ad76e5c33ee5645 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 25 Feb 2024 02:16:42 -0300 Subject: [PATCH 3/4] test: adjusting test of application --- .../github/rinha/AccountControllerTest.java | 94 +++++++++++++++ .../com/github/rinha/AccountServiceTest.java | 109 ------------------ .../github/rinha/CustomerControllerTest.java | 93 --------------- .../java/com/github/rinha/DomainFactory.java | 40 +++++++ 4 files changed, 134 insertions(+), 202 deletions(-) create mode 100644 src/test/java/com/github/rinha/AccountControllerTest.java delete mode 100644 src/test/java/com/github/rinha/AccountServiceTest.java delete mode 100644 src/test/java/com/github/rinha/CustomerControllerTest.java create mode 100644 src/test/java/com/github/rinha/DomainFactory.java diff --git a/src/test/java/com/github/rinha/AccountControllerTest.java b/src/test/java/com/github/rinha/AccountControllerTest.java new file mode 100644 index 0000000..1974096 --- /dev/null +++ b/src/test/java/com/github/rinha/AccountControllerTest.java @@ -0,0 +1,94 @@ +package com.github.rinha; + +import static com.github.rinha.DomainFactory.createAccount; +import static com.github.rinha.DomainFactory.createStatement; +import static com.github.rinha.DomainFactory.createTransact; +import static com.github.rinha.DomainFactory.invalidTransact; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.github.rinha.controllers.AccountController; +import com.github.rinha.services.AccountService; +import org.junit.jupiter.api.DisplayName; +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 reactor.core.publisher.Mono; + +@DisplayName("Account Controller Unit Test") +@ExtendWith(MockitoExtension.class) +public class AccountControllerTest { + + @Mock + private AccountService accountService; + + @InjectMocks + private AccountController controller; + + @Test + void shouldBeRetrieveStatement() { + // Given + when(accountService.retrieveStatement(anyInt())).thenReturn(Mono.just(createStatement())); + + // When and Then + var response = controller.retrieveStatement(5).block(); + + // Then + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + + verify(accountService, times(1)).retrieveStatement(5); + } + + @Test + void shouldBeReturnNotFoundWhenAccountIdIsInvalidForStatement() { + // When and Then + var response = controller.retrieveStatement(-1).block(); + + //Then + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + } + + @Test + void shouldBeCreateTransaction() { + // Given + when(accountService.processTransaction(anyInt(), anyInt(), anyString(), anyString())) + .thenReturn(Mono.just(createAccount())); + + // When + var response = controller.transact(5, createTransact()).block(); + + // Then + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + } + + @Test + void shouldBeReturnNotFoundWhenAccountIdIsInvalid() { + // When + var response = controller.transact(-1, createTransact()).block(); + + // Then + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + } + + @Test + void shouldBeReturnUnprocessableEntityWhenTransactionIsInvalid() { + // When + var response = controller.transact(5, invalidTransact()).block(); + + // Then + assertNotNull(response); + assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, response.getStatusCode()); + } +} diff --git a/src/test/java/com/github/rinha/AccountServiceTest.java b/src/test/java/com/github/rinha/AccountServiceTest.java deleted file mode 100644 index 7bbda50..0000000 --- a/src/test/java/com/github/rinha/AccountServiceTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.github.rinha; - -//@DisplayName("Account Service Unit Test") -//@ContextConfiguration(classes = {AccountService.class}) -//@ExtendWith(SpringExtension.class) -class AccountServiceTest { - -// @Autowired -// private AccountService accountService; -// -// @MockBean -// private AccountPersistence accountPersistence; -// -// @Test -// void shouldBeFindStatementByCustomerId() { -// // Given -// when(this.accountPersistence.findCustomerById(anyInt())).thenReturn(new Customer(1, 10, 10)); -// when(this.accountPersistence.findTransactionsByCustomerId(anyInt())).thenReturn(List.of(new TransactionResponse(1, "c", "test", "2024-02-19"))); -// -// -// // When -// Mono statement = this.accountService.retrieveStatement(1); -// -// // Then -// StepVerifier.create(statement) -// .expectNextMatches(s -> s.saldo().limite() == 10 && s.saldo().total() == 10 && s.ultimas_transacoes().size() == 1) -// .verifyComplete(); -// } -// -// @Test -// void shouldBeUpdateBalanceAndInsertTransaction() { -// // Given -// when(this.accountPersistence.findCustomerForUpdate(anyInt())).thenReturn(new Customer(1, 10, 10)); -// doNothing().when(this.accountPersistence).updateBalanceAndInsertTransaction(anyInt(), anyInt(), anyInt(), anyString(), anyString()); -// -// // When -// Mono customer = this.accountService.processTransaction(1, 1, "c", "test"); -// -// // Then -// StepVerifier.create(customer) -// .expectNextMatches(c -> c.limite() == 10 && c.saldo() == 11) -// .verifyComplete(); -// } -// -// @Test -// void shouldValidateBalanceOfCustomer() { -// // Given -// when(this.accountPersistence.findCustomerForUpdate(anyInt())).thenReturn(new Customer(1, 10, 10)); -// doNothing().when(this.accountPersistence).updateBalanceAndInsertTransaction(anyInt(), anyInt(), anyInt(), anyString(), anyString()); -// -// // When -// Mono customer = this.accountService.processTransaction(1, 10000, "d", "test"); -// -// // Then -// StepVerifier.create(customer) -// .expectErrorMatches(throwable -> throwable instanceof UnprocessableException) -// .verify(); -// } -// -// @Test -// void shouldVerifyCustomerId() { -// // Given -// when(this.accountPersistence.existsCustomerById(anyInt())).thenReturn(true); -// -// // When -// Mono isCustomerValid = this.accountService.isValidCustomerId(1); -// -// // Then -// StepVerifier.create(isCustomerValid) -// .expectNextMatches(b -> b.equals(true)) -// .verifyComplete(); -// } -// -// @Test -// void shouldVerifyIfIsInvalidCustomerId() { -// // Given -// when(this.accountPersistence.existsCustomerById(anyInt())).thenReturn(false); -// -// // When -// Mono isCustomerValid = this.accountService.isValidCustomerId(1); -// -// // Then -// StepVerifier.create(isCustomerValid) -// .expectNextMatches(b -> b.equals(false)) -// .verifyComplete(); -// } -// -// @Test -// void shouldVerifyIfIsTransactionValid() { -// // When -// Mono isTransactionValid = this.accountService.isTransactionValid(new TransactionRequest("1", "c", "test")); -// -// // Then -// StepVerifier.create(isTransactionValid) -// .expectNextMatches(b -> b.equals(true)) -// .verifyComplete(); -// } -// -// @Test -// void shouldVerifyIfIsTransactionInvalid() { -// // When -// Mono isTransactionValid = this.accountService.isTransactionValid(new TransactionRequest("c", "c", "test")); -// -// // Then -// StepVerifier.create(isTransactionValid) -// .expectErrorMatches(throwable -> throwable instanceof UnprocessableException) -// .verify(); -// } -} diff --git a/src/test/java/com/github/rinha/CustomerControllerTest.java b/src/test/java/com/github/rinha/CustomerControllerTest.java deleted file mode 100644 index 32a006f..0000000 --- a/src/test/java/com/github/rinha/CustomerControllerTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.github.rinha; - -//@DisplayName("Customer Controller Unit Test") -//@ExtendWith(MockitoExtension.class) -public class CustomerControllerTest { - -// @Mock -// private AccountService accountService; -// -// @InjectMocks -// private CustomerController controller; -// -// @Test -// void shouldBeRetrieveStatement() { -// // Given -// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); -// when(accountService.retrieveStatement(anyInt())).thenReturn(Mono.just(new StatementDTO(new BalanceDTO(10, 10), List.of(new TransactionResponse(1, "c", "test", "2024-02-19"))))); -// -// // When and Then -// controller.getExtratoByClienteId(123) -// .doOnNext(response -> { -// assert response.getStatusCodeValue() == HttpStatus.OK.value(); -// verify(accountService, times(1)).isValidCustomerId(123); -// verify(accountService, times(1)).retrieveStatement(123); -// }) -// .block(); -// } -// -// @Test -// void shouldBeReturnNotFoundWhenCustomerIdIsInvalidForStatement() { -// // Given -// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(false)); -// -// // When and Then -// controller.getExtratoByClienteId(123) -// .doOnNext(response -> { -// assert response.getStatusCodeValue() == HttpStatus.NOT_FOUND.value(); -// }) -// .block(); -// } -// -// @Test -// void shouldBeCreateTransaction() { -// // Given -// TransactionRequest transaction = new TransactionRequest("100", "c", "test"); -// -// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); -// when(accountService.isTransactionValid(transaction)).thenReturn(Mono.just(true)); -// when(accountService.processTransaction(anyInt(), anyInt(), anyString(), anyString())) -// .thenReturn(Mono.just(new CustomerDTO(123, 10))); -// -// // When and Then -// controller.transacionar(123, transaction) -// .doOnNext(response -> { -// assert response.getStatusCodeValue() == HttpStatus.OK.value(); -// verify(accountService, times(1)).isValidCustomerId(123); -// verify(accountService, times(1)).isTransactionValid(transaction); -// verify(accountService, times(1)).processTransaction(123, transaction.parseValueToInt(), transaction.tipo(), transaction.descricao()); -// }) -// .block(); -// } -// -// @Test -// void shouldBeReturnNotFoundWhenCustomerIdIsInvalid() { -// // Given -// TransactionRequest transaction = new TransactionRequest("100", "c", "test"); -// -// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(false)); -// -// // When and Then -// controller.transacionar(123, transaction) -// .doOnNext(response -> { -// assert response.getStatusCodeValue() == HttpStatus.NOT_FOUND.value(); -// }) -// .block(); -// } -// -// @Test -// void shouldBeReturnUnprocessableEntityWhenTransactionIsInvalid() { -// // Given -// TransactionRequest transaction = new TransactionRequest("100", "c", "test"); -// -// when(accountService.isValidCustomerId(anyInt())).thenReturn(Mono.just(true)); -// when(accountService.isTransactionValid(transaction)).thenReturn(Mono.just(false)); -// -// // When and Then -// controller.transacionar(123, transaction) -// .doOnNext(response -> { -// assert response.getStatusCodeValue() == HttpStatus.UNPROCESSABLE_ENTITY.value(); -// }) -// .block(); -// } -} diff --git a/src/test/java/com/github/rinha/DomainFactory.java b/src/test/java/com/github/rinha/DomainFactory.java new file mode 100644 index 0000000..1d4169a --- /dev/null +++ b/src/test/java/com/github/rinha/DomainFactory.java @@ -0,0 +1,40 @@ +package com.github.rinha; + +import com.github.rinha.domain.Account; +import com.github.rinha.domain.Balance; +import com.github.rinha.domain.Statement; +import com.github.rinha.domain.Transact; +import com.github.rinha.domain.Transaction; +import java.util.List; + +public class DomainFactory { + + private DomainFactory() { + throw new IllegalStateException("Utility class"); + } + + public static Transact createTransact() { + return new Transact("100", "d", "Market"); + } + + public static Account createAccount() { + return new Account(1000, 1000); + } + + public static Balance createBalance() { + return new Balance(1000, 1000); + } + + public static Transaction createTransaction() { + return new Transaction(100, "d", "Market", "2021-01-01T00:00:00Z"); + } + + public static Transact invalidTransact() { + return new Transact("-a", "e", null); + } + + public static Statement createStatement() { + return new Statement(createBalance(), List.of(createTransaction())); + } + +} From 0f3ed5814916e3056920b152795cde01eb916877 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 25 Feb 2024 02:43:02 -0300 Subject: [PATCH 4/4] doc: improvement documentation with steps, materials and description --- README.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 864b183..3f40606 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,123 @@ # Rinha de Backend 2024 Q1 -__Nome__: Frank Laércio +## Description + +This project, Rinha de Backend 2024 Q1, is a banking system simulation developed in Java using the +Spring Boot framework. It provides functionalities such as account management, transaction handling, +and balance inquiries. The system is designed to handle multiple accounts and transactions +concurrently, ensuring data consistency and +reliability. The project uses an SQL database for data persistence and Maven for dependency +management. It's designed for developers who want to understand how to build a robust and scalable +banking system using Spring Boot and Java. + +If you want to check more about of the challenge, please check the [Rinha de Backend 2024 Q1](https://github.com/franklaercio/rinha-de-backend-2024-q1). + +## Technologies Used -__Tecnologias__: - Java -- Spring WebFlux -- PostgreSQL -- NGINX +- Spring Boot +- Maven +- SQL +- JUnit +- Postgres +- Docker +- Nginx + +## Learning Objectives + +__In this project, you will:__ +- Learn how to build a banking system using Spring Boot and Java. +- Understand how to handle transactions and account management in a banking system. +- Learn how to use an SQL database for data persistence. +- Understand how to use Docker to run a database. +- Learn how to use Nginx as a reverse proxy server. + +__Concurrent programming learning:__ +- Understand how to handle concurrent transactions and account management. +- Learn how to ensure data consistency and reliability in a concurrent environment. +- Understand how to handle multiple requests concurrently. +- Learn how to use locks and synchronization to ensure data consistency. +- Understand how to use transactions to ensure data reliability. +- Learn about problems of deadlocks and how to avoid them. +- Understand how to use thread pools to handle multiple requests concurrently. + +__Prerequisites:__ +- Basic knowledge of Java and Spring Boot. +- Basic knowledge of Postgres. +- Basic knowledge of Docker. +- Basic knowledge of Nginx. +- Basic knowledge of Maven. + +__Study Materials:__ +- [Spring Boot Documentation](https://spring.io/projects/spring-boot) +- [Java Documentation](https://docs.oracle.com/en/java/) +- [Postgres Documentation](https://www.postgresql.org/docs/) +- [Docker Documentation](https://docs.docker.com/) +- [Nginx Documentation](https://nginx.org/en/docs/) +- [Concurrency in Java](https://www.baeldung.com/java-concurrency) +- [Java Multithreading](https://www.geeksforgeeks.org/multithreading-in-java/) +- [Java Thread Pools](https://www.baeldung.com/thread-pool-java-and-guava) +- [Java Synchronization](https://www.baeldung.com/java-synchronized) +- [Java Transactions](https://www.baeldung.com/java-transactions) +- [Concurrency Programming](https://www.geeksforgeeks.org/java-util-concurrent-package) +- [Concurrency Problems](https://www.geeksforgeeks.org/concurrency-in-operating-system/) + +## Setup and Installation + +To run this project, you need to have Java 11 and Maven installed on your machine. You also need to +have a Postgres database running. You can use Docker to run the database. Here are the steps to +install and run the project: + +1. Clone the repository: `bash git clone https://github.com/franklaercio/rinha-de-backend-java` +2. Navigate to the project directory: `bash cd rinha-de-backend-2024-q1` +3. Run the database using Docker: `bash docker-compose up -d` +4. Run the project using Maven: `bash mvn spring-boot:run` +5. The project will start running on `http://localhost:8080`. +You can use Postman or any other API client to test the endpoints. +6. To stop the database, run: +7. `bash docker-compose down` +8. To run the tests, run: +9. `bash mvn test` + +## Usage + +- To create a transaction, send a POST request to `http://localhost:8080/clientes/{id}/transacoes` +with the following JSON body: + + ```json + { + "valor": "1000", + "tipo": "d", + "descricao": "test" + } + ``` + +- To get a statement, send a GET request to `http://localhost:8080/clientes/{id}/extrato`. + +## Contributing + +If you want to contribute to this project, you can follow these steps: + +1. Fork the project. +2. Create a new branch with your changes: + ```bash + git checkout -b feature/my-feature + ``` +3. Save your changes and create a commit message telling what you did: + ```bash + git commit -m "My changes" + ``` +4. Push your branch: +5. ```bash + git push origin feature/my-feature + ``` +6. Open a pull request with a description of your changes. +7. After your changes are approved, they will be merged into the project. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. -__Link Repositório__: https://github.com/franklaercio/rinha-de-backend-java +## Contact Information -__LinkedIn__: https://www.linkedin.com/in/frank-laercio/ +If you have any questions about this project, please contact me at e-mail. \ No newline at end of file