├── dispatch.yaml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── resources │ │ ├── db │ │ │ └── changelog │ │ │ │ ├── db.changelog-master.yaml │ │ │ │ └── changesets │ │ │ │ ├── v001_initial.sql │ │ │ │ ├── v003_indexes.sql │ │ │ │ └── v002_models.sql │ │ └── application.properties │ └── kotlin │ │ └── com │ │ └── quorum │ │ └── api │ │ ├── annotations │ │ └── NoArgConstructor.kt │ │ ├── connectivity │ │ ├── Endpoints.kt │ │ └── Requests.kt │ │ ├── utils │ │ ├── Datas.kt │ │ ├── PageUtils.kt │ │ ├── XmlParser.kt │ │ └── ExceptionHandlers.kt │ │ ├── ApiApplication.kt │ │ ├── authentication │ │ ├── repositories │ │ │ └── RepositorioAutenticacao.kt │ │ ├── modelos │ │ │ └── Autenticacao.kt │ │ ├── controllers │ │ │ ├── AuthenticationController.kt │ │ │ └── AdminAuthController.kt │ │ └── servicos │ │ │ └── ServicoAutenticacao.kt │ │ ├── despesas │ │ ├── repositories │ │ │ └── RepositorioDespesa.kt │ │ ├── modelos │ │ │ └── Despesa.kt │ │ ├── servicos │ │ │ └── DespesaService.kt │ │ └── controllers │ │ │ ├── DespesaControllerAdmin.kt │ │ │ └── DespesaController.kt │ │ ├── vereadores │ │ ├── repositorios │ │ │ └── RepositorioVereador.kt │ │ ├── modelos │ │ │ └── Vereador.kt │ │ ├── servicos │ │ │ └── ServicoVereador.kt │ │ └── controllers │ │ │ ├── VereadoresControllerAdmin.kt │ │ │ └── VereadorController.kt │ │ ├── fornecedores │ │ ├── repositories │ │ │ └── RepositorioFornecedor.kt │ │ ├── modelos │ │ │ └── Fornecedor.kt │ │ ├── servicos │ │ │ └── ServicoFornecedor.kt │ │ └── controllers │ │ │ ├── FornecedorControllerAdmin.kt │ │ │ └── FornecedorController.kt │ │ ├── reembolsos │ │ ├── repositories │ │ │ ├── RepositorioReembolso.kt │ │ │ └── ReembolsoSpecification.kt │ │ ├── modelos │ │ │ └── ItemsReembolso.kt │ │ ├── controllers │ │ │ ├── ReembolsoController.kt │ │ │ └── ReembolsoControllerAdmin.kt │ │ └── servicos │ │ │ └── ServicoReembolso.kt │ │ ├── config │ │ ├── CacheConfig.kt │ │ ├── SecretManagerAccessor.kt │ │ ├── DataSourceConfig.kt │ │ ├── LiquibaseConfig.kt │ │ ├── OpenApiConfiguration.kt │ │ ├── SecurityConfig.kt │ │ └── TokenFilter.kt │ │ └── messaging │ │ └── ServicoEmail.kt └── test │ └── kotlin │ └── com │ └── quorum │ └── api │ └── ApiApplicationTests.kt ├── app.yaml ├── .gitignore ├── settings.gradle ├── .gcloudignore ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /dispatch.yaml: -------------------------------------------------------------------------------- 1 | dispatch: 2 | - url: "api.quorum-tech.io/*" 3 | service: default -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagobarbosa/quorum-api/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/db/changelog/db.changelog-master.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - includeAll: 3 | path: db/changelog/changesets/ 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/annotations/NoArgConstructor.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.annotations 2 | 3 | annotation class NoArgConstructor 4 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: java17 2 | instance_class: F2 3 | vpc_access_connector: 4 | name: projects/quorum-api/locations/southamerica-east1/connectors/vpc-connector -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/connectivity/Endpoints.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.connectivity 2 | 3 | val obterDebitoVereador = "https://sisgvconsulta.saopaulo.sp.leg.br/ws/Servicos.asmx/ObterDebitoVereador" 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/utils/Datas.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.utils 2 | 3 | import java.time.LocalDate 4 | 5 | val ANO_INICIO = 2015 6 | val ANO_ATUAL = LocalDate.now().year 7 | val MES_ATUAL = LocalDate.now().monthValue 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/test/kotlin/com/quorum/api/ApiApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | 6 | @SpringBootTest 7 | class ApiApplicationTests { 8 | 9 | @Test 10 | fun contextLoads() { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/utils/PageUtils.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.utils 2 | 3 | import org.springframework.data.domain.PageRequest 4 | import kotlin.math.min 5 | const val MAX_PAGE_SIZE = 100 6 | 7 | fun defaultPageable(page: Int? = 0, pageSize: Int? = MAX_PAGE_SIZE) = PageRequest.of(page ?: 0, min(pageSize ?: MAX_PAGE_SIZE, MAX_PAGE_SIZE)) 8 | -------------------------------------------------------------------------------- /src/main/resources/db/changelog/changesets/v001_initial.sql: -------------------------------------------------------------------------------- 1 | --liquibase formatted sql 2 | 3 | --changeset thiago:initial-1 dbms:mysql 4 | create table if not exists vereador 5 | ( 6 | id varchar(255) not null primary key, 7 | nome varchar(255) null, 8 | created_date timestamp null, 9 | modified_time timestamp null 10 | ); -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/utils/XmlParser.kt: -------------------------------------------------------------------------------- 1 | import com.fasterxml.jackson.dataformat.xml.XmlMapper 2 | import com.fasterxml.jackson.module.kotlin.registerKotlinModule 3 | import com.quorum.api.reembolsos.modelos.ItemsReembolso 4 | 5 | fun parseXmlResponse(xmlResponse: String): ItemsReembolso { 6 | val xmlMapper = XmlMapper().apply { 7 | registerKotlinModule() 8 | } 9 | return xmlMapper.readValue(xmlResponse, ItemsReembolso::class.java) 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | /build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | dump.rdb 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea/* 18 | !.idea/codeStyles/ 19 | *.iws 20 | *.iml 21 | *.ipr 22 | /out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/ApiApplication.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity 6 | 7 | @SpringBootApplication 8 | @EnableGlobalMethodSecurity(jsr250Enabled = true) 9 | class ApiApplication 10 | 11 | fun main(args: Array) { 12 | runApplication(*args) 13 | } 14 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | 2 | pluginManagement { 3 | repositories { 4 | gradlePluginPortal() 5 | mavenCentral() 6 | // add mavenLocal() if you are using a locally built version of the plugin 7 | } 8 | resolutionStrategy { 9 | eachPlugin { 10 | if (requested.id.id.startsWith('com.google.cloud.tools.appengine')) { 11 | useModule("com.google.cloud.tools:appengine-gradle-plugin:${requested.version}") 12 | } 13 | } 14 | } 15 | } 16 | 17 | rootProject.name = 'api' -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/authentication/repositories/RepositorioAutenticacao.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.authentication.repositories 2 | 3 | import com.quorum.api.authentication.modelos.Autenticacao 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | interface RepositorioAutenticacao : CrudRepository { 9 | fun findAllByRole(role: String): List 10 | 11 | fun findByEmail(email: String): Autenticacao? 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/despesas/repositories/RepositorioDespesa.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.despesas.repositories 2 | 3 | import com.quorum.api.despesas.modelos.Despesa 4 | import org.springframework.data.domain.Page 5 | import org.springframework.data.domain.Pageable 6 | import org.springframework.data.repository.CrudRepository 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | interface RepositorioDespesa : CrudRepository { 11 | fun findAll(page: Pageable): Page 12 | fun findByNomeCategoria(nome: String): Despesa? 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/vereadores/repositorios/RepositorioVereador.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.vereadores.repositorios 2 | 3 | import com.quorum.api.vereadores.modelos.Vereador 4 | import org.springframework.data.domain.Page 5 | import org.springframework.data.domain.Pageable 6 | import org.springframework.data.repository.CrudRepository 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | interface RepositorioVereador : CrudRepository { 11 | fun findAll(page: Pageable): Page 12 | fun findByNome(nome: String): Vereador? 13 | } 14 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Target directory for maven builds 17 | target/ -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/fornecedores/repositories/RepositorioFornecedor.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.fornecedores.repositories 2 | 3 | import com.quorum.api.fornecedores.modelos.Fornecedor 4 | import org.springframework.data.domain.Page 5 | import org.springframework.data.domain.Pageable 6 | import org.springframework.data.repository.CrudRepository 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | interface RepositorioFornecedor : CrudRepository { 11 | fun findAll(page: Pageable): Page 12 | fun findByCnpj(cnpj: String): Fornecedor? 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/reembolsos/repositories/RepositorioReembolso.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.reembolsos.repositories 2 | 3 | import com.quorum.api.reembolsos.modelos.Reembolso 4 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor 5 | import org.springframework.data.repository.CrudRepository 6 | import org.springframework.stereotype.Repository 7 | 8 | @Repository 9 | interface RepositorioReembolso : CrudRepository, JpaSpecificationExecutor { 10 | fun findAllByAno(ano: Int): List 11 | 12 | fun findAllByAnoEqualsAndMesEquals(ano: Int, mes: Int): List 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/db/changelog/changesets/v003_indexes.sql: -------------------------------------------------------------------------------- 1 | --liquibase formatted sql 2 | 3 | --changeset thiago:indexes-1 dbms:mysql 4 | create index idx_despesa_nome_categoria on despesa (nome_categoria); 5 | 6 | --changeset thiago:indexes-2 dbms:mysql 7 | create index idx_fornecedor_nome on fornecedor (nome); 8 | 9 | --changeset thiago:indexes-3 dbms:mysql 10 | create index idx_item_reembolso_id_vereador on item_reembolso (id_vereador); 11 | create index idx_item_reembolso_cnpj on item_reembolso (cnpj); 12 | create index idx_item_reembolso_ano_mes on item_reembolso (ano, mes); 13 | create index idx_item_reembolso_id_despesa on item_reembolso (id_despesa); 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/config/CacheConfig.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.config 2 | 3 | import com.github.benmanes.caffeine.cache.Caffeine 4 | import com.github.benmanes.caffeine.cache.LoadingCache 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import java.util.concurrent.TimeUnit 8 | 9 | @Configuration 10 | class CacheConfig { 11 | 12 | @Bean 13 | fun requestLimitCache(): LoadingCache { 14 | return Caffeine.newBuilder() 15 | .expireAfterWrite(1, TimeUnit.MINUTES) 16 | .maximumSize(100) 17 | .build { _ -> 0 } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/fornecedores/modelos/Fornecedor.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.fornecedores.modelos 2 | 3 | import com.quorum.api.annotations.NoArgConstructor 4 | import org.springframework.data.jpa.domain.support.AuditingEntityListener 5 | import javax.persistence.Column 6 | import javax.persistence.Entity 7 | import javax.persistence.EntityListeners 8 | import javax.persistence.Id 9 | import javax.persistence.Table 10 | 11 | @Entity 12 | @Table(name = "fornecedor") 13 | @EntityListeners(AuditingEntityListener::class) 14 | @NoArgConstructor 15 | data class Fornecedor( 16 | @Id 17 | @Column(name = "cnpj") 18 | val cnpj: String, 19 | 20 | @Column(name = "nome") 21 | val nome: String 22 | ) 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/config/SecretManagerAccessor.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.config 2 | 3 | import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse 4 | import com.google.cloud.secretmanager.v1.SecretManagerServiceClient 5 | import com.google.cloud.secretmanager.v1.SecretVersionName 6 | 7 | object SecretManagerAccessor { 8 | 9 | fun getSecret(secretId: String, versionId: String = "latest"): String { 10 | val projectId = "quorum-api" 11 | SecretManagerServiceClient.create().use { client -> 12 | val secretVersionName = SecretVersionName.of(projectId, secretId, versionId) 13 | val response: AccessSecretVersionResponse = client.accessSecretVersion(secretVersionName) 14 | return response.payload.data.toStringUtf8() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/connectivity/Requests.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.connectivity 2 | 3 | import okhttp3.FormBody 4 | import okhttp3.OkHttpClient 5 | import okhttp3.Request 6 | import okio.IOException 7 | 8 | fun makePostRequest(url: String, ano: Int, mes: Int): String { 9 | val client = OkHttpClient() 10 | 11 | val formBody = FormBody.Builder() 12 | .add("ano", ano.toString()) 13 | .add("mes", mes.toString()) 14 | .build() 15 | 16 | val request = Request.Builder() 17 | .url(url) 18 | .post(formBody) 19 | .addHeader("Content-Type", "application/x-www-form-urlencoded") 20 | .build() 21 | 22 | client.newCall(request).execute().use { response -> 23 | if (!response.isSuccessful) throw IOException("Unexpected code $response") 24 | 25 | return response.body!!.string() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #Spring Doc 2 | springdoc.swagger-ui.operationsSorter=alpha 3 | springdoc.swagger-ui.tagsSorter=alpha 4 | springdoc.swagger-ui.filter=true 5 | springdoc.writer-with-order-by-keys=true 6 | springdoc.api-docs.groups.enabled=true 7 | springdoc.paths-to-exclude= /v1/admin/** 8 | 9 | # JPA / Hibernate 10 | spring.jpa.show-sql=false 11 | spring.jpa.hibernate.ddl-auto=validate 12 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect 13 | spring.jpa.properties.hibernate.show_sql=false 14 | spring.jpa.properties.hibernate.use_sql_comments=true 15 | spring.jpa.properties.hibernate.format_sql=true 16 | spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true 17 | hibernate.dialect=org.hibernate.dialect.MySQL57Dialect 18 | hibernate.search.default.directory_provider=filesystem 19 | hibernate.search.default.indexBase=/tmp/lucene/indexes 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/messaging/ServicoEmail.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.messaging 2 | 3 | import com.quorum.api.config.SecretManagerAccessor 4 | import com.resend.Resend 5 | import com.resend.core.exception.ResendException 6 | import com.resend.services.emails.model.SendEmailRequest 7 | import org.springframework.stereotype.Service 8 | 9 | @Service 10 | class ServicoEmail { 11 | 12 | fun sendEmail(to: String, subject: String, text: String) { 13 | val resend = Resend(SecretManagerAccessor.getSecret("mailPassword")) 14 | 15 | val sendEmailRequest = SendEmailRequest.builder() 16 | .from("Quorum API ") 17 | .to(to) 18 | .subject(subject) 19 | .html(text) 20 | .build() 21 | 22 | try { 23 | resend.emails().send(sendEmailRequest) 24 | } catch (e: ResendException) { 25 | e.printStackTrace() 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/utils/ExceptionHandlers.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.utils 2 | 3 | import org.slf4j.LoggerFactory 4 | import org.springframework.http.HttpStatus 5 | import org.springframework.web.bind.annotation.ExceptionHandler 6 | import org.springframework.web.bind.annotation.ResponseStatus 7 | import org.springframework.web.bind.annotation.RestControllerAdvice 8 | 9 | @RestControllerAdvice 10 | class ExceptionHandlers { 11 | 12 | @ExceptionHandler(Exception::class) 13 | @ResponseStatus(HttpStatus.BAD_REQUEST) 14 | fun handleException(e: Exception): ErrorResponse { 15 | val status = HttpStatus.BAD_REQUEST.value().toString() 16 | val message = e.message ?: "Erro desconhecido" 17 | return ErrorResponse(status, message) 18 | } 19 | } 20 | 21 | data class ErrorResponse(val status: String, val message: String) { 22 | 23 | companion object { 24 | private val logger = LoggerFactory.getLogger(this::class.java) 25 | } 26 | 27 | init { 28 | logger.info("Server error: $message") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/vereadores/modelos/Vereador.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.vereadores.modelos 2 | 3 | import com.quorum.api.annotations.NoArgConstructor 4 | import org.springframework.data.annotation.CreatedDate 5 | import org.springframework.data.annotation.LastModifiedDate 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener 7 | import java.time.ZonedDateTime 8 | import javax.persistence.Column 9 | import javax.persistence.Entity 10 | import javax.persistence.EntityListeners 11 | import javax.persistence.Id 12 | import javax.persistence.Table 13 | 14 | @Entity 15 | @Table(name = "vereador") 16 | @EntityListeners(AuditingEntityListener::class) 17 | @NoArgConstructor 18 | data class Vereador( 19 | @Id 20 | @Column(name = "id") 21 | val id: String, 22 | 23 | @Column(name = "nome") 24 | val nome: String, 25 | 26 | @CreatedDate 27 | @Column(name = "created_date", updatable = false) 28 | var createdDate: ZonedDateTime = ZonedDateTime.now(), 29 | 30 | @LastModifiedDate 31 | @Column(name = "modified_time") 32 | var modifiedDate: ZonedDateTime = ZonedDateTime.now() 33 | ) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Thiago Barbosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/despesas/modelos/Despesa.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.despesas.modelos 2 | 3 | import com.quorum.api.annotations.NoArgConstructor 4 | import org.springframework.data.annotation.CreatedDate 5 | import org.springframework.data.annotation.LastModifiedDate 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener 7 | import java.time.ZonedDateTime 8 | import java.util.UUID 9 | import javax.persistence.Column 10 | import javax.persistence.Entity 11 | import javax.persistence.EntityListeners 12 | import javax.persistence.Id 13 | import javax.persistence.Table 14 | 15 | @Entity 16 | @Table(name = "despesa") 17 | @EntityListeners(AuditingEntityListener::class) 18 | @NoArgConstructor 19 | data class Despesa( 20 | @Id 21 | @Column(name = "id") 22 | val id: String = UUID.randomUUID().toString(), 23 | 24 | @Column(name = "nome_categoria") 25 | val nomeCategoria: String, 26 | 27 | @CreatedDate 28 | @Column(name = "created_date", updatable = false) 29 | var createdDate: ZonedDateTime = ZonedDateTime.now(), 30 | 31 | @LastModifiedDate 32 | @Column(name = "modified_time") 33 | var modifiedDate: ZonedDateTime = ZonedDateTime.now() 34 | ) 35 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/config/DataSourceConfig.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.config 2 | 3 | import org.springframework.boot.jdbc.DataSourceBuilder 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import javax.sql.DataSource 7 | 8 | @Configuration 9 | class DataSourceConfig { 10 | 11 | @Bean 12 | fun getDataSource(): DataSource { 13 | val dbUrl = try { 14 | SecretManagerAccessor.getSecret("dbUrl") 15 | } catch (e: Exception) { 16 | "jdbc:mysql://localhost:33061/quorum?useSSL=false&characterEncoding=utf8&character_set_server=utf8mb4" 17 | } 18 | 19 | val dbUsername = try { 20 | SecretManagerAccessor.getSecret("dbUsername") 21 | } catch (e: Exception) { 22 | "test" 23 | } 24 | 25 | val dbPassword = try { 26 | SecretManagerAccessor.getSecret("dbPassword") 27 | } catch (e: Exception) { 28 | "test" 29 | } 30 | 31 | return DataSourceBuilder.create() 32 | .driverClassName("com.mysql.cj.jdbc.Driver") 33 | .url(dbUrl) 34 | .username(dbUsername) 35 | .password(dbPassword) 36 | .build() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/config/LiquibaseConfig.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.config 2 | 3 | import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.context.annotation.Primary 7 | 8 | @Configuration 9 | class LiquibaseConfig { 10 | 11 | @Primary 12 | @Bean 13 | fun liquibaseProperties(): LiquibaseProperties { 14 | val dbUrl = try { 15 | SecretManagerAccessor.getSecret("dbUrl") 16 | } catch (e: Exception) { 17 | "jdbc:mysql://localhost:33061/quorum?useSSL=false&characterEncoding=utf8&character_set_server=utf8mb4" 18 | } 19 | 20 | val dbUsername = try { 21 | SecretManagerAccessor.getSecret("dbUsername") 22 | } catch (e: Exception) { 23 | "test" 24 | } 25 | 26 | val dbPassword = try { 27 | SecretManagerAccessor.getSecret("dbPassword") 28 | } catch (e: Exception) { 29 | "test" 30 | } 31 | 32 | return LiquibaseProperties().apply { 33 | this.changeLog = "classpath:/db/changelog/db.changelog-master.yaml" 34 | this.url = dbUrl 35 | this.user = dbUsername 36 | this.password = dbPassword 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/reembolsos/repositories/ReembolsoSpecification.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.reembolsos.repositories 2 | 3 | import com.quorum.api.reembolsos.modelos.Reembolso 4 | import org.springframework.data.jpa.domain.Specification 5 | import javax.persistence.criteria.CriteriaBuilder 6 | import javax.persistence.criteria.CriteriaQuery 7 | import javax.persistence.criteria.Predicate 8 | import javax.persistence.criteria.Root 9 | 10 | class ReembolsoSpecification( 11 | private val idVereador: String?, 12 | private val idDespesa: String?, 13 | private val cnpj: String?, 14 | private val ano: Int?, 15 | private val mes: Int? 16 | ) : Specification { 17 | 18 | override fun toPredicate(root: Root, query: CriteriaQuery<*>, cb: CriteriaBuilder): Predicate? { 19 | val predicates = mutableListOf() 20 | 21 | idVereador?.let { 22 | predicates.add(cb.equal(root.get("idVereador"), it)) 23 | } 24 | 25 | idDespesa?.let { 26 | predicates.add(cb.equal(root.get("idDespesa"), it)) 27 | } 28 | 29 | cnpj?.let { 30 | predicates.add(cb.equal(root.get("cnpj"), it)) 31 | } 32 | 33 | ano?.let { 34 | predicates.add(cb.equal(root.get("ano"), it)) 35 | } 36 | 37 | mes?.let { 38 | predicates.add(cb.equal(root.get("mes"), it)) 39 | } 40 | 41 | return cb.and(*predicates.toTypedArray()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/authentication/modelos/Autenticacao.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.authentication.modelos 2 | 3 | import com.quorum.api.annotations.NoArgConstructor 4 | import org.springframework.data.annotation.CreatedDate 5 | import org.springframework.data.annotation.LastModifiedDate 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener 7 | import java.time.ZonedDateTime 8 | import javax.persistence.Column 9 | import javax.persistence.Entity 10 | import javax.persistence.EntityListeners 11 | import javax.persistence.Id 12 | import javax.persistence.Table 13 | 14 | @Entity 15 | @Table(name = "autenticacao") 16 | @EntityListeners(AuditingEntityListener::class) 17 | @NoArgConstructor 18 | class Autenticacao( 19 | @Id 20 | @Column(name = "token") 21 | val token: String, 22 | 23 | @Column(name = "data_expiracao") 24 | val dataExpiracao: ZonedDateTime, 25 | 26 | @Column(name = "role") 27 | val role: String, 28 | 29 | @Column(name = "email") 30 | val email: String? = null, 31 | 32 | @CreatedDate 33 | @Column(name = "created_date", updatable = false) 34 | var createdDate: ZonedDateTime = ZonedDateTime.now(), 35 | 36 | @LastModifiedDate 37 | @Column(name = "modified_time") 38 | var modifiedDate: ZonedDateTime = ZonedDateTime.now() 39 | ) 40 | 41 | fun Autenticacao.obterLimiteRequisicao(): Int { 42 | return when (role) { 43 | "PUBLIC" -> 10 44 | "USER" -> 100 45 | "ADMIN" -> 1000 46 | else -> 0 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/config/OpenApiConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.config 2 | 3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition 4 | import io.swagger.v3.oas.annotations.info.Info 5 | import io.swagger.v3.oas.annotations.servers.Server 6 | import io.swagger.v3.oas.models.Paths 7 | import org.springdoc.core.GroupedOpenApi 8 | import org.springdoc.core.customizers.OpenApiCustomiser 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.Configuration 11 | 12 | @Configuration 13 | @OpenAPIDefinition( 14 | info = Info(title = "Quorum API", version = "v1", description = "API de acesso aos dados de gastos da Câmara de Vereadores de São Paulo"), 15 | servers = [Server(url = "https://api.quorum-tech.io")] 16 | ) 17 | class OpenApiConfiguration { 18 | @Bean 19 | fun publicApi(): GroupedOpenApi { 20 | return GroupedOpenApi.builder() 21 | .group("User API") 22 | .pathsToMatch("/v1/**") 23 | .pathsToExclude("/v1/admin", "/v1/admin/**") 24 | .addOpenApiCustomiser(sortPathsCustomizer()) 25 | .build() 26 | } 27 | 28 | fun sortPathsCustomizer(): OpenApiCustomiser { 29 | return OpenApiCustomiser { openApi -> 30 | val sortedPaths = Paths() 31 | openApi.paths.orEmpty().toSortedMap().forEach { (key, value) -> 32 | sortedPaths.addPathItem(key, value) 33 | } 34 | openApi.paths = sortedPaths 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/config/SecurityConfig.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.config 2 | 3 | import org.springframework.boot.web.servlet.FilterRegistrationBean 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 8 | import org.springframework.security.config.http.SessionCreationPolicy 9 | import org.springframework.security.web.DefaultSecurityFilterChain 10 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter 11 | 12 | @Configuration 13 | @EnableWebSecurity 14 | class SecurityConfig( 15 | private val tokenAuthenticationFilter: TokenFilter 16 | ) { 17 | @Bean 18 | fun filterChain(http: HttpSecurity): DefaultSecurityFilterChain? { 19 | http.csrf().disable() 20 | .addFilterBefore(tokenAuthenticationFilter, BasicAuthenticationFilter::class.java) 21 | .authorizeRequests() 22 | .antMatchers("/v1/auth/**").permitAll() 23 | http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER) 24 | return http.build() 25 | } 26 | 27 | @Bean 28 | fun tokenFilterRegistrationBean(): FilterRegistrationBean { 29 | val registrationBean = FilterRegistrationBean() 30 | registrationBean.filter = tokenAuthenticationFilter 31 | registrationBean.addUrlPatterns("/v1/**") 32 | return registrationBean 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/db/changelog/changesets/v002_models.sql: -------------------------------------------------------------------------------- 1 | --liquibase formatted sql 2 | 3 | --changeset thiago:models-1 dbms:mysql 4 | create table if not exists despesa 5 | ( 6 | id varchar(255) not null primary key, 7 | nome_categoria varchar(129) not null, 8 | created_date timestamp null, 9 | modified_time timestamp null 10 | ); 11 | 12 | --changeset thiago:models-2 dbms:mysql 13 | create table if not exists fornecedor 14 | ( 15 | cnpj varchar(255) not null primary key, 16 | nome varchar(129) not null, 17 | created_date timestamp null, 18 | modified_time timestamp null 19 | ); 20 | 21 | --changeset thiago:models-3 dbms:mysql 22 | create table if not exists item_reembolso 23 | ( 24 | id varchar(255) not null primary key, 25 | id_vereador varchar(255) not null, 26 | nome_vereador varchar(255) not null, 27 | id_centro_custo double not null, 28 | departamento varchar(255) not null, 29 | tipo_departamento int not null, 30 | ano int not null, 31 | mes int not null, 32 | nome_despesa varchar(128) not null, 33 | id_despesa varchar(255) not null, 34 | cnpj varchar(30) not null, 35 | fornecedor varchar(255) not null, 36 | valor double not null, 37 | created_date timestamp null, 38 | modified_time timestamp null, 39 | constraint FK_ItemReembolso_Vereador foreign key (id_vereador) references vereador (id), 40 | constraint FK_ItemReembolso_Despesa foreign key (id_despesa) references despesa (id), 41 | constraint FK_ItemReembolso_Fornecedor foreign key (cnpj) references fornecedor (cnpj) 42 | ); 43 | 44 | --changeset thiago:models-4 dbms:mysql 45 | create table if not exists autenticacao 46 | ( 47 | token varchar(255) not null primary key, 48 | data_expiracao timestamp not null, 49 | role varchar(64) not null, 50 | email varchar(255) null, 51 | created_date timestamp null, 52 | modified_time timestamp null 53 | ); 54 | 55 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/despesas/servicos/DespesaService.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.despesas.servicos 2 | 3 | import com.quorum.api.connectivity.makePostRequest 4 | import com.quorum.api.connectivity.obterDebitoVereador 5 | import com.quorum.api.despesas.modelos.Despesa 6 | import com.quorum.api.despesas.repositories.RepositorioDespesa 7 | import com.quorum.api.utils.ANO_ATUAL 8 | import com.quorum.api.utils.ANO_INICIO 9 | import com.quorum.api.utils.MES_ATUAL 10 | import com.quorum.api.utils.defaultPageable 11 | import org.springframework.stereotype.Service 12 | import org.springframework.transaction.annotation.Transactional 13 | import parseXmlResponse 14 | 15 | @Service 16 | class DespesaService( 17 | private val repositorioDespesa: RepositorioDespesa 18 | ) { 19 | 20 | @Transactional 21 | fun apagarTodasDespesas(): List { 22 | val despesas = repositorioDespesa.findAll().toList() 23 | repositorioDespesa.deleteAll(despesas) 24 | return despesas 25 | } 26 | 27 | fun obterTodasDespesas(page: Int? = 0, pageSize: Int? = 100): List { 28 | return repositorioDespesa.findAll(defaultPageable(page, pageSize)).toList() 29 | } 30 | 31 | fun obterDespesaPorId(id: String): Despesa? { 32 | return repositorioDespesa.findById(id).orElse(null) 33 | } 34 | 35 | fun obterDespesaPorNome(nome: String): Despesa? { 36 | return repositorioDespesa.findByNomeCategoria(nome) 37 | } 38 | 39 | @Transactional 40 | fun atualizarDespesas(ano: Int): List { 41 | if (ano > ANO_ATUAL || ano < ANO_INICIO) { 42 | throw Exception("Dados disponiveis somente a partir de $ANO_INICIO até $ANO_ATUAL") 43 | } 44 | 45 | val url = obterDebitoVereador 46 | 47 | val ultimoMes = if (ano == ANO_ATUAL) MES_ATUAL.minus(1) else 12 48 | val despesasAdicionadas: MutableList = mutableListOf() 49 | 50 | (1..ultimoMes).forEach { mes -> 51 | val xmlResponse = makePostRequest(url, ano, mes) 52 | val responseObj = parseXmlResponse(xmlResponse) 53 | 54 | val despesasDistintas = responseObj.items.distinctBy { it.nomeDespesa } 55 | despesasAdicionadas.addAll( 56 | despesasDistintas.map { 57 | repositorioDespesa.findByNomeCategoria(it.nomeDespesa) 58 | ?: repositorioDespesa.save(Despesa(nomeCategoria = it.nomeDespesa)) 59 | } 60 | ) 61 | } 62 | 63 | return despesasAdicionadas 64 | } 65 | 66 | @Transactional 67 | fun criarDespesa(despesa: Despesa): Despesa { 68 | return repositorioDespesa.save(despesa) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/config/TokenFilter.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.config 2 | 3 | import com.github.benmanes.caffeine.cache.LoadingCache 4 | import com.quorum.api.authentication.modelos.obterLimiteRequisicao 5 | import com.quorum.api.authentication.servicos.ServicoAutenticacao 6 | import org.springframework.http.HttpStatus 7 | import org.springframework.stereotype.Component 8 | import org.springframework.web.filter.GenericFilterBean 9 | import javax.servlet.FilterChain 10 | import javax.servlet.ServletRequest 11 | import javax.servlet.ServletResponse 12 | import javax.servlet.http.HttpServletRequest 13 | import javax.servlet.http.HttpServletResponse 14 | 15 | @Component 16 | class TokenFilter( 17 | private val servicoAutenticacao: ServicoAutenticacao, 18 | private val requestLimitCache: LoadingCache 19 | ) : GenericFilterBean() { 20 | 21 | companion object { 22 | private const val MAX_REQUESTS_PER_MINUTE = 5 23 | } 24 | 25 | override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { 26 | val httpRequest = request as HttpServletRequest 27 | val httpResponse = response as HttpServletResponse 28 | 29 | val headerToken = httpRequest.getHeader("token") 30 | 31 | if (headerToken != null) { 32 | val auth = try { 33 | servicoAutenticacao.obterAutenticacaoPorToken(headerToken) 34 | } catch (e: Exception) { 35 | httpResponse.sendResponse(HttpServletResponse.SC_UNAUTHORIZED, e.message) 36 | return 37 | } 38 | 39 | val requests = requestLimitCache.get(auth.token) ?: 0 40 | 41 | if (requests >= auth.obterLimiteRequisicao()) { 42 | httpResponse.sendResponse(HttpStatus.TOO_MANY_REQUESTS.value(), "Too many requests. Please wait a minute a try again.") 43 | return 44 | } 45 | 46 | requestLimitCache.put(headerToken, requests + 1) 47 | } 48 | 49 | chain.doFilter(request, response) 50 | } 51 | 52 | fun HttpServletResponse.sendResponse(responseStatus: Int, message: String?) { 53 | setHeader("Access-Control-Allow-Origin", "*") 54 | setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS") 55 | setHeader("Access-Control-Allow-Headers", "*") 56 | setHeader("Access-Control-Max-Age", "180") 57 | status = responseStatus 58 | contentType = "application/json" 59 | characterEncoding = "UTF-8" 60 | outputStream.writer().use { it.write("{\"status\": \"${responseStatus}\", \"message\": \"${message}\"}") } 61 | } 62 | fun getClientIP(request: HttpServletRequest): String { 63 | val xfHeader = request.getHeader("X-Forwarded-For") ?: return request.remoteAddr 64 | return xfHeader.split(",".toRegex()).dropLastWhile { it.isEmpty() } 65 | .toTypedArray()[0] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/reembolsos/modelos/ItemsReembolso.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.reembolsos.modelos 2 | 3 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper 4 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty 5 | import com.quorum.api.annotations.NoArgConstructor 6 | import org.springframework.data.annotation.CreatedDate 7 | import org.springframework.data.annotation.LastModifiedDate 8 | import org.springframework.data.jpa.domain.support.AuditingEntityListener 9 | import java.time.ZonedDateTime 10 | import java.util.UUID 11 | import javax.persistence.Column 12 | import javax.persistence.Entity 13 | import javax.persistence.EntityListeners 14 | import javax.persistence.Id 15 | import javax.persistence.Table 16 | 17 | data class ItemsReembolso( 18 | @JacksonXmlElementWrapper(useWrapping = false) 19 | @JacksonXmlProperty(localName = "TabelaPortalITEMREEMBOLSO") 20 | val items: List 21 | ) 22 | 23 | @Entity 24 | @Table(name = "item_reembolso") 25 | @EntityListeners(AuditingEntityListener::class) 26 | @NoArgConstructor 27 | data class Reembolso( 28 | @Id 29 | @Column(name = "id") 30 | val id: String = UUID.randomUUID().toString(), 31 | 32 | @Column(name = "id_vereador") 33 | @JacksonXmlProperty(localName = "Chave") 34 | val idVereador: String, 35 | 36 | @Column(name = "nome_vereador") 37 | @JacksonXmlProperty(localName = "VEREADOR") 38 | val nomeVereador: String, 39 | 40 | @Column(name = "id_centro_custo") 41 | @JacksonXmlProperty(localName = "CENTROCUSTOSID") 42 | val idCentroCusto: Double, 43 | 44 | @Column(name = "departamento") 45 | @JacksonXmlProperty(localName = "DEPARTAMENTO") 46 | val departamento: String, 47 | 48 | @Column(name = "tipo_departamento") 49 | @JacksonXmlProperty(localName = "TIPODEPARTAMENTO") 50 | val tipoDepartamento: Int, 51 | 52 | @Column(name = "ano") 53 | @JacksonXmlProperty(localName = "ANO") 54 | val ano: Int, 55 | 56 | @Column(name = "mes") 57 | @JacksonXmlProperty(localName = "MES") 58 | val mes: Int, 59 | 60 | @Column(name = "nome_despesa") 61 | @JacksonXmlProperty(localName = "DESPESA") 62 | val nomeDespesa: String, 63 | 64 | @Column(name = "id_despesa") 65 | val idDespesa: String? = null, 66 | 67 | @Column(name = "cnpj") 68 | @JacksonXmlProperty(localName = "CNPJ") 69 | val cnpj: String, 70 | 71 | @Column(name = "fornecedor") 72 | @JacksonXmlProperty(localName = "FORNECEDOR") 73 | val fornecedor: String, 74 | 75 | @Column(name = "valor") 76 | @JacksonXmlProperty(localName = "VALOR") 77 | val valor: Double, 78 | 79 | @CreatedDate 80 | @Column(name = "created_date", updatable = false) 81 | var createdDate: ZonedDateTime = ZonedDateTime.now(), 82 | 83 | @LastModifiedDate 84 | @Column(name = "modified_time") 85 | var modifiedDate: ZonedDateTime = ZonedDateTime.now() 86 | ) 87 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/vereadores/servicos/ServicoVereador.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.vereadores.servicos 2 | 3 | import com.quorum.api.connectivity.makePostRequest 4 | import com.quorum.api.connectivity.obterDebitoVereador 5 | import com.quorum.api.utils.ANO_ATUAL 6 | import com.quorum.api.utils.ANO_INICIO 7 | import com.quorum.api.utils.MES_ATUAL 8 | import com.quorum.api.utils.defaultPageable 9 | import com.quorum.api.vereadores.modelos.Vereador 10 | import com.quorum.api.vereadores.repositorios.RepositorioVereador 11 | import org.springframework.stereotype.Service 12 | import org.springframework.transaction.annotation.Transactional 13 | import parseXmlResponse 14 | 15 | @Service 16 | class ServicoVereador( 17 | private val repositorioVereador: RepositorioVereador 18 | ) { 19 | 20 | @Transactional 21 | fun apagarTodosVereadores(): List { 22 | val vereadores = repositorioVereador.findAll().toList() 23 | repositorioVereador.deleteAll(vereadores) 24 | return vereadores 25 | } 26 | 27 | fun obterVereadorPorId(id: String): Vereador? { 28 | return repositorioVereador.findById(id).orElse(null) 29 | } 30 | 31 | fun obterVereadorPorNome(nome: String): Vereador? { 32 | return repositorioVereador.findByNome(nome) 33 | } 34 | 35 | fun obterTodosVereadores(page: Int? = 0, pageSize: Int? = 100): List { 36 | return repositorioVereador.findAll(defaultPageable(page, pageSize)).toList() 37 | } 38 | 39 | @Transactional 40 | fun atualizarVereadores(ano: Int): List { 41 | if (ano > ANO_ATUAL || ano < ANO_INICIO) { 42 | throw Exception("Dados disponiveis somente a partir de $ANO_INICIO até $ANO_ATUAL") 43 | } 44 | 45 | val url = obterDebitoVereador 46 | 47 | val ultimoMes = if (ano == ANO_ATUAL) MES_ATUAL.minus(1) else 12 48 | val vereadoresAdicionados: MutableList = mutableListOf() 49 | 50 | (1..ultimoMes).forEach { mes -> 51 | val xmlResponse = makePostRequest(url, ano, mes) 52 | val responseObj = parseXmlResponse(xmlResponse) 53 | 54 | val vereadoresDistintos = responseObj.items.distinctBy { it.idVereador } 55 | vereadoresAdicionados.addAll( 56 | vereadoresDistintos.map { 57 | repositorioVereador.findById(it.idVereador).orElse( 58 | repositorioVereador.save( 59 | Vereador( 60 | id = it.idVereador, 61 | nome = it.nomeVereador 62 | ) 63 | ) 64 | ) 65 | } 66 | ) 67 | } 68 | 69 | return vereadoresAdicionados 70 | } 71 | 72 | @Transactional 73 | fun criarVereador(vereador: Vereador): Vereador { 74 | return repositorioVereador.save(vereador) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/fornecedores/servicos/ServicoFornecedor.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.fornecedores.servicos 2 | 3 | import com.quorum.api.connectivity.makePostRequest 4 | import com.quorum.api.connectivity.obterDebitoVereador 5 | import com.quorum.api.fornecedores.modelos.Fornecedor 6 | import com.quorum.api.fornecedores.repositories.RepositorioFornecedor 7 | import com.quorum.api.utils.ANO_ATUAL 8 | import com.quorum.api.utils.ANO_INICIO 9 | import com.quorum.api.utils.MES_ATUAL 10 | import com.quorum.api.utils.defaultPageable 11 | import org.springframework.stereotype.Service 12 | import org.springframework.transaction.annotation.Transactional 13 | import parseXmlResponse 14 | 15 | @Service 16 | class ServicoFornecedor( 17 | private val repositorioFornecedor: RepositorioFornecedor 18 | ) { 19 | 20 | @Transactional 21 | fun apagarTodosFornecedores(): List { 22 | val fornecedores = repositorioFornecedor.findAll().toList() 23 | repositorioFornecedor.deleteAll(fornecedores) 24 | return fornecedores 25 | } 26 | 27 | fun obterTodosFornecedores(page: Int? = 0, pageSize: Int? = 100): List { 28 | return repositorioFornecedor.findAll(defaultPageable(page, pageSize)).toList() 29 | } 30 | 31 | fun obterFornecedorPorCnpj(id: String): Fornecedor? { 32 | return repositorioFornecedor.findById(id).orElse(null) 33 | } 34 | 35 | @Transactional 36 | fun atualizarFornecedores(ano: Int): List { 37 | if (ano > ANO_ATUAL || ano < ANO_INICIO) { 38 | throw Exception("Dados disponiveis somente a partir de $ANO_INICIO até $ANO_ATUAL") 39 | } 40 | 41 | val ultimoMes = if (ano == ANO_ATUAL) MES_ATUAL.minus(1) else 12 42 | val fornecedoresAdicionados: MutableList = mutableListOf() 43 | 44 | val url = obterDebitoVereador 45 | 46 | (1..ultimoMes).forEach { mes -> 47 | val xmlResponse = makePostRequest(url, ano, mes) 48 | val responseObj = parseXmlResponse(xmlResponse) 49 | 50 | val fornecedoresDistintos = responseObj.items.distinctBy { it.fornecedor } 51 | fornecedoresAdicionados.addAll( 52 | fornecedoresDistintos.map { 53 | val cnpjFormatado = it.cnpj.replace(".", "").replace("/", "").replace("-", "") 54 | repositorioFornecedor.findByCnpj(cnpjFormatado) 55 | ?: repositorioFornecedor.save( 56 | Fornecedor( 57 | cnpj = cnpjFormatado, 58 | nome = it.fornecedor 59 | ) 60 | ) 61 | } 62 | ) 63 | } 64 | 65 | return fornecedoresAdicionados 66 | } 67 | 68 | @Transactional 69 | fun criarFornecedor(fornecedor: Fornecedor): Fornecedor { 70 | return repositorioFornecedor.save(fornecedor) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/reembolsos/controllers/ReembolsoController.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.reembolsos.controllers 2 | 3 | import com.quorum.api.reembolsos.modelos.Reembolso 4 | import com.quorum.api.reembolsos.servicos.ServicoReembolso 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.GetMapping 13 | import org.springframework.web.bind.annotation.RequestMapping 14 | import org.springframework.web.bind.annotation.RequestParam 15 | import org.springframework.web.bind.annotation.RestController 16 | 17 | @RestController 18 | @Tag(name = "5. Reembolsos", description = "Dados de reembolsos") 19 | @RequestMapping("/v1/reembolsos") 20 | class ReembolsoController( 21 | private val servicoReembolso: ServicoReembolso 22 | ) { 23 | 24 | @Operation( 25 | summary = "Listar reembolsos", 26 | description = "Lista todos os reembolsos de acordo com os filtros informados." + 27 | "\n\nNeste contexto, um 'reembolso' é uma solicitação feita por um vereador após uma despesa ser realizada." + 28 | "\n\nOs reembolsos são reportados mensalmente e contém informações como o ID do vereador, o CNPJ do fornecedor, o valor da despesa, etc.", 29 | responses = [ 30 | ApiResponse( 31 | responseCode = "200", 32 | description = "Sucesso", 33 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Reembolso::class))] 34 | ), 35 | ApiResponse( 36 | responseCode = "400", 37 | description = "Acesso negado" 38 | ) 39 | ], 40 | parameters = [ 41 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 42 | ] 43 | ) 44 | @GetMapping 45 | fun getAllReembolsos( 46 | @Parameter(description = "ID do vereador") @RequestParam idVereador: String? = null, 47 | @Parameter(description = "ID da despesa") @RequestParam idDespesa: String? = null, 48 | @Parameter(description = "CNPJ do fornecedor") @RequestParam cnpj: String? = null, 49 | @Parameter(description = "Ano do reembolso") @RequestParam ano: Int? = null, 50 | @Parameter(description = "Mes do reembolso") @RequestParam mes: Int? = null, 51 | @Parameter(description = "Pagina do resultado") @RequestParam page: Int?, 52 | @Parameter(description = "Tamanho da pagina do resultado") @RequestParam pageSize: Int? 53 | ): List { 54 | return servicoReembolso.obterTodosReembolsos(idVereador, idDespesa, cnpj, ano, mes, page, pageSize) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/authentication/controllers/AuthenticationController.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.authentication.controllers 2 | 3 | import com.quorum.api.authentication.modelos.Autenticacao 4 | import com.quorum.api.authentication.servicos.ServicoAutenticacao 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.PostMapping 13 | import org.springframework.web.bind.annotation.RequestMapping 14 | import org.springframework.web.bind.annotation.RequestParam 15 | import org.springframework.web.bind.annotation.RestController 16 | 17 | @RestController 18 | @Tag(name = "1. Autenticação", description = "Endpoints para criacao de tokens de autenticação") 19 | @RequestMapping("/v1/auth") 20 | class AuthenticationController( 21 | private val servicoAutenticacao: ServicoAutenticacao 22 | ) { 23 | @Operation( 24 | summary = "Criar token publico", 25 | description = "Cria um token publico para acesso aos endpoints.\n\nEste token permite apenas 10 requisicoes por minuto e expira após 7 dias.", 26 | responses = [ 27 | ApiResponse( 28 | responseCode = "200", 29 | description = "Sucesso", 30 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Autenticacao::class))] 31 | ), 32 | ApiResponse( 33 | responseCode = "400", 34 | description = "Acesso negado" 35 | ) 36 | ] 37 | ) 38 | @PostMapping("/publico/criar") 39 | fun generatePublicToken(): Autenticacao { 40 | return servicoAutenticacao.criarTokenPublico() 41 | } 42 | 43 | @Operation( 44 | summary = "Criar token privado", 45 | description = "Cria um token privado para acesso aos endpoints. " + 46 | "\n\nEste token permite 100 requisicoes por minuto e expira após 365 dias." + 47 | "\n\nO token será enviado para o email informado na requisicao.", 48 | responses = [ 49 | ApiResponse( 50 | responseCode = "200", 51 | description = "Sucesso", 52 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Autenticacao::class))] 53 | ), 54 | ApiResponse( 55 | responseCode = "400", 56 | description = "Acesso negado" 57 | ) 58 | ], 59 | parameters = [ 60 | Parameter(`in` = ParameterIn.QUERY, name = "email", description = "Email address", required = true, schema = Schema(type = "string")) 61 | ] 62 | ) 63 | @PostMapping("/privado/criar") 64 | fun generateUserToken( 65 | @RequestParam email: String 66 | ): Autenticacao { 67 | return servicoAutenticacao.criarTokenPrivado(email) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/despesas/controllers/DespesaControllerAdmin.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.despesas.controllers 2 | 3 | import com.quorum.api.despesas.modelos.Despesa 4 | import com.quorum.api.despesas.servicos.DespesaService 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.DeleteMapping 13 | import org.springframework.web.bind.annotation.PutMapping 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RequestParam 16 | import org.springframework.web.bind.annotation.RestController 17 | import javax.annotation.security.RolesAllowed 18 | 19 | @RolesAllowed("ROLE_ADMIN") 20 | @RestController 21 | @Tag(name = "0. Administrador", description = "Endpoints restritos para administradores") 22 | @RequestMapping("/v1/admin/despesas") 23 | class DespesaControllerAdmin( 24 | private val despesaService: DespesaService 25 | ) { 26 | 27 | @Operation( 28 | summary = "Atualizar despesas", 29 | description = "Atualiza todas as despesas.", 30 | responses = [ 31 | ApiResponse( 32 | responseCode = "200", 33 | description = "Sucesso", 34 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Despesa::class))] 35 | ), 36 | ApiResponse( 37 | responseCode = "400", 38 | description = "Acesso negado" 39 | ) 40 | ], 41 | parameters = [ 42 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 43 | ] 44 | ) 45 | @PutMapping("/atualizar") 46 | fun atualizarDespesas( 47 | @Parameter(description = "Ano da despesa") @RequestParam ano: Int 48 | ): List { 49 | return despesaService.atualizarDespesas(ano) 50 | } 51 | 52 | @Operation( 53 | summary = "Apagar despesas", 54 | description = "Apaga todas as despesas.", 55 | responses = [ 56 | ApiResponse( 57 | responseCode = "200", 58 | description = "Sucesso", 59 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Despesa::class))] 60 | ), 61 | ApiResponse( 62 | responseCode = "400", 63 | description = "Acesso negado" 64 | ) 65 | ], 66 | parameters = [ 67 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 68 | ] 69 | ) 70 | @DeleteMapping("/apagar/todas") 71 | fun apagarTodasDespesas(): List { 72 | return despesaService.apagarTodasDespesas() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/fornecedores/controllers/FornecedorControllerAdmin.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.fornecedores.controllers 2 | 3 | import com.quorum.api.fornecedores.modelos.Fornecedor 4 | import com.quorum.api.fornecedores.servicos.ServicoFornecedor 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.DeleteMapping 13 | import org.springframework.web.bind.annotation.PutMapping 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RequestParam 16 | import org.springframework.web.bind.annotation.RestController 17 | import javax.annotation.security.RolesAllowed 18 | 19 | @RolesAllowed("ROLE_ADMIN") 20 | @RestController 21 | @Tag(name = "0. Administrador", description = "Endpoints restritos para administradores") 22 | @RequestMapping("/v1/admin/fornecedores") 23 | class FornecedorControllerAdmin( 24 | private val servicoFornecedor: ServicoFornecedor 25 | ) { 26 | 27 | @Operation( 28 | summary = "Atualizar fornecedores", 29 | description = "Atualiza a lista de fornecedores com base no ano informado", 30 | responses = [ 31 | ApiResponse( 32 | responseCode = "200", 33 | description = "Sucesso", 34 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Fornecedor::class))] 35 | ), 36 | ApiResponse( 37 | responseCode = "400", 38 | description = "Acesso negado" 39 | ) 40 | ], 41 | parameters = [ 42 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 43 | ] 44 | ) 45 | @PutMapping("/atualizar") 46 | fun atualizarFornecedores( 47 | @RequestParam ano: Int 48 | ): List { 49 | return servicoFornecedor.atualizarFornecedores(ano) 50 | } 51 | 52 | @Operation( 53 | summary = "Apagar fornecedores", 54 | description = "Apaga todos os fornecedores do banco de dados", 55 | responses = [ 56 | ApiResponse( 57 | responseCode = "200", 58 | description = "Sucesso", 59 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Fornecedor::class))] 60 | ), 61 | ApiResponse( 62 | responseCode = "400", 63 | description = "Acesso negado" 64 | ) 65 | ], 66 | parameters = [ 67 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 68 | ] 69 | ) 70 | @DeleteMapping("/apagar/todos") 71 | fun apagarTodosFornecedores(): List { 72 | return servicoFornecedor.apagarTodosFornecedores() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/vereadores/controllers/VereadoresControllerAdmin.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.vereadores.controllers 2 | 3 | import com.quorum.api.vereadores.modelos.Vereador 4 | import com.quorum.api.vereadores.servicos.ServicoVereador 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.DeleteMapping 13 | import org.springframework.web.bind.annotation.PutMapping 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RequestParam 16 | import org.springframework.web.bind.annotation.RestController 17 | import javax.annotation.security.RolesAllowed 18 | 19 | @RolesAllowed("ROLE_ADMIN") 20 | @RestController 21 | @Tag(name = "0. Administrador", description = "Endpoints restritos para administradores") 22 | @RequestMapping("/v1/admin/vereadores") 23 | class VereadoresControllerAdmin( 24 | private val servicoVereador: ServicoVereador 25 | ) { 26 | 27 | @Operation( 28 | summary = "Atualizar vereadores", 29 | description = "Atualiza a lista de vereadores com base no ano informado", 30 | responses = [ 31 | ApiResponse( 32 | responseCode = "200", 33 | description = "Sucesso", 34 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Vereador::class))] 35 | ), 36 | ApiResponse( 37 | responseCode = "400", 38 | description = "Acesso negado" 39 | ) 40 | ], 41 | parameters = [ 42 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 43 | ] 44 | ) 45 | @PutMapping("/atualizar") 46 | fun atualizarVereadores( 47 | @Parameter(description = "Ano no banco de dados") @RequestParam ano: Int 48 | ): List { 49 | return servicoVereador.atualizarVereadores(ano) 50 | } 51 | 52 | @Operation( 53 | summary = "Apagar vereadores", 54 | description = "Apaga todos os vereadores do banco de dados", 55 | responses = [ 56 | ApiResponse( 57 | responseCode = "200", 58 | description = "Sucesso", 59 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Vereador::class))] 60 | ), 61 | ApiResponse( 62 | responseCode = "400", 63 | description = "Acesso negado" 64 | ) 65 | ], 66 | parameters = [ 67 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 68 | ] 69 | ) 70 | @DeleteMapping("/apagar/todos") 71 | fun apagarTodosVereadores(): List { 72 | return servicoVereador.apagarTodosVereadores() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/reembolsos/controllers/ReembolsoControllerAdmin.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.reembolsos.controllers 2 | 3 | import com.quorum.api.reembolsos.modelos.Reembolso 4 | import com.quorum.api.reembolsos.servicos.ServicoReembolso 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.DeleteMapping 13 | import org.springframework.web.bind.annotation.PutMapping 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RequestParam 16 | import org.springframework.web.bind.annotation.RestController 17 | import javax.annotation.security.RolesAllowed 18 | 19 | @RolesAllowed("ROLE_ADMIN") 20 | @RestController 21 | @Tag(name = "0. Administrador", description = "Endpoints restritos para administradores") 22 | @RequestMapping("/v1/admin/reembolsos") 23 | class ReembolsoControllerAdmin( 24 | private val servicoReembolso: ServicoReembolso 25 | ) { 26 | 27 | @Operation( 28 | summary = "Atualizar reembolsos", 29 | description = "Atualiza a lista de reembolsos com base no ano informado", 30 | responses = [ 31 | ApiResponse( 32 | responseCode = "200", 33 | description = "Sucesso", 34 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Reembolso::class))] 35 | ), 36 | ApiResponse( 37 | responseCode = "400", 38 | description = "Acesso negado" 39 | ) 40 | ], 41 | parameters = [ 42 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 43 | ] 44 | ) 45 | @PutMapping("/atualizar") 46 | fun atualizarReembolsos( 47 | @Parameter(description = "Ano do reembolso") 48 | @RequestParam(required = true) 49 | ano: Int, 50 | 51 | @Parameter(description = "Mes do reembolso") 52 | @RequestParam(required = false) 53 | mes: Int? 54 | ): List { 55 | return servicoReembolso.atualizarReembolsos(ano, mes) 56 | } 57 | 58 | @Operation( 59 | summary = "Apagar reembolsos", 60 | description = "Remove todos os reembolsos do banco de dados", 61 | responses = [ 62 | ApiResponse( 63 | responseCode = "200", 64 | description = "Sucesso", 65 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Reembolso::class))] 66 | ), 67 | ApiResponse( 68 | responseCode = "400", 69 | description = "Acesso negado" 70 | ) 71 | ], 72 | parameters = [ 73 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 74 | ] 75 | ) 76 | @DeleteMapping("/apagar/todos") 77 | fun apagarTodosReembolsos(): Boolean { 78 | return servicoReembolso.apagarTodosReembolsos() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/vereadores/controllers/VereadorController.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.vereadores.controllers 2 | 3 | import com.quorum.api.vereadores.modelos.Vereador 4 | import com.quorum.api.vereadores.servicos.ServicoVereador 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.GetMapping 13 | import org.springframework.web.bind.annotation.PathVariable 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RequestParam 16 | import org.springframework.web.bind.annotation.RestController 17 | 18 | @RestController 19 | @Tag(name = "2. Vereadores", description = "Endpoints para dados de vereadores") 20 | @RequestMapping("/v1/vereadores") 21 | class VereadorController( 22 | private val servicoVereador: ServicoVereador 23 | ) { 24 | @Operation( 25 | summary = "Listar vereadores", 26 | description = "Lista todos os vereadores.", 27 | responses = [ 28 | ApiResponse( 29 | responseCode = "200", 30 | description = "Sucesso", 31 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Vereador::class))] 32 | ), 33 | ApiResponse( 34 | responseCode = "400", 35 | description = "Acesso negado" 36 | ) 37 | ], 38 | parameters = [ 39 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 40 | ] 41 | ) 42 | @GetMapping 43 | fun obterTodosVereadores( 44 | @Parameter(description = "Pagina do resultado") @RequestParam page: Int?, 45 | @Parameter(description = "Tamanho da pagina do resultado") @RequestParam pageSize: Int? 46 | ): List { 47 | return servicoVereador.obterTodosVereadores(page, pageSize) 48 | } 49 | 50 | @Operation( 51 | summary = "Obter vereador", 52 | description = "Obtem vereador por ID.", 53 | responses = [ 54 | ApiResponse( 55 | responseCode = "200", 56 | description = "Sucesso", 57 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Vereador::class))] 58 | ), 59 | ApiResponse( 60 | responseCode = "400", 61 | description = "Acesso negado" 62 | ), 63 | ApiResponse( 64 | responseCode = "404", 65 | description = "Vereador não encontrado" 66 | ) 67 | ], 68 | parameters = [ 69 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 70 | ] 71 | ) 72 | @GetMapping("/{id}") 73 | fun obterVereadorPorId( 74 | @Parameter(description = "ID do vereador") @PathVariable id: String 75 | ): Vereador? { 76 | return servicoVereador.obterVereadorPorId(id) ?: throw Exception("Vereador não encontrado") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/fornecedores/controllers/FornecedorController.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.fornecedores.controllers 2 | 3 | import com.quorum.api.fornecedores.modelos.Fornecedor 4 | import com.quorum.api.fornecedores.servicos.ServicoFornecedor 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.GetMapping 13 | import org.springframework.web.bind.annotation.PathVariable 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RequestParam 16 | import org.springframework.web.bind.annotation.RestController 17 | 18 | @RestController 19 | @Tag(name = "4. Fornecedores", description = "Dados de fornecedores") 20 | @RequestMapping("/v1/fornecedores") 21 | class FornecedorController( 22 | private val servicoFornecedor: ServicoFornecedor 23 | ) { 24 | @Operation( 25 | summary = "Listar fornecedores", 26 | description = "Lista todos os fornecedores." + 27 | "\n\nNeste contexto, um 'fornecedor' é uma empresa ou pessoa jurídica que fornece produtos ou serviços para a Câmara dos Deputados.", 28 | responses = [ 29 | ApiResponse( 30 | responseCode = "200", 31 | description = "Sucesso", 32 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Fornecedor::class))] 33 | ), 34 | ApiResponse( 35 | responseCode = "400", 36 | description = "Acesso negado" 37 | ) 38 | ], 39 | parameters = [ 40 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 41 | ] 42 | ) 43 | @GetMapping 44 | fun getAllFornecedores( 45 | @Parameter(description = "Pagina do resultado") @RequestParam page: Int?, 46 | @Parameter(description = "Tamanho da pagina do resultado") @RequestParam pageSize: Int? 47 | ): List { 48 | return servicoFornecedor.obterTodosFornecedores(page, pageSize) 49 | } 50 | 51 | @Operation( 52 | summary = "Obter fornecedor", 53 | description = "Obtem fornecedor pelo CNPJ.", 54 | responses = [ 55 | ApiResponse( 56 | responseCode = "200", 57 | description = "Sucesso", 58 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Fornecedor::class))] 59 | ), 60 | ApiResponse( 61 | responseCode = "400", 62 | description = "Acesso negado" 63 | ) 64 | ], 65 | parameters = [ 66 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 67 | ] 68 | ) 69 | @GetMapping("/{cnpj}") 70 | fun obterFornecedorPorCnpj( 71 | @Parameter(description = "Codigo do CNPJ (sem pontos ou tracos") @PathVariable cnpj: String 72 | ): Fornecedor { 73 | return servicoFornecedor.obterFornecedorPorCnpj(cnpj) ?: throw Exception("Fornecedor não encontrado") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/despesas/controllers/DespesaController.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.despesas.controllers 2 | 3 | import com.quorum.api.despesas.modelos.Despesa 4 | import com.quorum.api.despesas.servicos.DespesaService 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.GetMapping 13 | import org.springframework.web.bind.annotation.PathVariable 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RequestParam 16 | import org.springframework.web.bind.annotation.RestController 17 | 18 | @RestController 19 | @Tag(name = "3. Despesas", description = "Dados de despesas") 20 | @RequestMapping("/v1/despesas") 21 | class DespesaController( 22 | private val despesaService: DespesaService 23 | ) { 24 | @Operation( 25 | summary = "Listar despesas", 26 | description = "Lista todas as despesas." + 27 | "\n\nNeste contexto, uma 'despesa' é uma categoria de gasto, como 'correios', 'estacionamento' ou 'locacao de veículos' .", 28 | responses = [ 29 | ApiResponse( 30 | responseCode = "200", 31 | description = "Sucesso", 32 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Despesa::class))] 33 | ), 34 | ApiResponse( 35 | responseCode = "400", 36 | description = "Acesso negado" 37 | ) 38 | ], 39 | parameters = [ 40 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 41 | ] 42 | ) 43 | @GetMapping 44 | fun obterTodasDespesas( 45 | @Parameter(description = "Pagina do resultado") @RequestParam page: Int?, 46 | @Parameter(description = "Tamanho da pagina do resultado") @RequestParam pageSize: Int? 47 | ): List { 48 | return despesaService.obterTodasDespesas(page, pageSize) 49 | } 50 | 51 | @Operation( 52 | summary = "Obter despesa", 53 | description = "Obtem despesa por ID.", 54 | responses = [ 55 | ApiResponse( 56 | responseCode = "200", 57 | description = "Sucesso", 58 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Despesa::class))] 59 | ), 60 | ApiResponse( 61 | responseCode = "400", 62 | description = "Acesso negado" 63 | ), 64 | ApiResponse( 65 | responseCode = "404", 66 | description = "Despesa não encontrada" 67 | ) 68 | ], 69 | parameters = [ 70 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 71 | ] 72 | ) 73 | @GetMapping("/{id}") 74 | fun obterDespesaPorId( 75 | @Parameter(description = "ID da despesa") @PathVariable id: String 76 | ): Despesa { 77 | return despesaService.obterDespesaPorId(id) ?: throw Exception("Despesa não encontrado") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/authentication/servicos/ServicoAutenticacao.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.authentication.servicos 2 | 3 | import com.quorum.api.authentication.modelos.Autenticacao 4 | import com.quorum.api.authentication.repositories.RepositorioAutenticacao 5 | import com.quorum.api.messaging.ServicoEmail 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken 7 | import org.springframework.security.core.authority.SimpleGrantedAuthority 8 | import org.springframework.security.core.context.SecurityContextHolder 9 | import org.springframework.stereotype.Service 10 | import org.springframework.transaction.annotation.Transactional 11 | import java.time.ZonedDateTime 12 | import java.util.UUID 13 | 14 | @Service 15 | class ServicoAutenticacao( 16 | private val repositorioAutenticacao: RepositorioAutenticacao, 17 | private val servicoEmail: ServicoEmail 18 | ) { 19 | 20 | // Temporarily setting the token as a role admin 21 | @Transactional 22 | fun criarTokenAdmin(): Autenticacao { 23 | val token = UUID.randomUUID().toString() 24 | return repositorioAutenticacao.save(Autenticacao(token = token, dataExpiracao = ZonedDateTime.now().plusDays(365), role = "ADMIN")) 25 | } 26 | 27 | fun obterTodasAutenticacoes(): List { 28 | return repositorioAutenticacao.findAll().toList() 29 | } 30 | 31 | @Transactional 32 | fun criarTokenPrivado(email: String): Autenticacao { 33 | if (email.isBlank()) throw Exception("Email nao pode ser vazio") 34 | 35 | val tokenExistente = repositorioAutenticacao.findByEmail(email) 36 | if (tokenExistente != null) { 37 | throw Exception( 38 | "Email $email já possui um token. " + 39 | "Se precisar recuperar seu token nos contate pelo Github: https://github.com/thiagobarbosa/quorum-api" 40 | ) 41 | } 42 | 43 | val token = UUID.randomUUID().toString() 44 | servicoEmail.sendEmail(email, "Seu token privado foi criado com sucesso", token) 45 | val tokenSalvo = repositorioAutenticacao.save(Autenticacao(token = token, dataExpiracao = ZonedDateTime.now().plusDays(365), role = "USER", email = email)) 46 | return Autenticacao(token = "token enviado por email", dataExpiracao = tokenSalvo.dataExpiracao, role = tokenSalvo.role, email = tokenSalvo.email) 47 | } 48 | 49 | @Transactional 50 | fun criarTokenPublico(): Autenticacao { 51 | val token = UUID.randomUUID().toString() 52 | return repositorioAutenticacao.save(Autenticacao(token = token, dataExpiracao = ZonedDateTime.now().plusDays(7), role = "PUBLIC")) 53 | } 54 | 55 | fun obterAutenticacaoPorToken(token: String): Autenticacao { 56 | val autenticacao = repositorioAutenticacao.findById(token).orElseThrow { Exception("Token $token nao encontrado.") } 57 | if (autenticacao.dataExpiracao.isBefore(ZonedDateTime.now())) { 58 | throw Exception("Token $token está expirado.") 59 | } 60 | definirSecurityContext(autenticacao) 61 | return autenticacao 62 | } 63 | 64 | @Transactional 65 | fun apagarToken(token: String): Autenticacao { 66 | val tokenExistente = repositorioAutenticacao.findById(token).orElseThrow { Exception("Token $token nao encontrado") } 67 | repositorioAutenticacao.delete(tokenExistente) 68 | return tokenExistente 69 | } 70 | 71 | @Transactional 72 | fun apagarTodasAutenticacoesAdmin(): List { 73 | val tokensAdmin = repositorioAutenticacao.findAllByRole("ADMIN") 74 | repositorioAutenticacao.deleteAll(tokensAdmin) 75 | return tokensAdmin 76 | } 77 | 78 | fun definirSecurityContext(autenticacao: Autenticacao) { 79 | SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken( 80 | autenticacao.token, 81 | null, 82 | listOf( 83 | SimpleGrantedAuthority("ROLE_${autenticacao.role}") 84 | ) 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/authentication/controllers/AdminAuthController.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.authentication.controllers 2 | 3 | import com.quorum.api.authentication.modelos.Autenticacao 4 | import com.quorum.api.authentication.servicos.ServicoAutenticacao 5 | import io.swagger.v3.oas.annotations.Operation 6 | import io.swagger.v3.oas.annotations.Parameter 7 | import io.swagger.v3.oas.annotations.enums.ParameterIn 8 | import io.swagger.v3.oas.annotations.media.Content 9 | import io.swagger.v3.oas.annotations.media.Schema 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.web.bind.annotation.DeleteMapping 13 | import org.springframework.web.bind.annotation.GetMapping 14 | import org.springframework.web.bind.annotation.PathVariable 15 | import org.springframework.web.bind.annotation.PostMapping 16 | import org.springframework.web.bind.annotation.RequestMapping 17 | import org.springframework.web.bind.annotation.RestController 18 | import javax.annotation.security.RolesAllowed 19 | 20 | @RolesAllowed("ROLE_ADMIN") 21 | @RestController 22 | @Tag(name = "0. Administrador", description = "Endpoints restritos para administradores") 23 | @RequestMapping("/v1/admin") 24 | class AdminAuthController( 25 | private val servicoAutenticacao: ServicoAutenticacao 26 | ) { 27 | 28 | @Operation( 29 | summary = "Obter todas as autenticações do banco de dados", 30 | description = "Obter lista com todas as autenticações criadas", 31 | responses = [ 32 | ApiResponse( 33 | responseCode = "200", 34 | description = "Sucesso", 35 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Autenticacao::class))] 36 | ), 37 | ApiResponse( 38 | responseCode = "400", 39 | description = "Acesso negado" 40 | ) 41 | ], 42 | parameters = [ 43 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 44 | ] 45 | ) 46 | @GetMapping("/auth") 47 | fun obterTodasAutenticacoes(): List { 48 | return servicoAutenticacao.obterTodasAutenticacoes() 49 | } 50 | 51 | @Operation( 52 | summary = "Criar autenticacao", 53 | description = "Criar uma autenticação com permissão de administrador", 54 | responses = [ 55 | ApiResponse( 56 | responseCode = "200", 57 | description = "Sucesso", 58 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Autenticacao::class))] 59 | ), 60 | ApiResponse( 61 | responseCode = "400", 62 | description = "Acesso negado" 63 | ) 64 | ], 65 | parameters = [ 66 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 67 | ] 68 | ) 69 | @PostMapping("/auth/criar") 70 | fun criarTokenAdmin(): Autenticacao { 71 | return servicoAutenticacao.criarTokenAdmin() 72 | } 73 | 74 | @Operation( 75 | summary = "Apagar autenticacoes", 76 | description = "Apagar todas as autenticacoes de administradores", 77 | responses = [ 78 | ApiResponse( 79 | responseCode = "200", 80 | description = "Sucesso", 81 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Autenticacao::class))] 82 | ), 83 | ApiResponse( 84 | responseCode = "400", 85 | description = "Acesso negado" 86 | ) 87 | ], 88 | parameters = [ 89 | Parameter(`in` = ParameterIn.HEADER, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 90 | ] 91 | ) 92 | @DeleteMapping("/auth/apagar/todos") 93 | fun apagarTodasAutenticacoesAdmin(): List { 94 | return servicoAutenticacao.apagarTodasAutenticacoesAdmin() 95 | } 96 | 97 | @Operation( 98 | summary = "Apagar autenticacao", 99 | description = "Apagar uma autenticacao de administrador", 100 | responses = [ 101 | ApiResponse( 102 | responseCode = "200", 103 | description = "Sucesso", 104 | content = [Content(mediaType = "application/json", schema = Schema(implementation = Autenticacao::class))] 105 | ), 106 | ApiResponse( 107 | responseCode = "400", 108 | description = "Acesso negado" 109 | ) 110 | ], 111 | parameters = [ 112 | Parameter(`in` = ParameterIn.PATH, name = "token", description = "Token de acesso", required = true, schema = Schema(type = "string")) 113 | ] 114 | ) 115 | @DeleteMapping("/auth/apagar/{token}") 116 | fun apagarToken( 117 | @Parameter(description = "Token a ser apagado") @PathVariable token: String 118 | ): Autenticacao { 119 | return servicoAutenticacao.apagarToken(token) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/kotlin/com/quorum/api/reembolsos/servicos/ServicoReembolso.kt: -------------------------------------------------------------------------------- 1 | package com.quorum.api.reembolsos.servicos 2 | 3 | import com.quorum.api.connectivity.makePostRequest 4 | import com.quorum.api.connectivity.obterDebitoVereador 5 | import com.quorum.api.despesas.modelos.Despesa 6 | import com.quorum.api.despesas.servicos.DespesaService 7 | import com.quorum.api.fornecedores.modelos.Fornecedor 8 | import com.quorum.api.fornecedores.servicos.ServicoFornecedor 9 | import com.quorum.api.reembolsos.modelos.Reembolso 10 | import com.quorum.api.reembolsos.repositories.ReembolsoSpecification 11 | import com.quorum.api.reembolsos.repositories.RepositorioReembolso 12 | import com.quorum.api.utils.ANO_ATUAL 13 | import com.quorum.api.utils.ANO_INICIO 14 | import com.quorum.api.utils.MES_ATUAL 15 | import com.quorum.api.utils.defaultPageable 16 | import com.quorum.api.vereadores.modelos.Vereador 17 | import com.quorum.api.vereadores.servicos.ServicoVereador 18 | import org.springframework.stereotype.Service 19 | import org.springframework.transaction.annotation.Transactional 20 | import parseXmlResponse 21 | import kotlin.math.min 22 | 23 | @Service 24 | class ServicoReembolso( 25 | private val repositorioReembolso: RepositorioReembolso, 26 | private val despesaService: DespesaService, 27 | private val servicoVereador: ServicoVereador, 28 | private val servicoFornecedor: ServicoFornecedor 29 | ) { 30 | 31 | @Transactional 32 | fun apagarTodosReembolsos(): Boolean { 33 | repositorioReembolso.deleteAll() 34 | return true 35 | } 36 | 37 | @Transactional 38 | fun atualizarReembolsos(ano: Int, mes: Int? = null): List { 39 | if (ano > ANO_ATUAL || ano < ANO_INICIO) { 40 | throw Exception("Dados disponiveis somente a partir de $ANO_INICIO até $ANO_ATUAL") 41 | } 42 | 43 | // calculate o ultimo mes disponivel para ser processado 44 | val ultimoMesDisponivel = if (ano == ANO_ATUAL) MES_ATUAL.minus(1) else 12 45 | 46 | mes?.let { 47 | if (it > ultimoMesDisponivel || it < 1) { 48 | throw Exception("Dados disponiveis somente a partir de $ANO_INICIO até $ANO_ATUAL no mes $ultimoMesDisponivel") 49 | } 50 | } 51 | 52 | // define o mes inicial a ser processado 53 | val mesInicial = mes ?: 1 54 | 55 | // limita o mes a ser processado ao ultimo mes disponivel 56 | val mesFinal = min(ultimoMesDisponivel, mes ?: ultimoMesDisponivel) 57 | 58 | // Já que a API fonte nao possui uma chave unica para reembolsos, apagamos todos os reembolsos do ano/mes 59 | val reembolsosExistentes = if (mes == null) repositorioReembolso.findAllByAno(ano) else repositorioReembolso.findAllByAnoEqualsAndMesEquals(ano, mesFinal) 60 | repositorioReembolso.deleteAll(reembolsosExistentes) 61 | 62 | val url = obterDebitoVereador 63 | 64 | val reembolsosAdicionados: MutableList = mutableListOf() 65 | 66 | (mesInicial..mesFinal).forEach { mesProcessado -> 67 | val xmlResponse = makePostRequest(url, ano, mesProcessado) 68 | val responseObj = parseXmlResponse(xmlResponse) 69 | 70 | repositorioReembolso.findById(responseObj.items.first().idVereador).ifPresent { 71 | throw Exception("Reembolsos já atualizados para o ano $ano") 72 | } 73 | 74 | reembolsosAdicionados.addAll( 75 | responseObj.items.map { 76 | val cnpjFormatado = it.cnpj.replace(".", "").replace("/", "").replace("-", "") 77 | val despesa = despesaService.obterDespesaPorNome(it.nomeDespesa) ?: despesaService.criarDespesa(Despesa(nomeCategoria = it.nomeDespesa)) 78 | val vereador = servicoVereador.obterVereadorPorId(it.idVereador) ?: servicoVereador.criarVereador(Vereador(id = it.idVereador, nome = it.nomeVereador)) 79 | val fornecedor = servicoFornecedor.obterFornecedorPorCnpj(cnpjFormatado) ?: servicoFornecedor.criarFornecedor( 80 | Fornecedor(cnpj = cnpjFormatado, nome = it.fornecedor) 81 | ) 82 | Reembolso( 83 | idVereador = vereador.id, 84 | nomeVereador = vereador.nome, 85 | idCentroCusto = it.idCentroCusto, 86 | departamento = it.departamento, 87 | tipoDepartamento = it.tipoDepartamento, 88 | ano = it.ano, 89 | mes = it.mes, 90 | nomeDespesa = despesa.nomeCategoria, 91 | idDespesa = despesa.id, 92 | cnpj = fornecedor.cnpj, 93 | fornecedor = fornecedor.nome, 94 | valor = it.valor 95 | ) 96 | } 97 | ) 98 | } 99 | 100 | repositorioReembolso.saveAll(reembolsosAdicionados) 101 | 102 | return reembolsosAdicionados 103 | } 104 | 105 | fun obterTodosReembolsos(idVereador: String?, idDespesa: String?, cnpj: String?, ano: Int?, mes: Int?, page: Int? = 0, pageSize: Int? = 100): List { 106 | val specification = ReembolsoSpecification(idVereador, idDespesa, cnpj, ano, mes) 107 | return repositorioReembolso.findAll(specification, defaultPageable(page, pageSize)).content 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quorum API 2 | 3 | ## Introdução 4 | Este projeto contém o código fonte de uma API para obter os gastos públicos dos membros da Câmara Municipal de São Paulo. 5 | Os dados são fornecidos pelo [sistema SisGV](https://www.saopaulo.sp.leg.br/transparencia/dados-abertos/dados-disponibilizados-em-formato-aberto), disponíveis publicamente sob a Lei de Acesso à Informação do Brasil. 6 | 7 | ## Motivação 8 | Os dados fornecidos pelo sistema da prefeitura de São Paulo não são muito amigáveis ou práticos: 9 | - É uma API SOAP, que não é muito moderno 10 | - Não é muito bem documentada 11 | - Não é muito bem estruturada, já que os objetos nas respostas não são tipados adequadamente. Na maioria das vezes, os registros sequer possuem uma chave única 12 | - Só é possível fazer solicitações filtrando por um ano e mês específicos, o que dificulta obter todos os dados de uma só vez ou aplicar filtros durante a requisição 13 | 14 | Com isso em mente, este projeto obtém os dados do sistema SisGV, mapeia esses dados em estruturas adequadas, salva-os em um banco de dados e os expõe em endpoints REST mais fáceis de consumir. 15 | 16 | ## Endpoints 17 | Todos os endpoints podem ser encontrados na página do [Swagger](https://api.quorum-tech.io/swagger-ui/index.html?urls.primaryName=User%20API). 18 | Também há uma coleção do Postman que você pode baixar [aqui](https://www.postman.com/thiagobarbosa-br/workspace/quorum). 19 | 20 | ## Casos de uso comuns 21 | Estes são alguns casos de uso comuns que você pode fazer com esta API: 22 |

23 |
24 | Obter todos os gastos de um vereador 25 | 26 | ### Requisição 27 | ``` 28 | GET /v1/reembolsos?idVereador=1386 29 | ``` 30 | ### Resposta 31 | ``` 32 | [ 33 | { 34 | "id": "0026a323-7bd1-4c37-95d2-49685a2304f5", 35 | "idVereador": "1386", 36 | "nomeVereador": "ISAC FELIX", 37 | "idCentroCusto": 386.0, 38 | "departamento": "GABINETE DE VEREADOR", 39 | "tipoDepartamento": 1, 40 | "ano": 2020, 41 | "mes": 10, 42 | "nomeDespesa": "CONTRATAÇAO DE PESSOA JURIDICA", 43 | "idDespesa": "5d72c2e7-2dc7-4de5-993a-5bb69ea2f391", 44 | "cnpj": "67002329000162", 45 | "fornecedor": "WORK LINE SYSTEM INFORMÁTICA LTDA", 46 | "valor": 1040.0, 47 | "createdDate": "2023-12-20T21:52:50Z", 48 | "modifiedDate": "2023-12-20T21:52:50Z" 49 | }, 50 | { 51 | "id": "005a4b48-9e22-4d20-85d6-be069daf140b", 52 | "idVereador": "1386", 53 | "nomeVereador": "ISAC FELIX", 54 | "idCentroCusto": 386.0, 55 | "departamento": "GABINETE DE VEREADOR", 56 | "tipoDepartamento": 1, 57 | "ano": 2018, 58 | "mes": 1, 59 | "nomeDespesa": "COMPOSIÇÃO/ARTE/DIAGRAMAÇÃO/PRODUÇÃO/IMPRESSAO GRAFICA", 60 | "idDespesa": "b86d108f-7a15-41f3-b1d8-dea72a6fd2ec", 61 | "cnpj": "09520361000133", 62 | "fornecedor": "ECO WORLD PRODUTOS E SERVICOS CORPORATIVOS LTDA", 63 | "valor": 7980.0, 64 | "createdDate": "2023-12-20T22:03:12Z", 65 | "modifiedDate": "2023-12-20T22:03:12Z" 66 | }, 67 | { 68 | "id": "014223ce-7488-4b62-b6bb-5691e74c2f4a", 69 | "idVereador": "1386", 70 | "nomeVereador": "ISAC FELIX", 71 | "idCentroCusto": 386.0, 72 | "departamento": "GABINETE DE VEREADOR", 73 | "tipoDepartamento": 1, 74 | "ano": 2019, 75 | "mes": 9, 76 | "nomeDespesa": "MATERIAL DE ESCRITORIO E OUTROS MATERIAIS DE CONSUMO", 77 | "idDespesa": "380c745b-b7ec-4e41-925a-5fa49ed9f5e8", 78 | "cnpj": "43283811003256", 79 | "fornecedor": "KALUNGA S/A", 80 | "valor": 1575.44, 81 | "createdDate": "2023-12-20T21:53:43Z", 82 | "modifiedDate": "2023-12-20T21:53:43Z" 83 | } 84 | ] 85 | ``` 86 |
87 | 88 |
89 |
90 | Obter os gastos de todos os vereadores em um ano específico 91 | 92 | ### 93 | ### Requisição 94 | ``` 95 | GET /v1/reembolsos?ano=2022 96 | ``` 97 | ### Resposta 98 | ``` 99 | [ 100 | { 101 | "id": "00b424ef-716c-46c0-874a-846c677ac292", 102 | "idVereador": "1494", 103 | "nomeVereador": "SONAIRA FERNANDES", 104 | "idCentroCusto": 494.0, 105 | "departamento": "GABINETE DE VEREADOR", 106 | "tipoDepartamento": 1, 107 | "ano": 2022, 108 | "mes": 1, 109 | "nomeDespesa": "ELABORAÇÃO/MANUTENÇAO DE SITE/HOSPEDAGEM", 110 | "idDespesa": "575018f7-f0d1-46c8-8f07-52b19fbe8f8e", 111 | "cnpj": "23584331000111", 112 | "fornecedor": "GENTE - AGENCIA DE PUBLICIDADE EIRELI - ME", 113 | "valor": 7300.0, 114 | "createdDate": "2023-12-20T21:46:49Z", 115 | "modifiedDate": "2023-12-20T21:46:49Z" 116 | }, 117 | { 118 | "id": "01d11821-0461-45ec-a20a-c5d120553c0f", 119 | "idVereador": "1378", 120 | "nomeVereador": "ANDRÉ SANTOS", 121 | "idCentroCusto": 378.0, 122 | "departamento": "GABINETE DE VEREADOR", 123 | "tipoDepartamento": 1, 124 | "ano": 2022, 125 | "mes": 1, 126 | "nomeDespesa": "CONTRATAÇAO DE PESSOA JURIDICA", 127 | "idDespesa": "5d72c2e7-2dc7-4de5-993a-5bb69ea2f391", 128 | "cnpj": "11521613000190", 129 | "fornecedor": "TECNEGOCIOS SOLUÇOES EM INFORMATICA LTDA- ME", 130 | "valor": 599.0, 131 | "createdDate": "2023-12-20T21:46:48Z", 132 | "modifiedDate": "2023-12-20T21:46:48Z" 133 | }, 134 | { 135 | "id": "0316b83c-8b82-4de9-96cb-0c1b66495de1", 136 | "idVereador": "1388", 137 | "nomeVereador": "JOÃO JORGE", 138 | "idCentroCusto": 388.0, 139 | "departamento": "GABINETE DE VEREADOR", 140 | "tipoDepartamento": 1, 141 | "ano": 2022, 142 | "mes": 1, 143 | "nomeDespesa": "INTERMEDIADO - LOCAÇÃO VEICULOS HIBRIDOS", 144 | "idDespesa": "575c1442-2347-41db-b8e0-8fcf811bf8fb", 145 | "cnpj": "50176288000128", 146 | "fornecedor": "CAMARA MUNICIPAL DE SÃO PAULO", 147 | "valor": 4800.0, 148 | "createdDate": "2023-12-20T21:46:48Z", 149 | "modifiedDate": "2023-12-20T21:46:48Z" 150 | } 151 | ] 152 | ``` 153 |
154 | 155 |
156 | 157 |
158 | Obter todos os gastos feitos para uma empresa específica 159 | 160 | ### Requisição 161 | ``` 162 | /v1/reembolsos?cnpj=07679089000103 163 | ``` 164 | 165 | ### Resposta: 166 | ``` 167 | [ 168 | { 169 | "id": "0001dab3-15eb-4d7c-b338-3bfc64fa7ef6", 170 | "idVereador": "1394", 171 | "nomeVereador": "RUTE COSTA", 172 | "idCentroCusto": 394.0, 173 | "departamento": "GABINETE DE VEREADOR", 174 | "tipoDepartamento": 1, 175 | "ano": 2023, 176 | "mes": 2, 177 | "nomeDespesa": "COMBUSTIVEL", 178 | "idDespesa": "016c0dab-8167-4444-8e57-2491794298ac", 179 | "cnpj": "07679089000103", 180 | "fornecedor": "AUTO POSTO INDEPENDENCIA DO CAMBUCI LTDA.", 181 | "valor": 447.7, 182 | "createdDate": "2023-12-20T21:45:05Z", 183 | "modifiedDate": "2023-12-20T21:45:05Z" 184 | }, 185 | { 186 | "id": "000b9534-a8ed-4453-be63-2f7fed60f1e8", 187 | "idVereador": "1394", 188 | "nomeVereador": "RUTE COSTA", 189 | "idCentroCusto": 394.0, 190 | "departamento": "GABINETE DE VEREADOR", 191 | "tipoDepartamento": 1, 192 | "ano": 2019, 193 | "mes": 9, 194 | "nomeDespesa": "COMBUSTIVEL", 195 | "idDespesa": "016c0dab-8167-4444-8e57-2491794298ac", 196 | "cnpj": "07679089000103", 197 | "fornecedor": "AUTO POSTO INDEPENDENCIA DO CAMBUCI LTDA.", 198 | "valor": 695.15, 199 | "createdDate": "2023-12-20T21:53:44Z", 200 | "modifiedDate": "2023-12-20T21:53:44Z" 201 | }, 202 | { 203 | "id": "0023e468-1484-4bb4-9934-84c7227d3ed4", 204 | "idVereador": "1394", 205 | "nomeVereador": "RUTE COSTA", 206 | "idCentroCusto": 394.0, 207 | "departamento": "GABINETE DE VEREADOR", 208 | "tipoDepartamento": 1, 209 | "ano": 2019, 210 | "mes": 8, 211 | "nomeDespesa": "COMBUSTIVEL", 212 | "idDespesa": "016c0dab-8167-4444-8e57-2491794298ac", 213 | "cnpj": "07679089000103", 214 | "fornecedor": "AUTO POSTO INDEPENDENCIA DO CAMBUCI LTDA.", 215 | "valor": 978.24, 216 | "createdDate": "2023-12-20T21:53:42Z", 217 | "modifiedDate": "2023-12-20T21:53:42Z" 218 | } 219 | ] 220 | ``` 221 | 222 |
223 | 224 | ## Sobre autenticação 225 | Para usar esta API, você precisa de um token de autenticação. Existem 2 tipos de tokens que você pode obter: um token público ou privado. 226 | 227 |
228 | Token público 229 | 230 | Este é um token que você pode gerar livremente sem qualquer informação pessoal. Ele é útil para fins de testes ou se você apenas quiser explorar a API. 231 | Você pode gerá-lo através deste endpoint: 232 | ``` 233 | POST /v1/auth/publico/criar 234 | ``` 235 | Tokens públicos só podem fazer até 10 solicitações por minuto e expiram após 7 dias. 236 | 237 |
238 | 239 |
240 | 241 |
242 | Token privado 243 | 244 | Este é um token que você pode gerar usando seu endereço de e-mail. É útil se você quiser usar a API em um ambiente de produção, 245 | ou se quiser usar a API com maior frequência. 246 | Você pode gerá-lo através deste endpoint: 247 | 248 | ``` 249 | POST /v1/auth/privado/criar 250 | 251 | Parameter: 252 | email: 253 | (use form URL Encoded) 254 | ``` 255 | 256 | O token será enviado para o seu endereço de e-mail. 257 | Tokens privados podem fazer até 100 solicitações por minuto e expiram após 1 ano. 258 | Cada e-mail pode ter somente um token privado. 259 |
260 | 261 | ## Tecnologias 262 | - Java 17 / Kotlin 263 | - Spring Boot 264 | - MySQL 265 | - Hibernate / Liquibase 266 | 267 | A API está hospedada no Google Cloud, usando o App Engine e o Cloud SQL. 268 | 269 | ## Contribuições 270 | Se você quiser contribuir para este projeto, sinta-se à vontade para abrir um pull request ou iniciar uma discussão via issues. 271 | 272 | ## Licença 273 | Este projeto está sob a [licença MIT](https://github.com/thiagobarbosa/quorum-api/tree/main?tab=MIT-1-ov-file). -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | --------------------------------------------------------------------------------