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
+
+[](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