diff --git a/.cache/google-java-format-1.7-all-deps.jar b/.cache/google-java-format-1.7-all-deps.jar new file mode 100644 index 0000000000..e2d40de463 Binary files /dev/null and b/.cache/google-java-format-1.7-all-deps.jar differ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..a5ddd162d5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +* +!target/desafio-dev-0.0.1-SNAPSHOT.jar \ No newline at end of file diff --git a/.github/workflows/api-scan.yml b/.github/workflows/api-scan.yml new file mode 100644 index 0000000000..c50c51e6da --- /dev/null +++ b/.github/workflows/api-scan.yml @@ -0,0 +1,16 @@ +name: API Scan workflow + +on: + workflow_call: + secrets: + END_POINT: + required: true + +jobs: + reusable_workflow_job: + runs-on: ubuntu-latest + name: API Scan + steps: + - uses: zaproxy/action-api-scan@v0.4.0 + with: + target: ${{ secrets.END_POINT }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..22576652cf --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,30 @@ +name: Build and Publish Docker image + +on: + workflow_call: + secrets: + PROJECT_ID: + required: true + CLOUD_CREDENTIAL: + required: true + ZONE: + required: true + CLUSTER_NAME: + required: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: google-github-actions/auth@v1 + with: + credentials_json: ${{ secrets.CLOUD_CREDENTIAL }} + - uses: google-github-actions/get-gke-credentials@v1 + with: + cluster_name: ${{ secrets.CLUSTER_NAME }} + location: ${{ secrets.ZONE }} + - run: kubectl apply -f kube\ + - run: kubectl rollout status deployment/desafio-dev + - run: kubectl get services -o wide + diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..587ed21d2b --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,28 @@ +name: Build and Publish Docker image + +on: + workflow_call: + secrets: + DOCKERHUB_USERNAME: + required: true + DOCKERHUB_TOKEN: + required: true + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: 1.11 + - name: Build with Maven + run: mvn clean install -DskipTests + - name: Publish to Docker Hub + uses: docker/build-push-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: leonardoscalabrini/desafio-dev + tags: latest \ No newline at end of file diff --git a/.github/workflows/github-ci.yml b/.github/workflows/github-ci.yml new file mode 100644 index 0000000000..ee612c4e8c --- /dev/null +++ b/.github/workflows/github-ci.yml @@ -0,0 +1,35 @@ +name: GitHub CI + +on: + push: + branches: [ main ] + +jobs: + quality-check: + uses: ./.github/workflows/sonarcloud.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + build-image: + needs: [quality-check] + uses: ./.github/workflows/docker.yml + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + create-infrastructure: + needs: [ build-image ] + uses: ./.github/workflows/iac.yml + secrets: + TF_API_TOKEN: ${{ secrets.TF_API_TOKEN }} + deploy: + needs: [ create-infrastructure ] + uses: ./.github/workflows/deploy.yml + secrets: + PROJECT_ID: ${{ secrets.PROJECT_ID }} + CLOUD_CREDENTIAL: ${{ secrets.CLOUD_CREDENTIAL }} + ZONE: ${{ secrets.ZONE }} + CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }} + api-scan: + needs: [ deploy ] + uses: ./.github/workflows/api-scan.yml + secrets: + END_POINT: ${{ secrets.END_POINT }} diff --git a/.github/workflows/iac.yml b/.github/workflows/iac.yml new file mode 100644 index 0000000000..b9d28679b3 --- /dev/null +++ b/.github/workflows/iac.yml @@ -0,0 +1,22 @@ +name: IaC workflow + +on: + workflow_call: + secrets: + TF_API_TOKEN: + required: true + +jobs: + reusable_workflow_job: + runs-on: ubuntu-latest + name: IaC + steps: + - uses: actions/checkout@v2 + - uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} + - run: terraform init + - run: terraform fmt + - run: terraform validate + - run: terraform plan + - run: terraform apply -auto-approve \ No newline at end of file diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000000..4f569d4529 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,39 @@ +name: SonarCloud + +on: + workflow_call: + secrets: + SONAR_TOKEN: + required: true + + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Build and analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=leonardoscalabrini_desafio-dev \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..55f05f4862 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ + +upload/targetFile.tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..3781115e44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM openjdk:slim +LABEL maintainer="leonardo_scalabrini@hotmail.com" +ENV JAR_FILE=desafio-dev-0.0.1-SNAPSHOT.jar +COPY /target/${JAR_FILE} desafio-dev.jar +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["curl --fail --silent localhost:8080/actuator/health | grep UP || exit 1"] +ENTRYPOINT ["java","-jar","/desafio-dev.jar"] \ No newline at end of file diff --git a/README.md b/README.md index c2bfac4078..85e47016b1 100755 --- a/README.md +++ b/README.md @@ -1,85 +1,81 @@ -# Desafio programação - para vaga desenvolvedor - -Por favor leiam este documento do começo ao fim, com muita atenção. -O intuito deste teste é avaliar seus conhecimentos técnicos em programação. -O teste consiste em parsear [este arquivo de texto(CNAB)](https://github.com/ByCodersTec/desafio-ruby-on-rails/blob/master/CNAB.txt) e salvar suas informações(transações financeiras) em uma base de dados a critério do candidato. -Este desafio deve ser feito por você em sua casa. Gaste o tempo que você quiser, porém normalmente você não deve precisar de mais do que algumas horas. - -# Instruções de entrega do desafio - -1. Primeiro, faça um fork deste projeto para sua conta no Github (crie uma se você não possuir). -2. Em seguida, implemente o projeto tal qual descrito abaixo, em seu clone local. -3. Por fim, envie via email o projeto ou o fork/link do projeto para seu contato Bycoders_ com cópia para rh@bycoders.com.br. - -# Descrição do projeto - -Você recebeu um arquivo CNAB com os dados das movimentações finanaceira de várias lojas. -Precisamos criar uma maneira para que estes dados sejam importados para um banco de dados. - -Sua tarefa é criar uma interface web que aceite upload do [arquivo CNAB](https://github.com/ByCodersTec/desafio-ruby-on-rails/blob/master/CNAB.txt), normalize os dados e armazene-os em um banco de dados relacional e exiba essas informações em tela. - -**Sua aplicação web DEVE:** - -1. Ter uma tela (via um formulário) para fazer o upload do arquivo(pontos extras se não usar um popular CSS Framework ) -2. Interpretar ("parsear") o arquivo recebido, normalizar os dados, e salvar corretamente a informação em um banco de dados relacional, **se atente as documentações** que estão logo abaixo. -3. Exibir uma lista das operações importadas por lojas, e nesta lista deve conter um totalizador do saldo em conta -4. Ser escrita na sua linguagem de programação de preferência -5. Ser simples de configurar e rodar, funcionando em ambiente compatível com Unix (Linux ou Mac OS X). Ela deve utilizar apenas linguagens e bibliotecas livres ou gratuitas. -6. Git com commits atomicos e bem descritos -7. PostgreSQL, MySQL ou SQL Server -8. Ter testes automatizados -9. Docker compose (Pontos extras se utilizar) -10. Readme file descrevendo bem o projeto e seu setup -11. Incluir informação descrevendo como consumir o endpoint da API - -**Sua aplicação web não precisa:** - -1. Lidar com autenticação ou autorização (pontos extras se ela fizer, mais pontos extras se a autenticação for feita via OAuth). -2. Ser escrita usando algum framework específico (mas não há nada errado em usá-los também, use o que achar melhor). -3. Documentação da api.(Será um diferencial e pontos extras se fizer) - -# Documentação do CNAB - -| Descrição do campo | Inicio | Fim | Tamanho | Comentário -| ------------- | ------------- | -----| ---- | ------ -| Tipo | 1 | 1 | 1 | Tipo da transação -| Data | 2 | 9 | 8 | Data da ocorrência -| Valor | 10 | 19 | 10 | Valor da movimentação. *Obs.* O valor encontrado no arquivo precisa ser divido por cem(valor / 100.00) para normalizá-lo. -| CPF | 20 | 30 | 11 | CPF do beneficiário -| Cartão | 31 | 42 | 12 | Cartão utilizado na transação -| Hora | 43 | 48 | 6 | Hora da ocorrência atendendo ao fuso de UTC-3 -| Dono da loja | 49 | 62 | 14 | Nome do representante da loja -| Nome loja | 63 | 81 | 19 | Nome da loja - -# Documentação sobre os tipos das transações - -| Tipo | Descrição | Natureza | Sinal | -| ---- | -------- | --------- | ----- | -| 1 | Débito | Entrada | + | -| 2 | Boleto | Saída | - | -| 3 | Financiamento | Saída | - | -| 4 | Crédito | Entrada | + | -| 5 | Recebimento Empréstimo | Entrada | + | -| 6 | Vendas | Entrada | + | -| 7 | Recebimento TED | Entrada | + | -| 8 | Recebimento DOC | Entrada | + | -| 9 | Aluguel | Saída | - | - -# Avaliação - -Seu projeto será avaliado de acordo com os seguintes critérios. - -1. Sua aplicação preenche os requerimentos básicos? -2. Você documentou a maneira de configurar o ambiente e rodar sua aplicação? -3. Você seguiu as instruções de envio do desafio? -4. Qualidade e cobertura dos testes unitários. - -Adicionalmente, tentaremos verificar a sua familiarização com as bibliotecas padrões (standard libs), bem como sua experiência com programação orientada a objetos a partir da estrutura de seu projeto. - -# Referência - -Este desafio foi baseado neste outro desafio: https://github.com/lschallenges/data-engineering - ---- - -Boa sorte! +# desafio-dev + +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=leonardoscalabrini_desafio-dev&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=leonardoscalabrini_desafio-dev) +[][dockerhub] + +[dockerhub]: https://hub.docker.com/r/leonardoscalabrini/desafio-dev + +## Dev quick start ## + +1. Install dependencies +```` +mvn clean install +```` + +2. Install Infraestructure +```` +docker-compose up db +```` + +3. Start locally +```` +mvn spring-boot:run +```` + +## Build and run image ## + +1. Install Infraestructure and Image +```` +docker-compose up +```` + +## Build and run image ## + +1. Prepare minikube +```` +minikube tunnel +```` + +2. Apply kube +```` +kubectl apply -f .\kube\ +```` + + +# API Documentation # + +POST http://localhost:8080/api/v1/upload/cnab +```JSON +Content-Disposition: form-data; filename="example.txt" +``` + +GET http://localhost:8080/api/v1/store +```JSON +[ + { + "storeId": "9cb36ec1-e16d-487c-a1b8-fe59ba72f6d0", + "storeName": "BAR DO JOÃO", + "ownerName": "JOÃO MACEDO", + "storeBalance": -102.0 + } +] +``` + +GET http://localhost:8080/api/v1/transaction?storeId=9cb36ec1-e16d-487c-a1b8-fe59ba72f6d0 + +```JSON +[ + { + "transactionId": "9633630e-c320-4d1b-a916-382398eca093", + "storeId": "77ad1329-5d36-402d-b0c2-be8aa9dbbef7", + "type": "FINANCIAMENTO", + "date": "2019-03-01T12:34:53", + "transactionValue": 142.0, + "cpfNumber": "09620676017", + "creditCardNumber": "4753****3153", + "storeName": "BAR DO JOÃO", + "ownerName": "JOÃO MACEDO", + "storeBalance": -306.0 + } +] +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..b085d6675b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.4' +services: + db: + image: postgres + container_name: desafio-dev-db + restart: always + ports: + - "5432:5432" + volumes: + - db:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: dev + desafio-dev: + container_name: desafio-dev + build: + context: . + dockerfile: Dockerfile + ports: + - '8080:8080' + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://desafio-dev-db:5432/dev + - SPRING_DATASOURCE_USERNAME=postgres + - SPRING_DATASOURCE_PASSWORD=password + - SPRING_PROFILES_ACTIVE=develop + depends_on: + - db +volumes: + db: \ No newline at end of file diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100644 index 0000000000..887dfd9d54 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +echo "*****Running check code******" + +RELEASE=1.7 +JAR_NAME="google-java-format-${RELEASE}-all-deps.jar" +RELEASES_URL=https://repo1.maven.org/maven2/com/google/googlejavaformat/google-java-format +JAR_URL="${RELEASES_URL}/${RELEASE}/${JAR_NAME}" + +CACHE_DIR="./.cache/" +JAR_FILE="$CACHE_DIR/$JAR_NAME" + +echo $JAR_FILE +changed_java_files=$(git diff --cached --name-only --diff-filter=ACMR *.java || true) +if [[ -n "$changed_java_files" ]] +then + echo "Reformatting Java files: $changed_java_files" + java -jar "$JAR_FILE" --replace --set-exit-if-changed $changed_java_files +else + echo "No Java files changes found." +fi + + +status=$? + +echo "*****Done with check code******" + +exit $status diff --git a/hooks/pre-push b/hooks/pre-push new file mode 100644 index 0000000000..13632ac125 --- /dev/null +++ b/hooks/pre-push @@ -0,0 +1,11 @@ +#!/bin/sh + +echo "*****Running check tests******" + +mvn test + +status=$? + +echo "*****Done with check tests******" + +exit $status \ No newline at end of file diff --git a/kube/desafio-dev.yml b/kube/desafio-dev.yml new file mode 100644 index 0000000000..6eda6930dc --- /dev/null +++ b/kube/desafio-dev.yml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Service +metadata: + name: desafio-dev + labels: + app: desafio-dev +spec: + type: LoadBalancer + ports: + - port: 8080 + targetPort: 8080 + protocol: "TCP" + selector: + app: desafio-dev +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: desafio-dev + labels: + app: desafio-dev +spec: + replicas: 3 + selector: + matchLabels: + app: desafio-dev + template: + metadata: + labels: + app: desafio-dev + spec: + containers: + - name: desafio-dev + image: leonardoscalabrini/desafio-dev:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + env: + - name: SPRING_DATASOURCE_URL + value: "jdbc:postgresql://postgres:5432/db?useUnicode=yes&characterEncoding=UTF-8" + - name: SPRING_DATASOURCE_USERNAME + value: "postgres" + - name: SPRING_DATASOURCE_PASSWORD + value: "password" \ No newline at end of file diff --git a/kube/postgres.yml b/kube/postgres.yml new file mode 100644 index 0000000000..bd5ea6aea1 --- /dev/null +++ b/kube/postgres.yml @@ -0,0 +1,54 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres +spec: + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres + ports: + - containerPort: 5432 + volumeMounts: + - name: storage + mountPath: /data/db + env: + - name: POSTGRES_DB + value: db + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: password + volumes: + - name: storage + persistentVolumeClaim: + claimName: postgres-pvc \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000000..6fce084b74 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +config.stopBubbling=true +lombok.addLombokGeneratedAnnotation=true \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000000..d7c67f4eb7 --- /dev/null +++ b/main.tf @@ -0,0 +1,50 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "4.51.0" + } + } + backend "remote" { + organization = "leonardoscalabrini_github" + workspaces { + name = "desafio-dev" + } + } +} + +provider "google" { + credentials = var.cloud_credential + project = var.project_id + region = var.region + zone = var.zone +} + +resource "google_service_account" "default" { + account_id = "service-account-id" + display_name = "Service Account" +} + +resource "google_container_cluster" "primary" { + name = var.cluster_name + location = var.region + + remove_default_node_pool = true + initial_node_count = 1 +} + +resource "google_container_node_pool" "primary_preemptible_nodes" { + name = var.node_name + location = var.region + cluster = google_container_cluster.primary.name + node_count = 1 + + node_config { + preemptible = true + machine_type = "g1-small" + service_account = google_service_account.default.email + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform" + ] + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000..eab1460bf4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,146 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.0.RELEASE + + + com + desafio-dev + 0.0.1-SNAPSHOT + desafio-dev + Demo project for Spring Boot + + + UTF-8 + UTF-8 + 11 + 0.8.6 + 1.18.26 + 42.5.4 + jacoco + reuseReports + ${project.basedir}/../target/jacoco.exec + java + leonardoscalabrini + https://sonarcloud.io + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.google.cloud.sql + postgres-socket-factory + 1.1.0 + + + org.springframework.boot + spring-boot-starter-actuator + + + org.postgresql + postgresql + ${postgresql.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + test + + + com.jparams + to-string-verifier + 1.4.8 + test + + + nl.jqno.equalsverifier + equalsverifier + 3.15 + test + + + com.google.guava + guava-testlib + 31.1-jre + test + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + package + + report + + + + + + com.rudikershaw.gitbuildhook + git-build-hook-maven-plugin + 3.3.0 + + + hooks/ + true + + + + + + configure + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/com/desafiodev/DesafioDevApplication.java b/src/main/java/com/desafiodev/DesafioDevApplication.java new file mode 100644 index 0000000000..fb877fb6e6 --- /dev/null +++ b/src/main/java/com/desafiodev/DesafioDevApplication.java @@ -0,0 +1,12 @@ +package com.desafiodev; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DesafioDevApplication { + + public static void main(String[] args) { + SpringApplication.run(DesafioDevApplication.class, args); + } +} diff --git a/src/main/java/com/desafiodev/application/domains/Cnab.java b/src/main/java/com/desafiodev/application/domains/Cnab.java new file mode 100644 index 0000000000..6347553b07 --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/Cnab.java @@ -0,0 +1,106 @@ +package com.desafiodev.application.domains; + +import static java.time.format.DateTimeFormatter.ofPattern; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; +import static org.apache.commons.lang.StringUtils.isNumeric; + +import com.desafiodev.application.domains.exceptions.IllegalStateExceptionFactory; +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import lombok.NonNull; +import lombok.Value; +import org.apache.commons.lang.StringUtils; + +@Value +public class Cnab { + + private static final int LINE_LENGTH = 62; + private static final int TYPE_INDEX = 0; + private static final int[] DATE_INDEX = new int[] {1, 9}; + private static final int[] VALUE_INDEX = new int[] {9, 19}; + private static final int[] CPF_INDEX = new int[] {19, 30}; + private static final int[] CREDIT_CARD_INDEX = new int[] {30, 42}; + private static final int[] HOUR_INDEX = new int[] {42, 48}; + private static final int[] OWNER_INDEX = new int[] {48, 62}; + private static final int[] STORE_NAME_INDEX = new int[] {62, 80}; + TransactionType type; + Instant instant; + double value; + String cpf; + String creditCard; + String owner; + String storeName; + + private Cnab(@NonNull String line) { + String[] array = line.split(""); + + if (array.length < LINE_LENGTH) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("The line must has more than 62 characters") + .param("line", line) + .build(); + + this.type = parseTransactionType(array[TYPE_INDEX]); + this.instant = parseDateHour(collectFrom(array, DATE_INDEX), collectFrom(array, HOUR_INDEX)); + this.value = parseValue(collectFrom(array, VALUE_INDEX)); + this.cpf = collectFrom(array, CPF_INDEX); + this.creditCard = collectFrom(array, CREDIT_CARD_INDEX); + this.owner = collectFrom(array, OWNER_INDEX).trim(); + this.storeName = collectFrom(array, STORE_NAME_INDEX).trim(); + } + + private String collectFrom(String[] array, int[] index) { + return stream(array, index[0], index[1]).collect(joining()); + } + + private TransactionType parseTransactionType(String type) { + return TransactionType.getTransactionType(type) + .orElseThrow( + () -> + IllegalStateExceptionFactory.builder(getClass()) + .message("Transaction type not found") + .param("typeInt", type) + .build()); + } + + private Instant parseDateHour(@NonNull String date, @NonNull String hour) { + DateTimeFormatter formatter = ofPattern("yyyyMMddHHmmss"); + try { + LocalDateTime localDateTime = LocalDateTime.parse(date.concat(hour), formatter); + return Instant.parse(localDateTime.format(ofPattern("yyyy-MM-dd'T'HH:mm:ss")).concat(".00Z")) + .atZone(ZoneId.of("America/Sao_Paulo")) + .toInstant(); + } catch (DateTimeException e) { + throw IllegalStateExceptionFactory.builder(getClass()) + .message("Date invalid") + .param("exception", e) + .param("date", date) + .param("hour", hour) + .build(); + } + } + + private double parseValue(@NonNull String value) { + String number = StringUtils.stripStart(value, "0"); + if (number.isEmpty()) return 0; + if (!isNumeric(number)) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("Value invalid") + .param("value", value) + .build(); + + double result = Double.parseDouble(number); + + if (result != 0) return result / 100; + + return result; + } + + public static Cnab newInstance(@NonNull String line) { + return new Cnab(line); + } +} diff --git a/src/main/java/com/desafiodev/application/domains/Cpf.java b/src/main/java/com/desafiodev/application/domains/Cpf.java new file mode 100644 index 0000000000..85b1cfc090 --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/Cpf.java @@ -0,0 +1,29 @@ +package com.desafiodev.application.domains; + +import com.desafiodev.application.domains.exceptions.IllegalStateExceptionFactory; +import lombok.NonNull; +import lombok.Value; + +@Value +public class Cpf { + private static final int CPF_LENGTH = 11; + String number; + + private Cpf(@NonNull String number) { + if (number.isEmpty()) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("CPF can't be empty") + .param("number", number) + .build(); + if (number.length() != CPF_LENGTH) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("CPF must has 11 numbers") + .param("number", number) + .build(); + this.number = number; + } + + public static Cpf newInstance(@NonNull String number) { + return new Cpf(number); + } +} diff --git a/src/main/java/com/desafiodev/application/domains/CreditCard.java b/src/main/java/com/desafiodev/application/domains/CreditCard.java new file mode 100644 index 0000000000..52737eb1de --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/CreditCard.java @@ -0,0 +1,29 @@ +package com.desafiodev.application.domains; + +import com.desafiodev.application.domains.exceptions.IllegalStateExceptionFactory; +import lombok.NonNull; +import lombok.Value; + +@Value +public class CreditCard { + private static final int CREDIT_CARD_LENGTH = 12; + String number; + + private CreditCard(@NonNull String number) { + if (number.isEmpty()) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("Credit Card can't be empty") + .param("number", number) + .build(); + if (number.length() != CREDIT_CARD_LENGTH) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("Credit Card must has 12 numbers") + .param("number", number) + .build(); + this.number = number; + } + + public static CreditCard newInstance(@NonNull String number) { + return new CreditCard(number); + } +} diff --git a/src/main/java/com/desafiodev/application/domains/Store.java b/src/main/java/com/desafiodev/application/domains/Store.java new file mode 100644 index 0000000000..262873f222 --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/Store.java @@ -0,0 +1,55 @@ +package com.desafiodev.application.domains; + +import com.desafiodev.application.domains.exceptions.IllegalStateExceptionFactory; +import com.desafiodev.application.domains.ids.StoreId; +import lombok.NonNull; +import lombok.Value; + +@Value +public class Store { + + StoreId storeId; + String name; + String ownerName; + double balance; + + private Store( + @NonNull StoreId storeId, @NonNull String name, @NonNull String ownerName, double balance) { + if (name.isEmpty()) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("Name can't be empty") + .param("name", name) + .build(); + if (ownerName.isEmpty()) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("Owner name can't be empty") + .param("ownerName", ownerName) + .build(); + this.storeId = storeId; + this.name = name.toUpperCase(); + this.ownerName = ownerName.toUpperCase(); + this.balance = balance; + } + + public Store sum(@NonNull Transaction transaction) { + double newBalance = balance + transaction.getType().apply(transaction.getValue()); + return getInstance(this, newBalance); + } + + private static Store getInstance(@NonNull Store store, double balance) { + return new Store(store.getStoreId(), store.getName(), store.getOwnerName(), balance); + } + + public static Store getInstance( + @NonNull String id, @NonNull String name, @NonNull String ownerName, double balance) { + return new Store(StoreId.getInstance(id), name, ownerName, balance); + } + + public static Store newInstance(@NonNull String name, @NonNull String ownerName) { + return new Store(StoreId.newInstance(), name, ownerName, 0.0); + } + + public static Store from(Cnab cnab) { + return new Store(StoreId.newInstance(), cnab.getStoreName(), cnab.getOwner(), 0.0); + } +} diff --git a/src/main/java/com/desafiodev/application/domains/Transaction.java b/src/main/java/com/desafiodev/application/domains/Transaction.java new file mode 100644 index 0000000000..6f2fbcef82 --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/Transaction.java @@ -0,0 +1,58 @@ +package com.desafiodev.application.domains; + +import com.desafiodev.application.domains.exceptions.IllegalStateExceptionFactory; +import com.desafiodev.application.domains.ids.StoreId; +import com.desafiodev.application.domains.ids.TransactionId; +import java.time.Instant; +import lombok.NonNull; +import lombok.Value; + +@Value +public class Transaction { + + TransactionId transactionId; + StoreId storeId; + TransactionType type; + Instant date; + double value; + Cpf cpf; + CreditCard creditCard; + + private Transaction( + @NonNull TransactionType type, + @NonNull Instant date, + double value, + @NonNull Cpf cpf, + @NonNull CreditCard creditCard, + @NonNull StoreId storeId) { + if (value < 0) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("Value must be positive") + .param("value", value) + .build(); + this.transactionId = TransactionId.newInstance(); + this.type = type; + this.date = date; + this.value = value; + this.cpf = cpf; + this.creditCard = creditCard; + this.storeId = storeId; + } + + public static Transaction newInstance( + @NonNull TransactionType type, + @NonNull Instant date, + double value, + @NonNull Cpf cpf, + @NonNull CreditCard creditCard, + @NonNull StoreId storeId) { + return new Transaction(type, date, value, cpf, creditCard, storeId); + } + + public static Transaction parse(@NonNull Cnab cnab, @NonNull StoreId storeId) { + Cpf cpf = Cpf.newInstance(cnab.getCpf()); + CreditCard creditCard = CreditCard.newInstance(cnab.getCreditCard()); + return new Transaction( + cnab.getType(), cnab.getInstant(), cnab.getValue(), cpf, creditCard, storeId); + } +} diff --git a/src/main/java/com/desafiodev/application/domains/TransactionType.java b/src/main/java/com/desafiodev/application/domains/TransactionType.java new file mode 100644 index 0000000000..b9e8c77b98 --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/TransactionType.java @@ -0,0 +1,65 @@ +package com.desafiodev.application.domains; + +import static com.desafiodev.application.domains.TransactionType.MovimentType.ENTRADA; +import static com.desafiodev.application.domains.TransactionType.MovimentType.SAIDA; + +import com.desafiodev.application.domains.exceptions.IllegalStateExceptionFactory; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.UnaryOperator; +import lombok.NonNull; + +public enum TransactionType { + DEBITO("1", ENTRADA), + BOLETO("2", SAIDA), + FINANCIAMENTO("3", SAIDA), + CREDITO("4", ENTRADA), + RECEBIMENTO_EMPRESTIMO("5", ENTRADA), + VENDAS("6", ENTRADA), + RECEBIMENTO_TED("7", ENTRADA), + RECEBIMENTO_DOC("8", ENTRADA), + ALUGUEL("9", SAIDA); + private static final Map map = new HashMap<>(); + private final String cnabPosition; + + static { + Arrays.stream(TransactionType.values()) + .forEach(transactionType -> map.put(transactionType.cnabPosition, transactionType)); + } + + private final MovimentType movimentType; + + TransactionType(@NonNull String cnabPosition, @NonNull MovimentType movimentType) { + this.cnabPosition = cnabPosition; + this.movimentType = movimentType; + } + + public static Optional getTransactionType(@NonNull String cnabPosition) { + return Optional.ofNullable(map.get(cnabPosition)); + } + + public double apply(double value) { + return this.movimentType.apply(value); + } + + enum MovimentType { + ENTRADA(x -> x), + SAIDA(x -> x * -1); + private final UnaryOperator function; + + MovimentType(UnaryOperator function) { + this.function = function; + } + + double apply(double value) { + if (value < 0) + throw IllegalStateExceptionFactory.builder(getClass()) + .message("The transaction value, should be a positive value") + .param("value", value) + .build(); + return function.apply(value); + } + } +} diff --git a/src/main/java/com/desafiodev/application/domains/exceptions/IllegalStateExceptionFactory.java b/src/main/java/com/desafiodev/application/domains/exceptions/IllegalStateExceptionFactory.java new file mode 100644 index 0000000000..2626d4764f --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/exceptions/IllegalStateExceptionFactory.java @@ -0,0 +1,27 @@ +package com.desafiodev.application.domains.exceptions; + +public final class IllegalStateExceptionFactory { + private final StringBuilder stringBuilder = new StringBuilder(); + + private IllegalStateExceptionFactory(Class tClass) { + stringBuilder.append("class").append(tClass.getName()); + } + + public static IllegalStateExceptionFactory builder(Class tClass) { + return new IllegalStateExceptionFactory(tClass); + } + + public IllegalStateExceptionFactory param(String param, T value) { + stringBuilder.append(param).append(" ").append(value.toString()).append(" "); + return this; + } + + public IllegalStateExceptionFactory message(String massage) { + stringBuilder.append(massage).append(" "); + return this; + } + + public IllegalStateException build() { + return new IllegalStateException(stringBuilder.toString()); + } +} diff --git a/src/main/java/com/desafiodev/application/domains/ids/StoreId.java b/src/main/java/com/desafiodev/application/domains/ids/StoreId.java new file mode 100644 index 0000000000..08517d7b97 --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/ids/StoreId.java @@ -0,0 +1,22 @@ +package com.desafiodev.application.domains.ids; + +import java.util.UUID; +import lombok.NonNull; +import lombok.Value; + +@Value +public class StoreId { + String id; + + private StoreId(@NonNull String id) { + this.id = id; + } + + public static StoreId newInstance() { + return new StoreId(UUID.randomUUID().toString()); + } + + public static StoreId getInstance(@NonNull String id) { + return new StoreId(id); + } +} diff --git a/src/main/java/com/desafiodev/application/domains/ids/TransactionId.java b/src/main/java/com/desafiodev/application/domains/ids/TransactionId.java new file mode 100644 index 0000000000..a4747d1e61 --- /dev/null +++ b/src/main/java/com/desafiodev/application/domains/ids/TransactionId.java @@ -0,0 +1,22 @@ +package com.desafiodev.application.domains.ids; + +import java.util.UUID; +import lombok.NonNull; +import lombok.Value; + +@Value +public class TransactionId { + String id; + + private TransactionId(@NonNull String id) { + this.id = id; + } + + public static TransactionId newInstance() { + return new TransactionId(UUID.randomUUID().toString()); + } + + public static TransactionId getInstance(@NonNull String id) { + return new TransactionId(id); + } +} diff --git a/src/main/java/com/desafiodev/application/ports/in/UploadService.java b/src/main/java/com/desafiodev/application/ports/in/UploadService.java new file mode 100644 index 0000000000..e8109807d4 --- /dev/null +++ b/src/main/java/com/desafiodev/application/ports/in/UploadService.java @@ -0,0 +1,7 @@ +package com.desafiodev.application.ports.in; + +import java.io.File; + +public interface UploadService { + void accept(File uploadFile); +} diff --git a/src/main/java/com/desafiodev/application/ports/out/StoreRepository.java b/src/main/java/com/desafiodev/application/ports/out/StoreRepository.java new file mode 100644 index 0000000000..4b13466d1c --- /dev/null +++ b/src/main/java/com/desafiodev/application/ports/out/StoreRepository.java @@ -0,0 +1,10 @@ +package com.desafiodev.application.ports.out; + +import com.desafiodev.application.domains.Store; +import java.util.Optional; + +public interface StoreRepository { + Optional findByNameAndOwnerName(String storeName, String ownerName); + + void save(Store store); +} diff --git a/src/main/java/com/desafiodev/application/ports/out/TransactionRepository.java b/src/main/java/com/desafiodev/application/ports/out/TransactionRepository.java new file mode 100644 index 0000000000..3de1f9d5d6 --- /dev/null +++ b/src/main/java/com/desafiodev/application/ports/out/TransactionRepository.java @@ -0,0 +1,9 @@ +package com.desafiodev.application.ports.out; + +import com.desafiodev.application.domains.Store; +import com.desafiodev.application.domains.Transaction; + +public interface TransactionRepository { + + void save(Transaction transaction, Store store); +} diff --git a/src/main/java/com/desafiodev/application/services/CnabUploadServiceImpl.java b/src/main/java/com/desafiodev/application/services/CnabUploadServiceImpl.java new file mode 100644 index 0000000000..faca87d6fb --- /dev/null +++ b/src/main/java/com/desafiodev/application/services/CnabUploadServiceImpl.java @@ -0,0 +1,56 @@ +package com.desafiodev.application.services; + +import com.desafiodev.application.domains.Cnab; +import com.desafiodev.application.domains.Store; +import com.desafiodev.application.domains.Transaction; +import com.desafiodev.application.domains.exceptions.IllegalStateExceptionFactory; +import com.desafiodev.application.ports.in.UploadService; +import com.desafiodev.application.ports.out.StoreRepository; +import com.desafiodev.application.ports.out.TransactionRepository; +import java.io.*; +import java.nio.charset.StandardCharsets; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class CnabUploadServiceImpl implements UploadService { + + private final TransactionRepository transactionRepository; + + private final StoreRepository storeRepository; + + @Autowired + public CnabUploadServiceImpl( + TransactionRepository transactionRepository, StoreRepository storeRepository) { + this.transactionRepository = transactionRepository; + this.storeRepository = storeRepository; + } + + @Override + public void accept(@NonNull File uploadFile) { + try { + BufferedReader reader = + new BufferedReader( + new InputStreamReader(new FileInputStream(uploadFile), StandardCharsets.UTF_8)); + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + Cnab cnab = Cnab.newInstance(line); + Store store = + storeRepository + .findByNameAndOwnerName(cnab.getStoreName(), cnab.getOwner()) + .orElse(Store.from(cnab)); + Transaction transaction = Transaction.parse(Cnab.newInstance(line), store.getStoreId()); + Store newStore = store.sum(transaction); + storeRepository.save(newStore); + transactionRepository.save(transaction, newStore); + } + reader.close(); + } catch (IOException e) { + throw IllegalStateExceptionFactory.builder(getClass()) + .message("File not accepted") + .param("exception", e) + .param("file", uploadFile) + .build(); + } + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/api/commons/ApiError.java b/src/main/java/com/desafiodev/infrastructure/api/commons/ApiError.java new file mode 100644 index 0000000000..421db798b5 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/api/commons/ApiError.java @@ -0,0 +1,16 @@ +package com.desafiodev.infrastructure.api.commons; + +import java.time.Instant; +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +@Builder +@Value +@RequiredArgsConstructor +public class ApiError { + Instant timestamp = Instant.now(); + int status; + String error; + String message; +} diff --git a/src/main/java/com/desafiodev/infrastructure/api/commons/RestExceptionHandler.java b/src/main/java/com/desafiodev/infrastructure/api/commons/RestExceptionHandler.java new file mode 100644 index 0000000000..71bd44f8bb --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/api/commons/RestExceptionHandler.java @@ -0,0 +1,31 @@ +package com.desafiodev.infrastructure.api.commons; + +import java.io.IOException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class RestExceptionHandler { + + private ResponseEntity handleException(Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + ApiError.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .message(ex.getMessage()) + .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) + .build()); + } + + @ExceptionHandler(IllegalStateException.class) + private ResponseEntity handleIllegalStateException(IllegalStateException ex) { + return handleException(ex); + } + + @ExceptionHandler(IOException.class) + private ResponseEntity handleIOException(IllegalStateException ex) { + return handleException(ex); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/api/dtos/StoreResponseDTO.java b/src/main/java/com/desafiodev/infrastructure/api/dtos/StoreResponseDTO.java new file mode 100644 index 0000000000..4b7e1453ef --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/api/dtos/StoreResponseDTO.java @@ -0,0 +1,34 @@ +package com.desafiodev.infrastructure.api.dtos; + +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import java.util.List; +import java.util.stream.Collectors; +import lombok.NonNull; +import lombok.Value; + +@Value +public class StoreResponseDTO { + String storeId; + + String storeName; + + String ownerName; + + double storeBalance; + + private StoreResponseDTO( + String storeId, String storeName, String ownerName, double storeBalance) { + this.storeId = storeId; + this.storeName = storeName; + this.ownerName = ownerName; + this.storeBalance = storeBalance; + } + + private static StoreResponseDTO getInstance(@NonNull StoreEntity s) { + return new StoreResponseDTO(s.getId(), s.getName(), s.getOwnerName(), s.getBalance()); + } + + public static List asList(@NonNull List storeEntities) { + return storeEntities.stream().map(StoreResponseDTO::getInstance).collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/api/dtos/TransactionResponseDTO.java b/src/main/java/com/desafiodev/infrastructure/api/dtos/TransactionResponseDTO.java new file mode 100644 index 0000000000..9c4b481b72 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/api/dtos/TransactionResponseDTO.java @@ -0,0 +1,50 @@ +package com.desafiodev.infrastructure.api.dtos; + +import com.desafiodev.infrastructure.repositories.entities.TransactionEntity; +import java.util.List; +import java.util.stream.Collectors; +import lombok.NonNull; +import lombok.Value; + +@Value +public class TransactionResponseDTO { + + String transactionId; + String type; + String date; + double transactionValue; + String cpfNumber; + String creditCardNumber; + + private TransactionResponseDTO( + String transactionId, + String type, + String date, + double transactionValue, + String cpfNumber, + String creditCardNumber) { + this.transactionId = transactionId; + this.type = type; + this.date = date; + this.transactionValue = transactionValue; + this.cpfNumber = cpfNumber; + this.creditCardNumber = creditCardNumber; + } + + private static TransactionResponseDTO getInstance(@NonNull TransactionEntity t) { + return new TransactionResponseDTO( + t.getId(), + t.getType().name(), + t.getDate().toString(), + t.getValue(), + t.getCpf(), + t.getCreditCard()); + } + + public static List asList( + @NonNull List transactionEntities) { + return transactionEntities.stream() + .map(TransactionResponseDTO::getInstance) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/StoreController.java b/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/StoreController.java new file mode 100644 index 0000000000..6a44babfee --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/StoreController.java @@ -0,0 +1,32 @@ +package com.desafiodev.infrastructure.api.v1.controllers; + +import com.desafiodev.infrastructure.api.dtos.StoreResponseDTO; +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.infrastructure.repositories.jpas.StoreEntityJpaRepository; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@CrossOrigin +@RestController() +@RequestMapping("api/v1/store") +public class StoreController { + + private final StoreEntityJpaRepository storeEntityJpaRepository; + + @Autowired + public StoreController(StoreEntityJpaRepository storeEntityJpaRepository) { + this.storeEntityJpaRepository = storeEntityJpaRepository; + } + + @GetMapping() + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> findAll() { + List entities = storeEntityJpaRepository.findAll(); + if (entities.isEmpty()) return ResponseEntity.noContent().build(); + + return ResponseEntity.ok(StoreResponseDTO.asList(entities)); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/TransactionController.java b/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/TransactionController.java new file mode 100644 index 0000000000..b2c060f1b3 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/TransactionController.java @@ -0,0 +1,45 @@ +package com.desafiodev.infrastructure.api.v1.controllers; + +import com.desafiodev.infrastructure.api.dtos.TransactionResponseDTO; +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.infrastructure.repositories.entities.TransactionEntity; +import com.desafiodev.infrastructure.repositories.jpas.StoreEntityJpaRepository; +import com.desafiodev.infrastructure.repositories.jpas.TransactionEntityJpaRepository; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@CrossOrigin +@RestController() +@RequestMapping("api/v1/transaction") +public class TransactionController { + + private final TransactionEntityJpaRepository transactionEntityJpaRepository; + private final StoreEntityJpaRepository storeEntityJpaRepository; + + @Autowired + public TransactionController( + TransactionEntityJpaRepository transactionEntityJpaRepository, + StoreEntityJpaRepository storeEntityJpaRepository) { + this.transactionEntityJpaRepository = transactionEntityJpaRepository; + this.storeEntityJpaRepository = storeEntityJpaRepository; + } + + @GetMapping() + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> findByStore(@RequestParam String storeId) { + + Optional optionalStoreEntity = storeEntityJpaRepository.findById(storeId); + + if (optionalStoreEntity.isEmpty()) return ResponseEntity.noContent().build(); + + List entities = + transactionEntityJpaRepository.findByStore(optionalStoreEntity.get()); + if (entities.isEmpty()) return ResponseEntity.noContent().build(); + + return ResponseEntity.ok(TransactionResponseDTO.asList(entities)); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/UploadController.java b/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/UploadController.java new file mode 100644 index 0000000000..18fc112700 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/api/v1/controllers/UploadController.java @@ -0,0 +1,31 @@ +package com.desafiodev.infrastructure.api.v1.controllers; + +import com.desafiodev.application.ports.in.UploadService; +import com.desafiodev.infrastructure.storeges.interfaces.StorageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@CrossOrigin +@RestController() +@RequestMapping("api/v1/upload") +public class UploadController { + private final UploadService uploadService; + + private final StorageService storageService; + + @Autowired + public UploadController(UploadService uploadService, StorageService storageService) { + this.uploadService = uploadService; + this.storageService = storageService; + } + + @PostMapping("/cnab") + @ResponseStatus(HttpStatus.ACCEPTED) + public @ResponseBody ResponseEntity cnab(@RequestParam("file") MultipartFile file) { + uploadService.accept(storageService.save(file)); + return ResponseEntity.accepted().body("File uploaded successfully."); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/configurations/UploadConfigurationImpl.java b/src/main/java/com/desafiodev/infrastructure/configurations/UploadConfigurationImpl.java new file mode 100644 index 0000000000..8b7b2ee88a --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/configurations/UploadConfigurationImpl.java @@ -0,0 +1,29 @@ +package com.desafiodev.infrastructure.configurations; + +import com.desafiodev.infrastructure.configurations.interfaces.UploadConfiguration; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class UploadConfigurationImpl implements UploadConfiguration { + private final String uploadPath; + private final String filename; + + public UploadConfigurationImpl( + @NonNull @Value("${upload.path}") String uploadPath, + @NonNull @Value("${upload.file}") String filename) { + this.uploadPath = uploadPath; + this.filename = filename; + } + + @Override + public String getPathname() { + return uploadPath; + } + + @Override + public String getFilename() { + return filename; + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/configurations/interfaces/UploadConfiguration.java b/src/main/java/com/desafiodev/infrastructure/configurations/interfaces/UploadConfiguration.java new file mode 100644 index 0000000000..7806e5fecd --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/configurations/interfaces/UploadConfiguration.java @@ -0,0 +1,8 @@ +package com.desafiodev.infrastructure.configurations.interfaces; + +public interface UploadConfiguration { + + String getPathname(); + + String getFilename(); +} diff --git a/src/main/java/com/desafiodev/infrastructure/repositories/StoreRepositoryImpl.java b/src/main/java/com/desafiodev/infrastructure/repositories/StoreRepositoryImpl.java new file mode 100644 index 0000000000..bd65814b70 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/repositories/StoreRepositoryImpl.java @@ -0,0 +1,34 @@ +package com.desafiodev.infrastructure.repositories; + +import com.desafiodev.application.domains.Store; +import com.desafiodev.application.ports.out.StoreRepository; +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.infrastructure.repositories.jpas.StoreEntityJpaRepository; +import java.util.Optional; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class StoreRepositoryImpl implements StoreRepository { + + private final StoreEntityJpaRepository storeEntityJpaRepository; + + @Autowired + public StoreRepositoryImpl(StoreEntityJpaRepository storeEntityJpaRepository) { + this.storeEntityJpaRepository = storeEntityJpaRepository; + } + + @Override + public Optional findByNameAndOwnerName( + @NonNull String storeName, @NonNull String ownerName) { + return storeEntityJpaRepository + .findByNameIgnoreCaseAndOwnerNameIgnoreCase(storeName, ownerName) + .map(StoreEntity::getStore); + } + + @Override + public void save(@NonNull Store store) { + storeEntityJpaRepository.save(StoreEntity.from(store)); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/repositories/TransactionRepositoryImpl.java b/src/main/java/com/desafiodev/infrastructure/repositories/TransactionRepositoryImpl.java new file mode 100644 index 0000000000..9497b4e283 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/repositories/TransactionRepositoryImpl.java @@ -0,0 +1,26 @@ +package com.desafiodev.infrastructure.repositories; + +import com.desafiodev.application.domains.Store; +import com.desafiodev.application.domains.Transaction; +import com.desafiodev.application.ports.out.TransactionRepository; +import com.desafiodev.infrastructure.repositories.entities.TransactionEntity; +import com.desafiodev.infrastructure.repositories.jpas.TransactionEntityJpaRepository; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class TransactionRepositoryImpl implements TransactionRepository { + + private final TransactionEntityJpaRepository transactionEntityJpaRepository; + + @Autowired + public TransactionRepositoryImpl(TransactionEntityJpaRepository transactionEntityJpaRepository) { + this.transactionEntityJpaRepository = transactionEntityJpaRepository; + } + + @Override + public void save(@NonNull Transaction transaction, @NonNull Store store) { + transactionEntityJpaRepository.save(TransactionEntity.from(transaction, store)); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/repositories/entities/StoreEntity.java b/src/main/java/com/desafiodev/infrastructure/repositories/entities/StoreEntity.java new file mode 100644 index 0000000000..9d8aa56bbf --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/repositories/entities/StoreEntity.java @@ -0,0 +1,46 @@ +package com.desafiodev.infrastructure.repositories.entities; + +import com.desafiodev.application.domains.Store; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Entity(name = "store") +@Getter +@ToString +@EqualsAndHashCode +@Table( + uniqueConstraints = { + @UniqueConstraint( + name = "UniqueNameAndOwnerName", + columnNames = {"name", "ownerName"}) + }) +public class StoreEntity { + @Id @EqualsAndHashCode.Exclude private String id; + + @NotBlank private String name; + @NotBlank private String ownerName; + + @NotNull private double balance; + + public StoreEntity() {} + + private StoreEntity(String id, String name, String ownerName, double balance) { + this.id = id; + this.name = name; + this.ownerName = ownerName; + this.balance = balance; + } + + public Store getStore() { + return Store.getInstance(id, name, ownerName, balance); + } + + public static StoreEntity from(Store store) { + return new StoreEntity( + store.getStoreId().getId(), store.getName(), store.getOwnerName(), store.getBalance()); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/repositories/entities/TransactionEntity.java b/src/main/java/com/desafiodev/infrastructure/repositories/entities/TransactionEntity.java new file mode 100644 index 0000000000..b4defcce84 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/repositories/entities/TransactionEntity.java @@ -0,0 +1,71 @@ +package com.desafiodev.infrastructure.repositories.entities; + +import com.desafiodev.application.domains.Store; +import com.desafiodev.application.domains.Transaction; +import com.desafiodev.application.domains.TransactionType; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import org.hibernate.annotations.Immutable; + +@Immutable +@Entity(name = "transactions") +@Getter +@ToString +@EqualsAndHashCode +public class TransactionEntity { + @Id @EqualsAndHashCode.Exclude private String id; + + @ManyToOne + @JoinColumn(name = "fk_store") + private StoreEntity store; + + @NotNull + @Enumerated(EnumType.STRING) + private TransactionType type; + + @NotNull private LocalDateTime date; + private double value; + + @NotBlank private String cpf; + + @NotBlank private String creditCard; + + public TransactionEntity() {} + + private TransactionEntity( + @NonNull String id, + @NonNull TransactionType type, + @NonNull LocalDateTime date, + double value, + @NonNull String cpf, + @NonNull String creditCard, + @NonNull StoreEntity store) { + this.id = id; + this.type = type; + this.date = date; + this.value = value; + this.cpf = cpf; + this.creditCard = creditCard; + this.store = store; + } + + public static TransactionEntity from(Transaction transaction, Store store) { + return new TransactionEntity( + transaction.getTransactionId().getId(), + transaction.getType(), + LocalDateTime.ofInstant( + transaction.getDate().truncatedTo(ChronoUnit.MILLIS), ZoneId.of("America/Sao_Paulo")), + transaction.getValue(), + transaction.getCpf().getNumber(), + transaction.getCreditCard().getNumber(), + StoreEntity.from(store)); + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/repositories/jpas/StoreEntityJpaRepository.java b/src/main/java/com/desafiodev/infrastructure/repositories/jpas/StoreEntityJpaRepository.java new file mode 100644 index 0000000000..4a925de634 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/repositories/jpas/StoreEntityJpaRepository.java @@ -0,0 +1,11 @@ +package com.desafiodev.infrastructure.repositories.jpas; + +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface StoreEntityJpaRepository extends JpaRepository { + Optional findByNameIgnoreCaseAndOwnerNameIgnoreCase(String name, String ownerName); +} diff --git a/src/main/java/com/desafiodev/infrastructure/repositories/jpas/TransactionEntityJpaRepository.java b/src/main/java/com/desafiodev/infrastructure/repositories/jpas/TransactionEntityJpaRepository.java new file mode 100644 index 0000000000..473e11ad5a --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/repositories/jpas/TransactionEntityJpaRepository.java @@ -0,0 +1,12 @@ +package com.desafiodev.infrastructure.repositories.jpas; + +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.infrastructure.repositories.entities.TransactionEntity; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TransactionEntityJpaRepository extends JpaRepository { + List findByStore(StoreEntity store); +} diff --git a/src/main/java/com/desafiodev/infrastructure/storeges/LocalStorageServiceImpl.java b/src/main/java/com/desafiodev/infrastructure/storeges/LocalStorageServiceImpl.java new file mode 100644 index 0000000000..da671dd40a --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/storeges/LocalStorageServiceImpl.java @@ -0,0 +1,39 @@ +package com.desafiodev.infrastructure.storeges; + +import com.desafiodev.application.domains.exceptions.IllegalStateExceptionFactory; +import com.desafiodev.infrastructure.configurations.interfaces.UploadConfiguration; +import com.desafiodev.infrastructure.storeges.interfaces.StorageService; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class LocalStorageServiceImpl implements StorageService { + + private final UploadConfiguration uploadConfiguration; + + @Autowired + public LocalStorageServiceImpl(UploadConfiguration uploadConfiguration) { + this.uploadConfiguration = uploadConfiguration; + } + + @Override + public File save(MultipartFile multipartFile) { + Path root = Paths.get(this.uploadConfiguration.getPathname()); + try { + if (!Files.exists(root)) Files.createDirectories(root); + Path patch = root.resolve(uploadConfiguration.getFilename()); + multipartFile.transferTo(patch); + return patch.toFile(); + } catch (IOException e) { + throw IllegalStateExceptionFactory.builder(getClass()) + .message("Could not initialize folder for upload!") + .build(); + } + } +} diff --git a/src/main/java/com/desafiodev/infrastructure/storeges/interfaces/StorageService.java b/src/main/java/com/desafiodev/infrastructure/storeges/interfaces/StorageService.java new file mode 100644 index 0000000000..63caf6c237 --- /dev/null +++ b/src/main/java/com/desafiodev/infrastructure/storeges/interfaces/StorageService.java @@ -0,0 +1,8 @@ +package com.desafiodev.infrastructure.storeges.interfaces; + +import java.io.File; +import org.springframework.web.multipart.MultipartFile; + +public interface StorageService { + File save(MultipartFile multipartFile); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000000..45a6c4c4c2 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,10 @@ +spring.jpa.database=POSTGRESQL +spring.datasource.platform=postgres +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=create +spring.database.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/dev?useUnicode=yes&characterEncoding=UTF-8 +spring.datasource.username=postgres +spring.datasource.password=password +upload.path=upload +upload.file=targetFile.tmp \ No newline at end of file diff --git a/src/test/java/com/desafiodev/application/domains/CnabTest.java b/src/test/java/com/desafiodev/application/domains/CnabTest.java new file mode 100644 index 0000000000..60c37c4532 --- /dev/null +++ b/src/test/java/com/desafiodev/application/domains/CnabTest.java @@ -0,0 +1,58 @@ +package com.desafiodev.application.domains; + +import static com.desafiodev.application.domains.TransactionType.FINANCIAMENTO; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.desafiodev.utils.Fixture; +import com.desafiodev.utils.UtilsTest; +import java.time.Instant; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EmptySource; + +class CnabTest extends UtilsTest { + + @Test + void testClass() { + Cnab cnab = Fixture.getCnab(); + assertClass(Cnab.class, Fixture.getCnab()); + assertEquals(FINANCIAMENTO, cnab.getType()); + assertEquals(Instant.parse("2019-03-01T15:34:53.00Z"), cnab.getInstant()); + assertEquals(142.00, cnab.getValue()); + assertEquals("09620676017", cnab.getCpf()); + assertEquals("4753****3153", cnab.getCreditCard()); + assertEquals("JOÃO MACEDO", cnab.getOwner()); + assertEquals("BAR DO JOÃO", cnab.getStoreName()); + } + + @ParameterizedTest + @EmptySource + @CsvSource({ + "6760174753****3153153453JOÃO MACEDO BAR DO JOÃO ", + "0201903010000014200096206760174753****3153153453JOÃO MACEDO BAR DO JOÃO *", + "3201913010000014200096206760174753****3153153453JOÃO MACEDO BAR DO JOÃO *", + "3201903010000014200096206760174753****3153283453JOÃO MACEDO BAR DO JOÃO *", + "32019030100000ABC00096206760174753****3153153453JOÃO MACEDO BAR DO JOÃO *", + }) + void getInstanceWithError(String line) { + assertThrows(IllegalStateException.class, () -> Cnab.newInstance(line)); + } + + @Test + void valueEmpty() { + Cnab cnab = + Cnab.newInstance( + "3201903010000000000096206760174753****3153153453JOÃO MACEDO BAR DO JOÃO *"); + assertEquals(0, cnab.getValue()); + } + + @Test + void testHour() { + Cnab cnab = + Cnab.newInstance( + "1201903010000015200096206760171234****7890233000JOÃO MACEDO BAR DO JOÃO "); + assertEquals(Instant.parse("2019-03-01T23:30:00.00Z"), cnab.getInstant()); + } +} diff --git a/src/test/java/com/desafiodev/application/domains/CpfTest.java b/src/test/java/com/desafiodev/application/domains/CpfTest.java new file mode 100644 index 0000000000..ba2350ebdb --- /dev/null +++ b/src/test/java/com/desafiodev/application/domains/CpfTest.java @@ -0,0 +1,27 @@ +package com.desafiodev.application.domains; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.desafiodev.utils.UtilsTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EmptySource; + +class CpfTest extends UtilsTest { + + @Test + void getInstance() { + Cpf cpf = Cpf.newInstance("22222222222"); + assertClass(Cpf.class, cpf); + assertEquals("22222222222", cpf.getNumber()); + } + + @ParameterizedTest + @EmptySource + @CsvSource("2222222222") + void getInstanceWithError(String number) { + assertThrows(IllegalStateException.class, () -> Cpf.newInstance(number)); + } +} diff --git a/src/test/java/com/desafiodev/application/domains/CreditCardTest.java b/src/test/java/com/desafiodev/application/domains/CreditCardTest.java new file mode 100644 index 0000000000..7606ee1b59 --- /dev/null +++ b/src/test/java/com/desafiodev/application/domains/CreditCardTest.java @@ -0,0 +1,27 @@ +package com.desafiodev.application.domains; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.desafiodev.utils.UtilsTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EmptySource; + +class CreditCardTest extends UtilsTest { + + @Test + void getInstance() { + CreditCard creditCard = CreditCard.newInstance("222222222222"); + assertClass(CreditCard.class, creditCard); + assertEquals("222222222222", creditCard.getNumber()); + } + + @ParameterizedTest + @EmptySource + @CsvSource("2222222222") + void getInstanceWithError(String number) { + assertThrows(IllegalStateException.class, () -> CreditCard.newInstance(number)); + } +} diff --git a/src/test/java/com/desafiodev/application/domains/StoreTest.java b/src/test/java/com/desafiodev/application/domains/StoreTest.java new file mode 100644 index 0000000000..7f944efbd4 --- /dev/null +++ b/src/test/java/com/desafiodev/application/domains/StoreTest.java @@ -0,0 +1,46 @@ +package com.desafiodev.application.domains; + +import static org.junit.jupiter.api.Assertions.*; + +import com.desafiodev.utils.Fixture; +import com.desafiodev.utils.UtilsTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class StoreTest extends UtilsTest { + + @Test + void store() { + Store store = Store.newInstance("Name", "OwnerName"); + assertClass(Store.class, store); + assertNotNull(store.getStoreId()); + assertEquals("NAME", store.getName()); + assertEquals("OWNERNAME", store.getOwnerName()); + assertEquals(0.0, store.getBalance()); + } + + @Test + void from() { + Store store = Store.from(Fixture.getCnab()); + assertNotNull(store.getStoreId()); + assertEquals("BAR DO JOÃO", store.getName()); + assertEquals("JOÃO MACEDO", store.getOwnerName()); + assertEquals(0.0, store.getBalance()); + } + + @ParameterizedTest + @CsvSource({"DEBITO, 10, 10", "BOLETO, 10, -10"}) + void sum(TransactionType type, double transactionValue, double expected) { + Store store = Fixture.getStore(); + Transaction transaction = Fixture.getTransaction(type, transactionValue); + assertEquals(expected, store.sum(transaction).getBalance()); + assertEquals(0, store.getBalance()); + } + + @Test + void storeWithError() { + assertThrows(IllegalStateException.class, () -> Store.newInstance("", "OwnerName")); + assertThrows(IllegalStateException.class, () -> Store.newInstance("Name", "")); + } +} diff --git a/src/test/java/com/desafiodev/application/domains/TransactionTest.java b/src/test/java/com/desafiodev/application/domains/TransactionTest.java new file mode 100644 index 0000000000..b7eb28574f --- /dev/null +++ b/src/test/java/com/desafiodev/application/domains/TransactionTest.java @@ -0,0 +1,62 @@ +package com.desafiodev.application.domains; + +import static org.junit.jupiter.api.Assertions.*; + +import com.desafiodev.application.domains.ids.StoreId; +import com.desafiodev.utils.Fixture; +import com.desafiodev.utils.UtilsTest; +import java.time.Instant; +import java.time.ZoneId; +import org.junit.jupiter.api.Test; + +class TransactionTest extends UtilsTest { + + @Test + void builder() { + Cpf cpf = Fixture.getCpf(); + TransactionType transactionType = Fixture.getTransactionType(); + CreditCard creditCard = Fixture.getCreditCard(); + Instant instant = Instant.now(); + StoreId storeId = Fixture.getStoreId(); + Transaction transaction = + Transaction.newInstance(transactionType, instant, 10, cpf, creditCard, storeId); + assertClass(Transaction.class, transaction); + assertNotNull(transaction.getTransactionId()); + assertEquals(cpf, transaction.getCpf()); + assertEquals(transactionType, transaction.getType()); + assertEquals(creditCard, transaction.getCreditCard()); + assertEquals(instant, transaction.getDate()); + assertEquals(10, transaction.getValue()); + assertEquals(storeId, transaction.getStoreId()); + } + + @Test + void builderWithError() { + Cpf cpf = Fixture.getCpf(); + TransactionType transactionType = Fixture.getTransactionType(); + CreditCard creditCard = Fixture.getCreditCard(); + Instant instant = Instant.now(); + StoreId storeId = Fixture.getStoreId(); + assertThrows( + IllegalStateException.class, + () -> Transaction.newInstance(transactionType, instant, -10, cpf, creditCard, storeId)); + } + + @Test + void parse() { + Cpf cpf = Cpf.newInstance("09620676017"); + TransactionType transactionType = TransactionType.FINANCIAMENTO; + CreditCard creditCard = CreditCard.newInstance("4753****3153"); + Instant instant = + Instant.parse("2019-03-01T15:34:53.00Z").atZone(ZoneId.of("America/Sao_Paulo")).toInstant(); + StoreId storeId = Fixture.getStoreId(); + Transaction transaction = Transaction.parse(Fixture.getCnab(), storeId); + assertNotNull(transaction.getTransactionId()); + assertEquals(cpf, transaction.getCpf()); + assertEquals(transactionType, transaction.getType()); + assertEquals(creditCard, transaction.getCreditCard()); + assertEquals(instant, transaction.getDate()); + assertEquals(142.00, transaction.getValue()); + assertEquals(storeId, transaction.getStoreId()); + } +} diff --git a/src/test/java/com/desafiodev/application/domains/TransactionTypeTest.java b/src/test/java/com/desafiodev/application/domains/TransactionTypeTest.java new file mode 100644 index 0000000000..ccdc6e7eac --- /dev/null +++ b/src/test/java/com/desafiodev/application/domains/TransactionTypeTest.java @@ -0,0 +1,35 @@ +package com.desafiodev.application.domains; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Optional; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class TransactionTypeTest { + + @ParameterizedTest + @CsvSource({ + "0, false, DEBITO, 0", + "1, true, DEBITO, 10", + "2, true, BOLETO, -10", + "3, true, FINANCIAMENTO, -10", + "4, true, CREDITO, 10", + "5, true, RECEBIMENTO_EMPRESTIMO, 10", + "6, true, VENDAS, 10", + "7, true, RECEBIMENTO_TED, 10", + "8, true, RECEBIMENTO_DOC, 10", + "9, true, ALUGUEL, -10" + }) + void transactionTypeTest( + String cnabPosition, boolean expected, TransactionType transactionType, int expectedValue) { + Optional optional = TransactionType.getTransactionType(cnabPosition); + assertEquals(expected, optional.isPresent()); + if (!expected) return; + TransactionType result = optional.orElseThrow(); + assertEquals(transactionType, result); + assertEquals(expectedValue, result.apply(10)); + assertThrows(IllegalStateException.class, () -> result.apply(-10)); + } +} diff --git a/src/test/java/com/desafiodev/application/domains/ids/StoreIdTest.java b/src/test/java/com/desafiodev/application/domains/ids/StoreIdTest.java new file mode 100644 index 0000000000..ceda6ab73e --- /dev/null +++ b/src/test/java/com/desafiodev/application/domains/ids/StoreIdTest.java @@ -0,0 +1,20 @@ +package com.desafiodev.application.domains.ids; + +import static org.junit.jupiter.api.Assertions.*; + +import com.desafiodev.utils.UtilsTest; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class StoreIdTest extends UtilsTest { + + @Test + void storeId() { + assertClass(StoreId.class, StoreId.newInstance()); + var id = StoreId.newInstance(); + var uuid = UUID.randomUUID().toString(); + assertNotEquals(id, StoreId.newInstance()); + assertEquals(id, StoreId.getInstance(id.getId())); + assertEquals(uuid, StoreId.getInstance(uuid).getId()); + } +} diff --git a/src/test/java/com/desafiodev/application/domains/ids/TransactionIdTest.java b/src/test/java/com/desafiodev/application/domains/ids/TransactionIdTest.java new file mode 100644 index 0000000000..992a8a1812 --- /dev/null +++ b/src/test/java/com/desafiodev/application/domains/ids/TransactionIdTest.java @@ -0,0 +1,20 @@ +package com.desafiodev.application.domains.ids; + +import static org.junit.jupiter.api.Assertions.*; + +import com.desafiodev.utils.UtilsTest; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class TransactionIdTest extends UtilsTest { + + @Test + void transactionId() { + assertClass(TransactionId.class, TransactionId.newInstance()); + var id = TransactionId.newInstance(); + var uuid = UUID.randomUUID().toString(); + assertNotEquals(id, TransactionId.newInstance()); + assertEquals(id, TransactionId.getInstance(id.getId())); + assertEquals(uuid, TransactionId.getInstance(uuid).getId()); + } +} diff --git a/src/test/java/com/desafiodev/application/services/CnabUploadServiceImplTest.java b/src/test/java/com/desafiodev/application/services/CnabUploadServiceImplTest.java new file mode 100644 index 0000000000..9d47e86bab --- /dev/null +++ b/src/test/java/com/desafiodev/application/services/CnabUploadServiceImplTest.java @@ -0,0 +1,60 @@ +package com.desafiodev.application.services; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.desafiodev.application.ports.out.StoreRepository; +import com.desafiodev.application.ports.out.TransactionRepository; +import com.desafiodev.utils.Fixture; +import java.io.File; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CnabUploadServiceImplTest { + + @Mock private TransactionRepository transactionRepository; + @Mock private StoreRepository storeRepository; + + private CnabUploadServiceImpl cnabUploadService; + + @BeforeEach + void setUp() { + cnabUploadService = new CnabUploadServiceImpl(transactionRepository, storeRepository); + } + + @Test + void accept() { + when(storeRepository.findByNameAndOwnerName(any(), any())) + .thenReturn(Optional.of(Fixture.getStore())); + doNothing().when(storeRepository).save(any()); + doNothing().when(transactionRepository).save(any(), any()); + cnabUploadService.accept(new File("src/test/resources/CNAB.txt")); + verify(storeRepository, times(21)).findByNameAndOwnerName(any(), any()); + verify(storeRepository, times(21)).save(any()); + verify(transactionRepository, times(21)).save(any(), any()); + } + + @Test + void acceptWithNoStory() { + when(storeRepository.findByNameAndOwnerName(any(), any())).thenReturn(Optional.empty()); + doNothing().when(storeRepository).save(any()); + doNothing().when(transactionRepository).save(any(), any()); + cnabUploadService.accept(new File("src/test/resources/CNAB.txt")); + verify(storeRepository, times(21)).findByNameAndOwnerName(any(), any()); + verify(storeRepository, times(21)).save(any()); + verify(transactionRepository, times(21)).save(any(), any()); + } + + @Test + void acceptWithError() { + File file = new File("not_exist"); + assertThrows(IllegalStateException.class, () -> cnabUploadService.accept(file)); + verify(transactionRepository, times(0)).save(any(), any()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/api/dtos/StoreResponseDTOTest.java b/src/test/java/com/desafiodev/infrastructure/api/dtos/StoreResponseDTOTest.java new file mode 100644 index 0000000000..db8fd00686 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/api/dtos/StoreResponseDTOTest.java @@ -0,0 +1,32 @@ +package com.desafiodev.infrastructure.api.dtos; + +import static com.desafiodev.utils.Fixture.getStore; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; + +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.jparams.verifier.tostring.ToStringVerifier; +import java.util.List; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.jupiter.api.Test; + +class StoreResponseDTOTest { + + @Test + void testClass() { + EqualsVerifier.forClass(StoreResponseDTO.class).suppress(Warning.STRICT_INHERITANCE).verify(); + ToStringVerifier.forClass(StoreResponseDTO.class).verify(); + } + + @Test + void asList() { + List list = + StoreResponseDTO.asList(singletonList(StoreEntity.from(getStore()))); + StoreResponseDTO dto = list.stream().findFirst().orElseThrow(); + assertNotNull(dto.getStoreId()); + assertEquals("NAME", dto.getStoreName()); + assertEquals("OWNER NAME", dto.getOwnerName()); + assertEquals(0.0, dto.getStoreBalance()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/api/dtos/TransactionResponseDTOTest.java b/src/test/java/com/desafiodev/infrastructure/api/dtos/TransactionResponseDTOTest.java new file mode 100644 index 0000000000..e80228b3b4 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/api/dtos/TransactionResponseDTOTest.java @@ -0,0 +1,39 @@ +package com.desafiodev.infrastructure.api.dtos; + +import static com.desafiodev.utils.Fixture.getStore; +import static com.desafiodev.utils.Fixture.getTransaction; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.desafiodev.infrastructure.repositories.entities.TransactionEntity; +import com.jparams.verifier.tostring.ToStringVerifier; +import java.util.List; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.jupiter.api.Test; + +class TransactionResponseDTOTest { + + @Test + void testClass() { + EqualsVerifier.forClass(TransactionResponseDTO.class) + .suppress(Warning.STRICT_INHERITANCE) + .verify(); + ToStringVerifier.forClass(TransactionResponseDTO.class).verify(); + } + + @Test + void asList() { + List list = + TransactionResponseDTO.asList( + singletonList(TransactionEntity.from(getTransaction(), getStore()))); + TransactionResponseDTO dto = list.stream().findFirst().orElseThrow(); + assertNotNull(dto.getTransactionId()); + assertEquals("ALUGUEL", dto.getType()); + assertNotNull(dto.getDate()); + assertEquals(10.0, dto.getTransactionValue()); + assertEquals("11111111111", dto.getCpfNumber()); + assertEquals("111111111111", dto.getCreditCardNumber()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/StoreControllerTest.java b/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/StoreControllerTest.java new file mode 100644 index 0000000000..b0ebb5be17 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/StoreControllerTest.java @@ -0,0 +1,42 @@ +package com.desafiodev.infrastructure.api.v1.controllers; + +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.times; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.infrastructure.repositories.jpas.StoreEntityJpaRepository; +import com.desafiodev.utils.Fixture; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(StoreController.class) +class StoreControllerTest { + @Autowired private MockMvc mockMvc; + @MockBean private StoreEntityJpaRepository storeEntityJpaRepository; + + @Test + void findAll() throws Exception { + when(storeEntityJpaRepository.findAll()) + .thenReturn(Collections.singletonList(StoreEntity.from(Fixture.getStore()))); + mockMvc + .perform(get("/api/v1/store").content("").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + verify(storeEntityJpaRepository, times(1)).findAll(); + } + + @Test + void findAllEmpty() throws Exception { + when(storeEntityJpaRepository.findAll()).thenReturn(Collections.emptyList()); + mockMvc + .perform(get("/api/v1/store").content("").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + verify(storeEntityJpaRepository, times(1)).findAll(); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/TransactionControllerTest.java b/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/TransactionControllerTest.java new file mode 100644 index 0000000000..b6d51e8e68 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/TransactionControllerTest.java @@ -0,0 +1,78 @@ +package com.desafiodev.infrastructure.api.v1.controllers; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.infrastructure.repositories.entities.TransactionEntity; +import com.desafiodev.infrastructure.repositories.jpas.StoreEntityJpaRepository; +import com.desafiodev.infrastructure.repositories.jpas.TransactionEntityJpaRepository; +import com.desafiodev.utils.Fixture; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(TransactionController.class) +class TransactionControllerTest { + + @Autowired private MockMvc mockMvc; + @MockBean private TransactionEntityJpaRepository transactionEntityJpaRepository; + + @MockBean private StoreEntityJpaRepository storeEntityJpaRepository; + + @Test + void findByStore() throws Exception { + StoreEntity storeEntity = StoreEntity.from(Fixture.getStore()); + when(storeEntityJpaRepository.findById(any())).thenReturn(Optional.of(storeEntity)); + when(transactionEntityJpaRepository.findByStore(storeEntity)) + .thenReturn( + Collections.singletonList( + TransactionEntity.from(Fixture.getTransaction(), Fixture.getStore()))); + mockMvc + .perform( + get("/api/v1/transaction?storeId=" + storeEntity.getId()) + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + verify(storeEntityJpaRepository, times(1)).findById(storeEntity.getId()); + verify(transactionEntityJpaRepository, times(1)).findByStore(storeEntity); + } + + @Test + void findByStoreEmpty() throws Exception { + StoreEntity storeEntity = StoreEntity.from(Fixture.getStore()); + when(storeEntityJpaRepository.findById(any())).thenReturn(Optional.of(storeEntity)); + when(transactionEntityJpaRepository.findByStore(any())).thenReturn(Collections.emptyList()); + mockMvc + .perform( + get("/api/v1/transaction?storeId=" + storeEntity.getId()) + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + verify(storeEntityJpaRepository, times(1)).findById(storeEntity.getId()); + verify(transactionEntityJpaRepository, times(1)).findByStore(storeEntity); + } + + @Test + void StoreEmpty() throws Exception { + String uuid = UUID.randomUUID().toString(); + when(storeEntityJpaRepository.findById(any())).thenReturn(Optional.empty()); + when(transactionEntityJpaRepository.findByStore(any())).thenReturn(Collections.emptyList()); + mockMvc + .perform( + get("/api/v1/transaction?storeId=" + uuid) + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + verify(storeEntityJpaRepository, times(1)).findById(uuid); + verify(transactionEntityJpaRepository, times(0)).findByStore(any()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/UploadControllerTest.java b/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/UploadControllerTest.java new file mode 100644 index 0000000000..2e8e7f10b7 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/api/v1/controllers/UploadControllerTest.java @@ -0,0 +1,40 @@ +package com.desafiodev.infrastructure.api.v1.controllers; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.desafiodev.application.ports.in.UploadService; +import com.desafiodev.infrastructure.storeges.interfaces.StorageService; +import java.io.File; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(UploadController.class) +class UploadControllerTest { + @Autowired private MockMvc mockMvc; + @MockBean private StorageService storageService; + @MockBean private UploadService uploadService; + + @BeforeEach + void setUp() { + when(storageService.save(any())).thenReturn(new File("src/test/resources/CNAB.txt")); + doNothing().when(uploadService).accept(any()); + } + + @Test + void cnab() throws Exception { + MockMultipartFile file = + new MockMultipartFile( + "file", "hello.txt", MediaType.TEXT_PLAIN_VALUE, "Hello, World!".getBytes()); + mockMvc.perform(multipart("/api/v1/upload/cnab").file(file)).andExpect(status().isAccepted()); + verify(storageService, times(1)).save(any()); + verify(uploadService, times(1)).accept(any()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/configurations/UploadConfigurationImplTest.java b/src/test/java/com/desafiodev/infrastructure/configurations/UploadConfigurationImplTest.java new file mode 100644 index 0000000000..2d3febef13 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/configurations/UploadConfigurationImplTest.java @@ -0,0 +1,23 @@ +package com.desafiodev.infrastructure.configurations; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = UploadConfigurationImpl.class) +class UploadConfigurationImplTest { + + @Autowired private UploadConfigurationImpl uploadConfigurations; + + @Test + void getPathname() { + assertEquals("upload", uploadConfigurations.getPathname()); + } + + @Test + void getFilename() { + assertEquals("targetFile.tmp", uploadConfigurations.getFilename()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/repositories/StoreRepositoryImplTest.java b/src/test/java/com/desafiodev/infrastructure/repositories/StoreRepositoryImplTest.java new file mode 100644 index 0000000000..0a92e3ea0f --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/repositories/StoreRepositoryImplTest.java @@ -0,0 +1,52 @@ +package com.desafiodev.infrastructure.repositories; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.infrastructure.repositories.jpas.StoreEntityJpaRepository; +import com.desafiodev.utils.Fixture; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class StoreRepositoryImplTest { + + @Mock private StoreEntityJpaRepository storeEntityJpaRepository; + + private StoreRepositoryImpl storeRepository; + + @BeforeEach + void setUp() { + storeRepository = new StoreRepositoryImpl(storeEntityJpaRepository); + } + + @Test + void findByNameAndOwnerName() { + when(storeEntityJpaRepository.findByNameIgnoreCaseAndOwnerNameIgnoreCase(any(), any())) + .thenReturn(Optional.of(StoreEntity.from(Fixture.getStore()))); + storeRepository.findByNameAndOwnerName("STORE NAME", "OWNER NAME"); + verify(storeEntityJpaRepository, times(1)) + .findByNameIgnoreCaseAndOwnerNameIgnoreCase(any(), any()); + } + + @Test + void findByNameAndOwnerNameEmpty() { + when(storeEntityJpaRepository.findByNameIgnoreCaseAndOwnerNameIgnoreCase(any(), any())) + .thenReturn(Optional.empty()); + storeRepository.findByNameAndOwnerName("STORE NAME", "OWNER NAME"); + verify(storeEntityJpaRepository, times(1)) + .findByNameIgnoreCaseAndOwnerNameIgnoreCase(any(), any()); + } + + @Test + void save() { + when(storeEntityJpaRepository.save(any())).thenReturn(StoreEntity.from(Fixture.getStore())); + storeRepository.save(Fixture.getStore()); + verify(storeEntityJpaRepository, times(1)).save(any()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/repositories/TransactionRepositoryImplTest.java b/src/test/java/com/desafiodev/infrastructure/repositories/TransactionRepositoryImplTest.java new file mode 100644 index 0000000000..161882efe4 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/repositories/TransactionRepositoryImplTest.java @@ -0,0 +1,32 @@ +package com.desafiodev.infrastructure.repositories; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.desafiodev.infrastructure.repositories.jpas.TransactionEntityJpaRepository; +import com.desafiodev.utils.Fixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TransactionRepositoryImplTest { + + @Mock private TransactionEntityJpaRepository transactionEntityJpaRepository; + + private TransactionRepositoryImpl transactionRepositoryImpl; + + @BeforeEach + void setUp() { + when(transactionEntityJpaRepository.save(any())).thenReturn(any()); + transactionRepositoryImpl = new TransactionRepositoryImpl(transactionEntityJpaRepository); + } + + @Test + void save() { + transactionRepositoryImpl.save(Fixture.getTransaction(), Fixture.getStore()); + verify(transactionEntityJpaRepository, times(1)).save(any()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/repositories/entities/StoreEntityTest.java b/src/test/java/com/desafiodev/infrastructure/repositories/entities/StoreEntityTest.java new file mode 100644 index 0000000000..d9af30d11e --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/repositories/entities/StoreEntityTest.java @@ -0,0 +1,22 @@ +package com.desafiodev.infrastructure.repositories.entities; + +import static org.junit.jupiter.api.Assertions.*; + +import com.desafiodev.application.domains.Store; +import com.desafiodev.utils.Fixture; +import com.desafiodev.utils.UtilsTest; +import org.junit.jupiter.api.Test; + +class StoreEntityTest extends UtilsTest { + + @Test + void from() { + Store store = Fixture.getStore(); + StoreEntity entity = StoreEntity.from(store); + assertClass(StoreEntity.class, entity); + assertEquals(store.getStoreId().getId(), entity.getId()); + assertEquals(store.getName(), entity.getName()); + assertEquals(store.getOwnerName(), entity.getOwnerName()); + assertEquals(store.getBalance(), entity.getBalance()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/repositories/entities/TransactionEntityTest.java b/src/test/java/com/desafiodev/infrastructure/repositories/entities/TransactionEntityTest.java new file mode 100644 index 0000000000..409e056e12 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/repositories/entities/TransactionEntityTest.java @@ -0,0 +1,31 @@ +package com.desafiodev.infrastructure.repositories.entities; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.desafiodev.application.domains.Store; +import com.desafiodev.application.domains.Transaction; +import com.desafiodev.utils.Fixture; +import com.desafiodev.utils.UtilsTest; +import java.time.temporal.ChronoUnit; +import org.junit.jupiter.api.Test; + +class TransactionEntityTest extends UtilsTest { + + @Test + void from() { + Transaction transaction = Fixture.getTransaction(); + Store store = Fixture.getStore(); + TransactionEntity entity = TransactionEntity.from(transaction, store); + assertClass(TransactionEntity.class, entity); + assertEquals(transaction.getCpf().getNumber(), entity.getCpf()); + assertEquals(transaction.getTransactionId().getId(), entity.getId()); + assertEquals( + transaction.getDate().truncatedTo(ChronoUnit.MILLIS).getNano(), entity.getDate().getNano()); + assertEquals(transaction.getType(), entity.getType()); + assertEquals(transaction.getCreditCard().getNumber(), entity.getCreditCard()); + assertEquals(transaction.getValue(), entity.getValue()); + assertEquals(store.getStoreId().getId(), entity.getStore().getId()); + assertEquals(store.getName(), entity.getStore().getName()); + assertEquals(store.getOwnerName(), entity.getStore().getOwnerName()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/repositories/jpas/StoreEntityJpaRepositoryTest.java b/src/test/java/com/desafiodev/infrastructure/repositories/jpas/StoreEntityJpaRepositoryTest.java new file mode 100644 index 0000000000..1aac12b542 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/repositories/jpas/StoreEntityJpaRepositoryTest.java @@ -0,0 +1,67 @@ +package com.desafiodev.infrastructure.repositories.jpas; + +import static org.junit.jupiter.api.Assertions.*; + +import com.desafiodev.application.domains.Store; +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.utils.Fixture; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) +class StoreEntityJpaRepositoryTest { + @Autowired private StoreEntityJpaRepository storeEntityJpaRepository; + + @Test + void save() { + StoreEntity storeEntity = StoreEntity.from(Fixture.getStore()); + StoreEntity result = storeEntityJpaRepository.save(storeEntity); + assertEquals(storeEntity, result); + } + + @Test + void unique() { + StoreEntity storeEntity = StoreEntity.from(Fixture.getStore()); + storeEntityJpaRepository.save(storeEntity); + StoreEntity newStoreEntity = StoreEntity.from(Fixture.getStore()); + assertThrows( + DataIntegrityViolationException.class, () -> storeEntityJpaRepository.save(newStoreEntity)); + } + + @Test + void findByNameAndOwnerName() { + StoreEntity storeEntity = StoreEntity.from(Fixture.getStore()); + StoreEntity store = storeEntityJpaRepository.save(storeEntity); + assertEquals( + store, + storeEntityJpaRepository + .findByNameIgnoreCaseAndOwnerNameIgnoreCase(store.getName(), store.getOwnerName()) + .orElseThrow()); + assertEquals( + store, + storeEntityJpaRepository + .findByNameIgnoreCaseAndOwnerNameIgnoreCase( + store.getName().toLowerCase(), store.getOwnerName().toLowerCase()) + .orElseThrow()); + assertTrue( + storeEntityJpaRepository + .findByNameIgnoreCaseAndOwnerNameIgnoreCase("Not exist", "Not exist") + .isEmpty()); + } + + @Test + void findAll() { + Store store = Fixture.getStore(); + StoreEntity storeEntity = storeEntityJpaRepository.save(StoreEntity.from(store)); + List list = storeEntityJpaRepository.findAll(); + assertEquals(1, list.size()); + assertEquals(storeEntity, list.stream().findFirst().orElseThrow()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/repositories/jpas/TransactionEntityJpaRepositoryTest.java b/src/test/java/com/desafiodev/infrastructure/repositories/jpas/TransactionEntityJpaRepositoryTest.java new file mode 100644 index 0000000000..1bdb123f69 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/repositories/jpas/TransactionEntityJpaRepositoryTest.java @@ -0,0 +1,47 @@ +package com.desafiodev.infrastructure.repositories.jpas; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.desafiodev.application.domains.Store; +import com.desafiodev.infrastructure.repositories.entities.StoreEntity; +import com.desafiodev.infrastructure.repositories.entities.TransactionEntity; +import com.desafiodev.utils.Fixture; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) +class TransactionEntityJpaRepositoryTest { + @Autowired private TransactionEntityJpaRepository transactionEntityJpaRepository; + @Autowired private StoreEntityJpaRepository storeEntityJpaRepository; + + @Test + void save() { + Store store = Fixture.getStore(); + TransactionEntity transactionEntity = TransactionEntity.from(Fixture.getTransaction(), store); + storeEntityJpaRepository.save(StoreEntity.from(store)); + TransactionEntity result = transactionEntityJpaRepository.save(transactionEntity); + assertEquals(transactionEntity, result); + } + + @Test + void findByStore() { + Store store = Fixture.getStore(); + TransactionEntity transactionEntity = TransactionEntity.from(Fixture.getTransaction(), store); + StoreEntity storeEntity = StoreEntity.from(store); + assertTrue(transactionEntityJpaRepository.findByStore(storeEntity).isEmpty()); + storeEntityJpaRepository.save(storeEntity); + assertTrue(transactionEntityJpaRepository.findByStore(storeEntity).isEmpty()); + TransactionEntity result = transactionEntityJpaRepository.save(transactionEntity); + List list = transactionEntityJpaRepository.findByStore(storeEntity); + assertEquals(1, list.size()); + assertEquals(result, list.stream().findFirst().orElseThrow()); + assertEquals(storeEntity, list.stream().findFirst().orElseThrow().getStore()); + } +} diff --git a/src/test/java/com/desafiodev/infrastructure/storeges/LocalStorageServiceImplTest.java b/src/test/java/com/desafiodev/infrastructure/storeges/LocalStorageServiceImplTest.java new file mode 100644 index 0000000000..9002f302d6 --- /dev/null +++ b/src/test/java/com/desafiodev/infrastructure/storeges/LocalStorageServiceImplTest.java @@ -0,0 +1,49 @@ +package com.desafiodev.infrastructure.storeges; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.desafiodev.infrastructure.configurations.interfaces.UploadConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +@ExtendWith(MockitoExtension.class) +class LocalStorageServiceImplTest { + + @Mock private UploadConfiguration uploadConfiguration; + + private LocalStorageServiceImpl localStorageService; + + @BeforeEach + void setUp() { + when(uploadConfiguration.getPathname()).thenReturn("upload"); + when(uploadConfiguration.getFilename()).thenReturn("targetFile.tmp"); + localStorageService = new LocalStorageServiceImpl(uploadConfiguration); + } + + @Test + void save() { + MockMultipartFile file = + new MockMultipartFile( + "file", "hello.txt", MediaType.TEXT_PLAIN_VALUE, "Hello, World!".getBytes()); + localStorageService.save(file); + verify(uploadConfiguration, times(1)).getPathname(); + verify(uploadConfiguration, times(1)).getFilename(); + } + + @Test + void saveTwice() { + MockMultipartFile file = + new MockMultipartFile( + "file", "hello.txt", MediaType.TEXT_PLAIN_VALUE, "Hello, World!".getBytes()); + localStorageService.save(file); + localStorageService.save(file); + verify(uploadConfiguration, times(2)).getPathname(); + verify(uploadConfiguration, times(2)).getFilename(); + } +} diff --git a/src/test/java/com/desafiodev/utils/Fixture.java b/src/test/java/com/desafiodev/utils/Fixture.java new file mode 100644 index 0000000000..9c00e68caa --- /dev/null +++ b/src/test/java/com/desafiodev/utils/Fixture.java @@ -0,0 +1,47 @@ +package com.desafiodev.utils; + +import com.desafiodev.application.domains.*; +import com.desafiodev.application.domains.ids.StoreId; +import com.desafiodev.application.domains.ids.TransactionId; +import java.time.Instant; + +public class Fixture { + public static Cpf getCpf() { + return Cpf.newInstance("11111111111"); + } + + public static CreditCard getCreditCard() { + return CreditCard.newInstance("111111111111"); + } + + public static TransactionType getTransactionType() { + return TransactionType.ALUGUEL; + } + + public static Transaction getTransaction() { + return Transaction.newInstance( + getTransactionType(), Instant.now(), 10, getCpf(), getCreditCard(), StoreId.newInstance()); + } + + public static Cnab getCnab() { + return Cnab.newInstance( + "3201903010000014200096206760174753****3153153453JOÃO MACEDO BAR DO JOÃO "); + } + + public static StoreId getStoreId() { + return StoreId.newInstance(); + } + + public static TransactionId getTransactionId() { + return TransactionId.newInstance(); + } + + public static Store getStore() { + return Store.newInstance("NAME", "OWNER NAME"); + } + + public static Transaction getTransaction(TransactionType type, double transactionValue) { + return Transaction.newInstance( + type, Instant.now(), transactionValue, getCpf(), getCreditCard(), StoreId.newInstance()); + } +} diff --git a/src/test/java/com/desafiodev/utils/UtilsTest.java b/src/test/java/com/desafiodev/utils/UtilsTest.java new file mode 100644 index 0000000000..e4e98b11fa --- /dev/null +++ b/src/test/java/com/desafiodev/utils/UtilsTest.java @@ -0,0 +1,29 @@ +package com.desafiodev.utils; + +import com.desafiodev.application.domains.*; +import com.desafiodev.application.domains.ids.StoreId; +import com.desafiodev.application.domains.ids.TransactionId; +import com.google.common.testing.NullPointerTester; +import com.jparams.verifier.tostring.ToStringVerifier; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; + +public abstract class UtilsTest { + protected void assertClass(Class tClass, T instance) { + NullPointerTester test = + new NullPointerTester() + .setDefault(Cpf.class, Fixture.getCpf()) + .setDefault(CreditCard.class, Fixture.getCreditCard()) + .setDefault(TransactionType.class, Fixture.getTransactionType()) + .setDefault(Transaction.class, Fixture.getTransaction()) + .setDefault(Cnab.class, Fixture.getCnab()) + .setDefault(StoreId.class, Fixture.getStoreId()) + .setDefault(TransactionId.class, Fixture.getTransactionId()) + .setDefault(Store.class, Fixture.getStore()); + test.testAllPublicStaticMethods(tClass); + test.testAllPublicInstanceMethods(instance); + test.testAllPublicConstructors(tClass); + EqualsVerifier.forClass(tClass).suppress(Warning.STRICT_INHERITANCE).verify(); + ToStringVerifier.forClass(tClass).verify(); + } +} diff --git a/CNAB.txt b/src/test/resources/CNAB.txt old mode 100755 new mode 100644 similarity index 100% rename from CNAB.txt rename to src/test/resources/CNAB.txt diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000000..0305a172ec --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,10 @@ +spring.jpa.database=POSTGRESQL +spring.datasource.platform=postgres +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=create +spring.database.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL;INIT=CREATE SCHEMA IF NOT EXISTS test\\;SET SCHEMA test +spring.datasource.username=postgres +spring.datasource.password=password +upload.path=upload +upload.file=targetFile.tmp \ No newline at end of file diff --git a/variable.tf b/variable.tf new file mode 100644 index 0000000000..321ecc6e85 --- /dev/null +++ b/variable.tf @@ -0,0 +1,23 @@ +variable "project_id" { + type = string +} + +variable "region" { + type = string +} + +variable "zone" { + type = string +} + +variable "cloud_credential" { + type = string +} + +variable "cluster_name" { + type = string +} + +variable "node_name" { + type = string +} \ No newline at end of file