diff --git a/.docker/Dockerfile.api.development b/.docker/Dockerfile.api.development new file mode 100644 index 00000000..eedf2d91 --- /dev/null +++ b/.docker/Dockerfile.api.development @@ -0,0 +1,24 @@ +FROM composer:latest AS composer_stage + +FROM php:8.3-fpm + +# Instala dependencias +RUN apt-get update && apt-get install -y \ + git curl zip unzip libzip-dev libonig-dev libpq-dev \ + && docker-php-ext-install pdo pdo_pgsql zip \ + && pecl install redis \ + && docker-php-ext-enable redis + +# Copia o Composer do stage anterior +COPY --from=composer_stage /usr/bin/composer /usr/bin/composer + +# Copia o script de build +COPY .docker/build.sh /build.sh +RUN chmod +x /build.sh + +# Define o diretório de trabalho +WORKDIR /var/www + +EXPOSE 9000 + +CMD ["sh", "/build.sh"] diff --git a/.docker/build.sh b/.docker/build.sh new file mode 100644 index 00000000..093d3f84 --- /dev/null +++ b/.docker/build.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +APP_DIR="/var/www" + +# Aguarda volume montado +while [ ! -d "$APP_DIR" ]; do + echo "Aguardando montagem de volume..." + sleep 1 +done + +cd "$APP_DIR" || exit 1 + +# Instala dependencias do Laravel se vendor não existir +if [ ! -d "vendor" ]; then + echo "Executando composer install..." + composer install --no-interaction --prefer-dist --optimize-autoloader +fi + +# Gera cache se .env existir +if [ -f ".env" ]; then + php artisan config:cache + php artisan route:cache +fi + +# Inicia o PHP-FPM +exec php-fpm diff --git a/.docker/nginx.conf b/.docker/nginx.conf new file mode 100644 index 00000000..1bceafe2 --- /dev/null +++ b/.docker/nginx.conf @@ -0,0 +1,41 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + server { + listen 80; + server_name localhost; + + root /var/www/public; + index index.php index.html; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_pass app:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + } + + location ~ /\.ht { + deny all; + } + + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + } +} diff --git a/.docker/pg17.pg_hba.conf b/.docker/pg17.pg_hba.conf new file mode 100644 index 00000000..99103a4b --- /dev/null +++ b/.docker/pg17.pg_hba.conf @@ -0,0 +1,144 @@ +# PostgreSQL Client Authentication Configuration File +# =================================================== +# +# Refer to the "Client Authentication" section in the PostgreSQL +# documentation for a complete description of this file. A short +# synopsis follows. +# +# ---------------------- +# Authentication Records +# ---------------------- +# +# This file controls: which hosts are allowed to connect, how clients +# are authenticated, which PostgreSQL user names they can use, which +# databases they can access. Records take one of these forms: +# +# local DATABASE USER METHOD [OPTIONS] +# host DATABASE USER ADDRESS METHOD [OPTIONS] +# hostssl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS] +# +# (The uppercase items must be replaced by actual values.) +# +# The first field is the connection type: +# - "local" is a Unix-domain socket +# - "host" is a TCP/IP socket (encrypted or not) +# - "hostssl" is a TCP/IP socket that is SSL-encrypted +# - "hostnossl" is a TCP/IP socket that is not SSL-encrypted +# - "hostgssenc" is a TCP/IP socket that is GSSAPI-encrypted +# - "hostnogssenc" is a TCP/IP socket that is not GSSAPI-encrypted +# +# DATABASE can be "all", "sameuser", "samerole", "replication", a +# database name, a regular expression (if it starts with a slash (/)) +# or a comma-separated list thereof. The "all" keyword does not match +# "replication". Access to replication must be enabled in a separate +# record (see example below). +# +# USER can be "all", a user name, a group name prefixed with "+", a +# regular expression (if it starts with a slash (/)) or a comma-separated +# list thereof. In both the DATABASE and USER fields you can also write +# a file name prefixed with "@" to include names from a separate file. +# +# ADDRESS specifies the set of hosts the record matches. It can be a +# host name, or it is made up of an IP address and a CIDR mask that is +# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that +# specifies the number of significant bits in the mask. A host name +# that starts with a dot (.) matches a suffix of the actual host name. +# Alternatively, you can write an IP address and netmask in separate +# columns to specify the set of hosts. Instead of a CIDR-address, you +# can write "samehost" to match any of the server's own IP addresses, +# or "samenet" to match any address in any subnet that the server is +# directly connected to. +# +# METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", +# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". +# Note that "password" sends passwords in clear text; "md5" or +# "scram-sha-256" are preferred since they send encrypted passwords. +# +# OPTIONS are a set of options for the authentication in the format +# NAME=VALUE. The available options depend on the different +# authentication methods -- refer to the "Client Authentication" +# section in the documentation for a list of which options are +# available for which authentication methods. +# +# Database and user names containing spaces, commas, quotes and other +# special characters must be quoted. Quoting one of the keywords +# "all", "sameuser", "samerole" or "replication" makes the name lose +# its special character, and just match a database or username with +# that name. +# +# --------------- +# Include Records +# --------------- +# +# This file allows the inclusion of external files or directories holding +# more records, using the following keywords: +# +# include FILE +# include_if_exists FILE +# include_dir DIRECTORY +# +# FILE is the file name to include, and DIR is the directory name containing +# the file(s) to include. Any file in a directory will be loaded if suffixed +# with ".conf". The files of a directory are ordered by name. +# include_if_exists ignores missing files. FILE and DIRECTORY can be +# specified as a relative or an absolute path, and can be double-quoted if +# they contain spaces. +# +# ------------- +# Miscellaneous +# ------------- +# +# This file is read on server startup and when the server receives a +# SIGHUP signal. If you edit the file on a running system, you have to +# SIGHUP the server for the changes to take effect, run "pg_ctl reload", +# or execute "SELECT pg_reload_conf()". +# +# ---------------------------------- +# Put your actual configuration here +# ---------------------------------- +# +# If you want to allow non-local connections, you need to add more +# "host" records. In that case you will also need to make PostgreSQL +# listen on a non-local interface via the listen_addresses +# configuration parameter, or via the -i or -h command line switches. + +# CAUTION: Configuring the system for local "trust" authentication +# allows any local user to connect as any PostgreSQL user, including +# the database superuser. If you do not trust all your local users, +# use another authentication method. + + +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +host all all 127.0.0.1/32 trust +# IPv6 local connections: +host all all ::1/128 trust +# Allow replication connections from localhost, by a user with the +# replication privilege. +local replication all trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust + +# Replicacao +#host replication replicator 172.30.0.110/32 trust # replicacao +#host replication replicator 172.30.0.210/32 trust # BI + +# Acesso pela rede local + +host all all 192.168.0.0/24 md5 +host all all 172.16.0.0/12 md5 +host all all 172.30.0.0/24 md5 + +# Acesso a redes Docker (rede bridge padrão do Docker) +host all all 172.20.0.0/16 md5 + +# --------------------------- +# Acesso irrestrito +# --------------------------- +host all all 0.0.0.0/0 md5 diff --git a/.docker/redis.conf b/.docker/redis.conf new file mode 100644 index 00000000..85a523ec --- /dev/null +++ b/.docker/redis.conf @@ -0,0 +1,147 @@ +# redis.conf + +# ---------------------------------------------------------------------------- +# NETWORK +# ---------------------------------------------------------------------------- + +# By default, if no "bind" configuration directive is specified, +# Redis listens in all the network interfaces available on the server. +# In a container environment, binding to 0.0.0.0 is usually desired +# to allow access from other containers on the same network. +bind 0.0.0.0 + +# Protected mode is enabled by default. When protected mode is enabled, +# Redis only accepts connections from the loopback address (127.0.0.1). +# For production environments, you should disable protected mode and +# set a strong password using requirepass. +protected-mode no + +# Accept connections on the specified port. +port 6379 + +# ---------------------------------------------------------------------------- +# GENERAL +# ---------------------------------------------------------------------------- + +# Set the number of databases. The default database is DB 0. +databases 16 + +# Specify the server verbosity level. +# This can be one of: +# debug (a lot of information, useful for development/testing) +# verbose (many rarely useful info, but not as much as debug) +# notice (moderately verbose, good for production environments) +# warning (only very important / critical messages are logged) +loglevel notice + +# Specify the log file name. Also "stdout" can be used to force +# Redis to log on the standard output. +logfile stdout + +# ---------------------------------------------------------------------------- +# SNAPSHOTTING (RDB Persistence) +# ---------------------------------------------------------------------------- + +# Save the DB on disk: +# save +# Disable RDB persistence by commenting out all "save" lines. +# save 900 1 # Save if at least 1 key changed in 15 minutes +# save 300 10 # Save if at least 10 keys changed in 5 minutes +# save 60 10000 # Save if at least 10000 keys changed in 1 minute + +# By default Redis will refuse to write on disk if running out of disk space. +stop-writes-on-bgsave-error yes + +# Compress RDB files? +rdbcompression yes + +# Store checksum for RDB files? +rdbchecksum yes + +# The filename of the dump file. +dbfilename dump.rdb + +# ---------------------------------------------------------------------------- +# APPEND ONLY MODE (AOF Persistence) +# ---------------------------------------------------------------------------- + +# By default Redis asynchronously dumps the dataset on disk. This mode is +# good when your data is not critical and you can afford to lose a few +# seconds of data in case of a power outage, etc. +# +# Highly critical data sets may instead require a stronger guarantee that +# no data is lost. In order to obtain this in a usable way, Redis supports +# a fully durable mode called AOF (Append Only File). +# +# When AOF is enabled, every write operation received by the server is +# logged in the AOF file, using a format similar to the Redis protocol +# itself. +# +# It is strongly advised to use AOF + RDB persistence. In case of a crash, +# Redis will use the AOF file to recover the dataset, as it is usually +# the most complete one. +appendonly yes + +# The name of the append only file (default: "appendonly.aof") +appendfilename "appendonly.aof" + +# The fsync() system call tells the kernel to write data from the internal +# cache to disk. The three specified modes are: +# no: don't fsync, just put data in the kernel buffer. Faster. +# always: fsync every time new commands are appended to the AOF. Very slow. +# every: fsync every second. Compromise between speed and data safety. +appendfsync every + +# When the AOF file becomes too big, Redis is able to automatically +# rewrite it in the background. The following two options configure the +# automatic rewrite: +# auto-aof-rewrite-percentage 100 +# auto-aof-rewrite-min-size 64mb + +# ---------------------------------------------------------------------------- +# SECURITY +# ---------------------------------------------------------------------------- + +# Require a password to authenticate the clients. +# Please use a strong password. +# requirepass your_very_strong_password # <-- CHANGE THIS TO A REAL, STRONG PASSWORD! + +# ---------------------------------------------------------------------------- +# MEMORY MANAGEMENT +# ---------------------------------------------------------------------------- + +# Set a memory limit in bytes. When the memory limit is reached Redis +# will start removing keys according to the configured eviction policy. +# maxmemory # Example: maxmemory 100mb + +# Select the eviction policy to use when maxmemory is reached. +# Possible values are: +# volatile-lru -> Evict using approximated LRU among the keys with an expire set. +# allkeys-lru -> Evict using approximated LRU among all the keys. +# volatile-lfru -> Evict using approximated LFU among keys with an expire set. +# allkeys-lfru -> Evict using approximated LFU among all keys. +# volatile-random -> Remove random keys among keys with an expire set. +# allkeys-random -> Remove random keys among all keys. +# volatile-ttl -> Remove keys with the nearest expiration time (minor TTL) +# noeviction -> Don't evict anything, just return an error on write operations. +# maxmemory-policy noeviction + +# ---------------------------------------------------------------------------- +# LAZYFREEING +# ---------------------------------------------------------------------------- + +# Redis has two policies to free memory when needed: +# 1) Eager freeing: Immediately frees memory synchronously. +# 2) Lazy freeing: Defers memory freeing in a background thread. +# lazyfree-lazy-expire yes +# lazyfree-lazy-server-del yes +# lazyfree-lazy-eviction yes + +# ---------------------------------------------------------------------------- +# DOCKER +# ---------------------------------------------------------------------------- + +# In a Docker environment, setting 'supervised no' (the default) is typical. +# supervised no +# Or use systemd: supervised systemd +# Or use upstart: supervised upstart \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f4f8f126 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +./data/development \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..51154bac --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.blade.php] +indent_size = 4 + +[*.json] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1d83f325 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,24 @@ +# Force text and normalize line endings +* text=auto + +# Treat these as binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.zip binary +*.tar binary +*.gz binary +*.ttf binary +*.woff binary +*.woff2 binary + +# Blade templates +*.blade.php linguist-language=HTML + +# Treat PHP files as PHP +*.php linguist-language=PHP + +# Ignore migration changes in diffs +database/migrations/*.php -diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..740a344e --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Laravel +/vendor +/node_modules +/public/storage +/storage/*.key +.env +.env.*.local +.phpunit.result.cache +Homestead.json +Homestead.yaml +/.vagrant + +# Laravel IDE Helper +/_ide_helper.php +/_ide_helper_models.php + +# Cache e logs +/storage/framework/cache +/storage/framework/sessions +/storage/framework/views +/storage/logs + +# Docker +docker-compose.override.yml +.env.docker +data/ +*.pid + +# Composer +composer.lock + +# NPM / Yarn +package-lock.json +yarn.lock + +# IDEs +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# OS +.DS_Store +Thumbs.db + +# Test coverage +coverage/ + +#Composer +vendor/ \ No newline at end of file diff --git a/README.md b/README.md index ff000371..3b46248f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Teste para Desenvolvedor PHP/Laravel -Bem-vindo ao teste de desenvolvimento para a posição de Desenvolvedor PHP/Laravel. +Bem-vindo ao teste de desenvolvimento para a posição de Desenvolvedor PHP/Laravel. O objetivo deste teste é desenvolver uma API Rest para o cadastro de fornecedores, permitindo a busca por CNPJ ou CPF, utilizando Laravel no backend. @@ -40,7 +40,7 @@ O objetivo deste teste é desenvolver uma API Rest para o cadastro de fornecedor - Documentação do projeto, incluindo um README detalhado com instruções de instalação e operação. ## Bônus -- Implementação de Repository Pattern. +- Implementação de Strategie Pattern. - Implementação de testes automatizados. - Dockerização do ambiente de desenvolvimento. - Implementação de cache para otimizar o desempenho. @@ -51,4 +51,191 @@ O objetivo deste teste é desenvolver uma API Rest para o cadastro de fornecedor - Altere o arquivo README.md com as informações necessárias para executar o seu teste (comandos, migrations, seeds, etc); - Depois de finalizado, envie-nos o pull request; +## Teste Realizado +### Backend (API Laravel): + +Optei por utilizar o conceito de estabelecimentos, uma vez que temos clientes e fornecedores como um único cadastro. + +#### Design Pattern - Strategy +Foi utilizado o padrão de projeto **Strategy** para implementar a criação de estabelecimentos de forma desacoplada, com base no tipo de documento informado (`cpf` ou `cnpj`). + +- Cada tipo de documento possui sua própria classe de estratégia responsável pela validação e persistência. +- A estratégia correta é selecionada dinamicamente conforme o campo `tipo` enviado na requisição (`cpf` ou `cnpj`). +- Isso garante que cada regra de negócio fique isolada e facilmente extensível — por exemplo, para adicionar um novo tipo de documento como **passaporte**, basta criar uma nova estratégia. +- O padrão também reforça o princípio da responsabilidade única e melhora a testabilidade da lógica de criação. + +#### CRUD de Estabelecimentos: +- **Criar Estabelecimento:** + - Permite o cadastro de estabelecimentos usando CNPJ ou CPF, incluindo informações como nome, contato, e-mail e telefone. + - Validação rigorosa dos dados de entrada, incluindo formatos e tamanhos. + - A estratégia correta é automaticamente aplicada com base no tipo de documento informado (`cpf` ou `cnpj`). + +- **Editar Estabelecimento:** + - Atualiza informações do estabelecimento, mantendo as regras de validação. + +- **Excluir Estabelecimento:** + - Exclusão lógica com `softDeletes`. + +- **Listar Estabelecimentos:** + - Lista paginada de estabelecimentos. + +- **Buscar por CPF/CNPJ:** + - Endpoint específico para localizar estabelecimento por documento. + +#### Migrations: +- Utilização de migrations do Laravel para estruturação do banco de dados. +- Utilização de UUID como identificador principal na tabela de estabelecimentos. + +## Requisitos + +### Backend: +- Implementar busca por CNPJ na [BrasilAPI](https://brasilapi.com.br/docs#tag/CNPJ/paths/~1cnpj~1v1~1{cnpj}/get). + +## Tecnologias Utilizadas +- PHP 8.4 +- Laravel 12+ +- PostgreSQL +- Redis (Cache) +- Docker + +## Bônus +- Dockerização +- Cache para listagem +- Implementação de testes com PHPUnit + +## Entrega +Este repositório contém a implementação da API de Fornecedores(Estabelecimentos). + +## Configuração do Ambiente + +### Instalação + +1. Clone o repositório: + ```bash + git clone teste-dev-php + cd teste-dev-php + ``` + +2. Copie o `.env`: + ```bash + cp .env.development .env + ``` + +3. Gere a chave: + ```bash + php artisan key:generate + ``` + +4. Execute o build.sh para gerar os containers + ```bash + ./build.sh + ``` + +5. Execute as migrations: + ```bash + docker exec -it app php artisan migrate + ``` + +A API estará em `http://localhost:8000/api/`. + +### Ambiente Docker + +O Docker já está configurado para ser executado em modo desenvolvimento + +1. Configure o `.env` (`DB_HOST=postgres` para Docker). + +2. O projeto utiliza uma pasta `.docker/` contendo os arquivos de definição dos serviços necessários para o ambiente da aplicação (app, banco de dados etc). O Docker está configurado com **duas redes separadas**, garantindo uma maior segurança entre os serviços. + +Volumes são utilizados para persistência dos dados do banco e sincronização do código entre host e container. + +## Executando os Testes + +Docker: +```bash +docker exec -it app php artisan test +``` + +### Testes Realizados com PHPUnit +- Criar um estabelecimento com sucesso +- Impedir criação com dados inválidos (CNPJ duplicado) +- Atualizar um estabelecimento e validar mudança no campo `nome` +- Listar estabelecimentos com paginação +- Buscar estabelecimento por CPF/CNPJ +- Excluir estabelecimento (soft delete) e verificar que não afeta unicidade + +## Arquivo Postman + +O arquivo `postman.json` está incluído no repositório para facilitar os testes. Importe-o no Postman para acessar todos os endpoints disponíveis. + +## Endpoints da API + +### Estabelecimentos (`/api/v1/estabelecimentos`) + +- `GET /`: Lista os estabelecimentos paginados. +- `POST /`: Cria novo estabelecimento. +- `GET /{uuid}`: Consulta por UUID. +- `PUT /{uuid}`: Atualiza dados. +- `DELETE /{uuid}`: Exclui logicamente. +- `GET /by-cpf-cnpj/{documento}`: Busca por CPF ou CNPJ. + +### BrasilAPI + +- `GET /api/v1/external/brasilapi/cep/{cep}`: Consulta CEP via BrasilAPI. +- `GET /api/v1/external/brasilapi/cnpj/{cnpj}`: Consulta CNPJ via BrasilAPI. + +### ViaCEP + +- `GET /api/v1/external/viacep/cep/{cep}`: Consulta CEP via ViaCEP. + +### Sistema + +- `GET /api/sistema/config/cache`: Consulta configuração de cache. + +## Considerações + +- Soft delete implementado. Verificações de unicidade consideram registros ativos (com `deleted_at` NULL). +- Respostas padronizadas com classe `ApiResponse`, incluindo metadados (`status`, `msg`, `http`, `data`, `time`). +- Consulta externa de CNPJ via BrasilAPI integrada. + +## Modelo de Retorno + +```json +{ + "http": 200, + "status": true, + "msg": "Consulta de CEP realizada com sucesso", + "data": { + "cep": "81330140", + "state": "PR", + "city": "Curitiba", + "neighborhood": "Fazendinha", + "street": "Rua Henrique Mattioli", + "service": "open-cep", + "caches": true, + "cache": "api_cache:814d34c193c511d5aed9cd4fda7f4914" + }, + "time": 0.0996 +} +``` +cache : Indica que a api foi salva em cache por 30s + + +```json +{ + "http": 201, + "status": true, + "msg": "Estabelecimento criado com sucesso", + "data": { + "nome": "Novo Fornecedor SA", + "tipo": "cnpj", + "documento": "98765432000100", + "email": "contato@novofornecedor.com", + "telefone": "21988887777", + "uuid": "f5de4ec4-ae1b-4b54-8d7a-98e326dfab95", + "updated_at": "2025-06-08T23:25:27.000000Z", + "created_at": "2025-06-08T23:25:27.000000Z", + "id": 10 + }, + "time": 0.0434 +} \ No newline at end of file diff --git a/api/.env.development b/api/.env.development new file mode 100644 index 00000000..ecf46c01 --- /dev/null +++ b/api/.env.development @@ -0,0 +1,44 @@ +# App +APP_NAME=Laravel +APP_ENV=local +APP_KEY=base64:CLEwZ06m/gC4FC0T8+3oQXSJT/hnzHPzjgzem5P+fHA= +APP_DEBUG=true +APP_URL=http://localhost + +# Locale +APP_LOCALE=pt_BR +APP_FALLBACK_LOCALE=pt_BR +APP_FAKER_LOCALE=pt_BR + +# Logging +LOG_CHANNEL=stack +LOG_LEVEL=debug + +# Banco de Dados (PostgreSQL) +DB_CONNECTION=pgsql +DB_HOST=api_db +DB_PORT=5432 +DB_DATABASE=api_db +DB_USERNAME=api_db +DB_PASSWORD=api_db + +# Cache e sessão (Redis) +CACHE_DRIVER=redis +SESSION_DRIVER=redis +QUEUE_CONNECTION=redis + +REDIS_CLIENT=phpredis +REDIS_HOST=api_redis +REDIS_PORT=6379 +REDIS_PASSWORD=null + +# Email (log para desenvolvimento) +MAIL_MAILER=log +MAIL_FROM_ADDRESS=hello@example.com +MAIL_FROM_NAME="${APP_NAME}" + +# Outros +FILESYSTEM_DISK=local +BROADCAST_CONNECTION=log + +VITE_APP_NAME="${APP_NAME}" diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 00000000..35db1ddf --- /dev/null +++ b/api/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/api/app/Helpers/ApiResponse.php b/api/app/Helpers/ApiResponse.php new file mode 100644 index 00000000..795b9f9e --- /dev/null +++ b/api/app/Helpers/ApiResponse.php @@ -0,0 +1,94 @@ +startTime = microtime(true); + $this->msg = 'Chamada realizada'; + $this->data = []; + $this->http = 404; + $this->status = false; + } + + /** + * Sucesso na requisicao + * + * @param string $msg Mensagem descritiva + * @param mixed $data Dados a retornar + * @param int $http Código HTTP (default 200) + * @return $this + */ + public function success(string $msg = 'Operação realizada com sucesso', mixed $data = null, int $http = 200) + { + $this->status = true; + $this->msg = $msg; + $this->data = $data; + $this->http = $http; + + return $this->send(); + } + + /** + * Erro na requisicao + * + * @param string $msg Mensagem de erro + * @param mixed $data Dados adicionais de erro (opcional) + * @param int $http Código HTTP (default 400) + * @return $this + */ + public function error(string $msg = 'Erro na operação', mixed $data = null, int $http = 400) + { + + $this->status = false; + $this->msg = $msg; + $this->data = $data; + $this->http = $http; + + return $this->send(); + } + + /** + * Retorna a resposta JSON padronizada com os dados definidos. + * + * @return JsonResponse + */ + public function send(): JsonResponse + { + + return response()->json([ + 'http' => $this->http, + 'status' => $this->status, + 'msg' => $this->msg, + 'data' => $this->data, + 'time' => $this->getDuration() + ], $this->http); + } + + /** + * Calcula o tempo de execucao da resposta em Segundos + * + * @return float + */ + protected function getDuration(): float + { + return round((microtime(true) - $this->startTime), 4); + } +} diff --git a/api/app/Http/Controllers/Api/ApiExternalController.php b/api/app/Http/Controllers/Api/ApiExternalController.php new file mode 100644 index 00000000..ce11786d --- /dev/null +++ b/api/app/Http/Controllers/Api/ApiExternalController.php @@ -0,0 +1,97 @@ +apiResponse = new ApiResponse(); + } + + /** + * Realiza uma requisição HTTP GET genérica a um serviço externo. + * + * @param string $uri Caminho do recurso (ex: /api/cnpj/v1) + * @param string $params Parametros do request + * @param string $msg Mensagem de sucesso + * @return JsonResponse + */ + protected function request( + string $uri, + array $params = [] + ): array { + $url = $this->buildUrl($uri, $params); + $cacheKey = 'api_cache:' . md5($url); + + try { + + // Verifica se já existe no cache (TTL de 15 segundos) + if (Cache::has($cacheKey)) { + $cached = Cache::get($cacheKey); + $cached['cache'] = true; + + if (is_array($cached)) { + return $cached; + } + } + + $http = Http::send('GET', $url); + + if ($http->status() === 404) { + throw new \RuntimeException('Dado não encontrado na base de dados do serviço.', 404); + } + + if ($http->failed()) { + throw new \RuntimeException( + 'Erro ao consultar o serviço externo', $http->status() + ); + } + + $body = $http->body(); + $dados = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE || !is_array($dados)) { + throw new \RuntimeException('Resposta inválida do serviço externo (JSON malformado)', 502); + } + + // Armazena no Redis por 30 segundos + $dados['caches'] = Cache::put($cacheKey, $dados, now()->addSeconds(30)); + $dados['cache'] = $cacheKey; + + return $dados; + + } catch (\Throwable $e) { + throw $e; + } + } + + /** + * Constroi a URL para o request + * + * @param string $uri Caminho da URI com placeholders (ex: /ws/:cep/json) + * @param array $params Array associativo com os valores para substituir na URI + * (ex: ['cep' => '01001000']) + * @return string + */ + protected function buildUrl(string $uri, array $params = []): string + { + foreach ($params as $key => $value) { + $uri = str_replace(':' . $key, $value, $uri); + } + + return rtrim($this->url, '/') . '/' . ltrim($uri, '/'); + } + +} diff --git a/api/app/Http/Controllers/Api/Sistema/ApiSistemaController.php b/api/app/Http/Controllers/Api/Sistema/ApiSistemaController.php new file mode 100644 index 00000000..59b645d8 --- /dev/null +++ b/api/app/Http/Controllers/Api/Sistema/ApiSistemaController.php @@ -0,0 +1,34 @@ + Config::get('cache.default'), + 'prefix' => Config::get('cache.prefix'), + 'store' => Config::get('cache.stores.' . Config::get('cache.default')), + 'ttl_default' => ini_get('default_socket_timeout'), // TTL padrão de conexões + 'enabled' => Config::get('cache.default') !== 'null' + ]; + + return (new ApiResponse) + ->success('Configurações de cache carregadas com sucesso', $config) + ->send(); + + } catch (\Throwable $e) { + return (new ApiResponse) + ->error('Erro ao obter configurações de cache', ['exception' => $e->getMessage()], 500) + ->send(); + } + } +} diff --git a/api/app/Http/Controllers/Api/V1/ApiBrasilApiController.php b/api/app/Http/Controllers/Api/V1/ApiBrasilApiController.php new file mode 100644 index 00000000..990b397f --- /dev/null +++ b/api/app/Http/Controllers/Api/V1/ApiBrasilApiController.php @@ -0,0 +1,88 @@ +url = 'https://brasilapi.com.br'; + $this->uriCnpj = '/api/cnpj/v1/:cnpj'; + $this->uriCep = '/api/cep/v1/:cep'; + } + + /** + * Consulta o CNPJ + * + * @param string $cnpj : CNPJ + * @return JsonResponse + */ + public function consultarCnpj(string $cnpj): JsonResponse + { + try { + $cnpj = preg_replace('/\D/', '', $cnpj); + $cnpj = str_pad($cnpj, 14, '0', STR_PAD_LEFT); + + $validator = Validator::make(['cnpj' => $cnpj], ['cnpj' => ['required', new Cnpj]]); + + if ($validator->fails()) { + throw new \Exception('CNPJ inválido', 422); + } + + $dados = $this->request($this->uriCnpj, ['cnpj' => $cnpj]); + + return $this->apiResponse + ->success('Consulta de CNPJ realizada com sucesso', $dados, 200) + ->send(); + + } catch (\Throwable $e) { + return $this->apiResponse + ->error($e->getMessage(), [], $e->getCode() ?: 400) + ->send(); + } + } + + + /** + * Consulta o CEP + * + * @param string $cep : CEP + * @return JsonResponse + */ + public function consultarCep(string $cep): JsonResponse + { + try { + $cep = preg_replace('/\D/', '', $cep); + $cep = str_pad($cep, 8, '0', STR_PAD_LEFT); + + if (strlen($cep) > 8) { + throw new \Exception('CEP inválido', 422); + } + + $dados = $this->request($this->uriCep, ['cep' => $cep] ); + + return $this->apiResponse + ->success('Consulta de CEP realizada com sucesso', $dados, 200) + ->send(); + + } catch (\Throwable $e) { + + return $this->apiResponse + ->error($e->getMessage(), [], $e->getCode() ?: 400) + ->send(); + } + } + +} diff --git a/api/app/Http/Controllers/Api/V1/ApiViaCepController.php b/api/app/Http/Controllers/Api/V1/ApiViaCepController.php new file mode 100644 index 00000000..3d87fa40 --- /dev/null +++ b/api/app/Http/Controllers/Api/V1/ApiViaCepController.php @@ -0,0 +1,53 @@ +url = 'https://viacep.com.br/'; + $this->uriCep = '/ws/:cep/json'; + } + + /** + * Consulta o CEP + * + * @param string $cep : CEP + * @return JsonResponse + */ + public function consultarCep(string $cep): JsonResponse + { + try { + $cep = preg_replace('/\D/', '', $cep); + $cep = str_pad($cep, 8, '0', STR_PAD_LEFT); + + if (strlen($cep) > 8) { + throw new \Exception('CEP inválido', 422); + } + + $dados = $this->request($this->uriCep, ['cep'=>$cep]); + + return $this->apiResponse + ->success('Consulta de CEP realizada com sucesso', $dados, 200) + ->send(); + + } catch (\Throwable $e) { + return $this->apiResponse + ->error($e->getMessage(), [], $e->getCode()) + ->send(); + } + } +} diff --git a/api/app/Http/Controllers/Api/V1/EstabelecimentoController.php b/api/app/Http/Controllers/Api/V1/EstabelecimentoController.php new file mode 100644 index 00000000..e28a8e07 --- /dev/null +++ b/api/app/Http/Controllers/Api/V1/EstabelecimentoController.php @@ -0,0 +1,109 @@ +response = new ApiResponse(); + } + + public function index() + { + $data = Estabelecimento::paginate(10); + return $this->response->success('Lista de estabelecimentos carregada', $data); + } + + + public function store(Request $request) + { + $tipo = $request->input('tipo'); + + try { + $context = new EstabelecimentoContext($tipo); + $result = $context->handle($request); + + if ($result['status']) { + return $this->response->success('Estabelecimento criado com sucesso', $result['data'], 201); + } + + return $this->response->error('Erro de validação', $result['errors'], 422); + } catch (\InvalidArgumentException $e) { + return $this->response->error($e->getMessage(), null, 400); + } + } + + public function show($uuid) + { + $estabelecimento = Estabelecimento::where('uuid', $uuid)->first(); + + if (!$estabelecimento) { + return $this->response->error('Estabelecimento não encontrado', null, 404); + } + + return $this->response->success('Estabelecimento localizado', $estabelecimento); + } + + public function update(Request $request, $uuid) + { + $estabelecimento = Estabelecimento::where('uuid', $uuid)->first(); + + if (!$estabelecimento) { + return $this->response->error('Estabelecimento não encontrado', null, 404); + } + + $validator = Validator::make($request->all(), [ + 'tipo' => 'in:cpf,cnpj', + 'documento' => 'string|size:14|unique:estabelecimentos,documento,' . $estabelecimento->id . ',id,deleted_at,NULL', + 'nome' => 'string|max:100', + 'contato' => 'nullable|string|max:100', + 'email' => 'nullable|email|max:100', + 'telefone' => 'nullable|string|max:14', + ]); + + if ($validator->fails()) { + return $this->response->error('Erro de validação', $validator->errors(), 422); + } + + $estabelecimento->update($request->all()); + return $this->response->success('Estabelecimento atualizado com sucesso', $estabelecimento); + } + + public function destroy($uuid) + { + $estabelecimento = Estabelecimento::where('uuid', $uuid)->first(); + + if (!$estabelecimento) { + return $this->response->error('Estabelecimento não encontrado', null, 404); + } + + $estabelecimento->delete(); + return $this->response->success('Estabelecimento excluído com sucesso'); + } + + public function buscarPorDocumento(string $cpfCnpj) + { + $estabelecimento = Estabelecimento::where('documento', $cpfCnpj)->first(); + + if (!$estabelecimento) { + return $this->response + ->error('Estabelecimento não encontrado', null, 404) + ->send(); + } + + return $this->response + ->success('Estabelecimento localizado com sucesso', $estabelecimento) + ->send(); + } + +} diff --git a/api/app/Http/Controllers/Controller.php b/api/app/Http/Controllers/Controller.php new file mode 100644 index 00000000..8677cd5c --- /dev/null +++ b/api/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ + 'required|in:cpf,cnpj', + 'documento' => ['required', 'string', 'unique:estabelecimentos,documento'], + 'nome' => 'required|string|max:100', + 'contato' => 'nullable|string|max:100', + 'email' => 'nullable|email|max:100', + 'telefone' => 'nullable|string|max:14', + ]; + } + + public function withValidator($validator) + { + $validator->sometimes('documento', [new Cpf], function ($input) { + return $input->tipo === 'cpf'; + }); + + $validator->sometimes('documento', [new Cnpj], function ($input) { + return $input->tipo === 'cnpj'; + }); + } +} diff --git a/api/app/Http/Requests/UpdateEstabelecimentoRequest.php b/api/app/Http/Requests/UpdateEstabelecimentoRequest.php new file mode 100644 index 00000000..e69de29b diff --git a/api/app/Models/Estabelecimento.php b/api/app/Models/Estabelecimento.php new file mode 100644 index 00000000..5765f1ed --- /dev/null +++ b/api/app/Models/Estabelecimento.php @@ -0,0 +1,32 @@ +uuid = (string) Str::uuid(); + }); + } +} diff --git a/api/app/Models/User.php b/api/app/Models/User.php new file mode 100644 index 00000000..749c7b77 --- /dev/null +++ b/api/app/Models/User.php @@ -0,0 +1,48 @@ + */ + use HasFactory, Notifiable; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } +} diff --git a/api/app/Providers/AppServiceProvider.php b/api/app/Providers/AppServiceProvider.php new file mode 100644 index 00000000..452e6b65 --- /dev/null +++ b/api/app/Providers/AppServiceProvider.php @@ -0,0 +1,24 @@ +routes(function () { + // Rotas de API + Route::middleware('api') + ->prefix('api') + ->group(base_path('routes/api.php')); + + // Rotas web + Route::middleware('web') + ->group(base_path('routes/web.php')); + }); + } +} diff --git a/api/app/Rules/Cnpj.php b/api/app/Rules/Cnpj.php new file mode 100644 index 00000000..64d79cc2 --- /dev/null +++ b/api/app/Rules/Cnpj.php @@ -0,0 +1,45 @@ +strategy = match ($tipo) { + 'cpf' => new EstabelecimentoStrategyCpf(), + 'cnpj' => new EstabelecimentoStrategyCnpj(), + default => throw new \InvalidArgumentException('Tipo de documento inválido') + }; + } + + public function handle(Request $request): array + { + return $this->strategy->handle($request); + } +} diff --git a/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyCNPJ.php b/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyCNPJ.php new file mode 100644 index 00000000..04c70532 --- /dev/null +++ b/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyCNPJ.php @@ -0,0 +1,29 @@ +all(), [ + 'documento' => ['required', 'string', 'size:14', 'unique:estabelecimentos,documento', new Cnpj], + 'nome' => 'required|string|max:100', + 'email' => 'nullable|email|max:100', + 'telefone' => 'nullable|string|max:14', + ]); + + if ($validator->fails()) { + return ['status' => false, 'errors' => $validator->errors()]; + } + + $estabelecimento = Estabelecimento::create($request->all()); + + return ['status' => true, 'data' => $estabelecimento]; + } +} diff --git a/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyCPF.php b/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyCPF.php new file mode 100644 index 00000000..b969fd69 --- /dev/null +++ b/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyCPF.php @@ -0,0 +1,29 @@ +all(), [ + 'documento' => ['required', 'string', 'size:11', 'unique:estabelecimentos,documento', new Cpf], + 'nome' => 'required|string|max:100', + 'email' => 'nullable|email|max:100', + 'telefone' => 'nullable|string|max:14', + ]); + + if ($validator->fails()) { + return ['status' => false, 'errors' => $validator->errors()]; + } + + $estabelecimento = Estabelecimento::create($request->all()); + + return ['status' => true, 'data' => $estabelecimento]; + } +} diff --git a/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyInterface.php b/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyInterface.php new file mode 100644 index 00000000..0dcf1538 --- /dev/null +++ b/api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyInterface.php @@ -0,0 +1,10 @@ +handleCommand(new ArgvInput); + +exit($status); diff --git a/api/bootstrap/app.php b/api/bootstrap/app.php new file mode 100644 index 00000000..7b162dac --- /dev/null +++ b/api/bootstrap/app.php @@ -0,0 +1,18 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + // + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/api/bootstrap/cache/.gitignore b/api/bootstrap/cache/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/api/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/api/bootstrap/providers.php b/api/bootstrap/providers.php new file mode 100644 index 00000000..f4ecddf9 --- /dev/null +++ b/api/bootstrap/providers.php @@ -0,0 +1,6 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/api/config/auth.php b/api/config/auth.php new file mode 100644 index 00000000..7d1eb0de --- /dev/null +++ b/api/config/auth.php @@ -0,0 +1,115 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', App\Models\User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/api/config/cache.php b/api/config/cache.php new file mode 100644 index 00000000..4ebabf26 --- /dev/null +++ b/api/config/cache.php @@ -0,0 +1,108 @@ + env('CACHE_STORE', 'redis'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + +]; diff --git a/api/config/database.php b/api/config/database.php new file mode 100644 index 00000000..8910562d --- /dev/null +++ b/api/config/database.php @@ -0,0 +1,174 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/api/config/filesystems.php b/api/config/filesystems.php new file mode 100644 index 00000000..3d671bd9 --- /dev/null +++ b/api/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/api/config/logging.php b/api/config/logging.php new file mode 100644 index 00000000..1345f6f6 --- /dev/null +++ b/api/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/api/config/mail.php b/api/config/mail.php new file mode 100644 index 00000000..00345321 --- /dev/null +++ b/api/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/api/config/queue.php b/api/config/queue.php new file mode 100644 index 00000000..116bd8d0 --- /dev/null +++ b/api/config/queue.php @@ -0,0 +1,112 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/api/config/services.php b/api/config/services.php new file mode 100644 index 00000000..27a36175 --- /dev/null +++ b/api/config/services.php @@ -0,0 +1,38 @@ + [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/api/config/session.php b/api/config/session.php new file mode 100644 index 00000000..b5fa5319 --- /dev/null +++ b/api/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/api/database/.gitignore b/api/database/.gitignore new file mode 100644 index 00000000..9b19b93c --- /dev/null +++ b/api/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/api/database/factories/EstabelecimentoFactory.php b/api/database/factories/EstabelecimentoFactory.php new file mode 100644 index 00000000..67c7c51f --- /dev/null +++ b/api/database/factories/EstabelecimentoFactory.php @@ -0,0 +1,22 @@ + (string) Str::uuid(), + 'tipo' => $this->faker->randomElement(['cpf', 'cnpj']), + 'documento' => $this->faker->numerify('##############'), // 14 dígitos + 'nome' => $this->faker->company, + 'contato' => $this->faker->name, + 'email' => $this->faker->safeEmail, + 'telefone' => $this->faker->numerify('###########'), + ]; + } +} diff --git a/api/database/factories/UserFactory.php b/api/database/factories/UserFactory.php new file mode 100644 index 00000000..584104c9 --- /dev/null +++ b/api/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/api/database/migrations/0001_01_01_000000_create_users_table.php b/api/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 00000000..05fb5d9e --- /dev/null +++ b/api/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/api/database/migrations/0001_01_01_000001_create_cache_table.php b/api/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 00000000..b9c106be --- /dev/null +++ b/api/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/api/database/migrations/0001_01_01_000002_create_jobs_table.php b/api/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 00000000..425e7058 --- /dev/null +++ b/api/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/api/database/migrations/2025_06_05_000648_create_estabelecimentos_table.php b/api/database/migrations/2025_06_05_000648_create_estabelecimentos_table.php new file mode 100644 index 00000000..0a5a8d65 --- /dev/null +++ b/api/database/migrations/2025_06_05_000648_create_estabelecimentos_table.php @@ -0,0 +1,42 @@ +id(); + $table->uuid('uuid')->unique('idx_estabelecimentos_uuid'); + + $table->enum('tipo', ['cpf', 'cnpj']); + $table->string('documento', 14); + $table->string('nome', 100); + $table->string('contato', 100)->nullable(); + $table->string('email', 100)->nullable(); + $table->string('telefone', 14)->nullable(); + + $table->timestamps(); + $table->softDeletes(); + + //Indices + $table->index('nome', 'idx_estabelecimentos_documento'); + $table->index('nome', 'idx_estabelecimentos_nome'); + $table->index('telefone', 'idx_estabelecimentos_telefone'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('estabelecimentos'); + } +}; diff --git a/api/database/seeders/DatabaseSeeder.php b/api/database/seeders/DatabaseSeeder.php new file mode 100644 index 00000000..d01a0ef2 --- /dev/null +++ b/api/database/seeders/DatabaseSeeder.php @@ -0,0 +1,23 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 00000000..ef47e425 --- /dev/null +++ b/api/package.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.8.2", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^1.2.0", + "tailwindcss": "^4.0.0", + "vite": "^6.2.4" + } +} diff --git a/api/phpunit.xml b/api/phpunit.xml new file mode 100644 index 00000000..61c031c4 --- /dev/null +++ b/api/phpunit.xml @@ -0,0 +1,33 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + diff --git a/api/public/.htaccess b/api/public/.htaccess new file mode 100644 index 00000000..b574a597 --- /dev/null +++ b/api/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/api/public/favicon.ico b/api/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/api/public/index.php b/api/public/index.php new file mode 100644 index 00000000..ee8f07e9 --- /dev/null +++ b/api/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/api/public/robots.txt b/api/public/robots.txt new file mode 100644 index 00000000..eb053628 --- /dev/null +++ b/api/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/api/resources/css/app.css b/api/resources/css/app.css new file mode 100644 index 00000000..3e6abeab --- /dev/null +++ b/api/resources/css/app.css @@ -0,0 +1,11 @@ +@import 'tailwindcss'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../**/*.blade.php'; +@source '../**/*.js'; + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; +} diff --git a/api/resources/js/app.js b/api/resources/js/app.js new file mode 100644 index 00000000..e59d6a0a --- /dev/null +++ b/api/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/api/resources/js/bootstrap.js b/api/resources/js/bootstrap.js new file mode 100644 index 00000000..5f1390b0 --- /dev/null +++ b/api/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/api/resources/views/welcome.blade.php b/api/resources/views/welcome.blade.php new file mode 100644 index 00000000..c893b809 --- /dev/null +++ b/api/resources/views/welcome.blade.php @@ -0,0 +1,277 @@ + + + + + + + Laravel + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + +
+ @if (Route::has('login')) + + @endif +
+
+
+
+

Let's get started

+

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

+ + +
+
+ {{-- Laravel Logo --}} + + + + + + + + + + + {{-- Light Mode 12 SVG --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{-- Dark Mode 12 SVG --}} + +
+
+
+
+ + @if (Route::has('login')) + + @endif + + diff --git a/api/routes/api.php b/api/routes/api.php new file mode 100644 index 00000000..597d00b2 --- /dev/null +++ b/api/routes/api.php @@ -0,0 +1,20 @@ +group(function () { + Route::get('external/brasilapi/cep/{cep}' , [ApiBrasilApiController::class, 'consultarCep']); + Route::get('external/brasilapi/cnpj/{cnpj}', [ApiBrasilApiController::class, 'consultarCnpj']); + Route::get('external/viacep/cep/{cep}' , [ApiViaCepController::class, 'consultarCep']); + Route::get('estabelecimentos/by-cpf-cnpj/{cpfCnpj}', [EstabelecimentoController::class, 'buscarPorDocumento']); + Route::apiResource('estabelecimentos', EstabelecimentoController::class); +}); + +Route::prefix('sistema')->group(function () { + Route::get('/config/cache', [ApiSistemaController::class, 'cacheConfig']); +}); + diff --git a/api/routes/console.php b/api/routes/console.php new file mode 100644 index 00000000..3c9adf1a --- /dev/null +++ b/api/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/api/routes/web.php b/api/routes/web.php new file mode 100644 index 00000000..86a06c53 --- /dev/null +++ b/api/routes/web.php @@ -0,0 +1,7 @@ + 'cnpj', + 'documento' => '12345678901230', + 'nome' => 'Empresa Teste Ltda', + 'contato' => 'Joao da Silva', + 'email' => 'joao@empresa.com', + 'telefone' => '11999998888', + ]; + + $response = $this->postJson('/api/v1/estabelecimentos', $payload); + + $response->assertStatus(201) + ->assertJsonFragment(['nome' => 'Empresa Teste Ltda']); + + $this->assertDatabaseHas('estabelecimentos', [ + 'documento' => '12345678901230', + ]); + } + + public function test_criar_estabelecimento_duplicado() + { + Estabelecimento::factory()->create([ + 'documento' => '12345678901230', + ]); + + $payload = [ + 'tipo' => 'cnpj', + 'documento' => '12345678901230', + 'nome' => 'Empresa Teste 2', + ]; + + $response = $this->postJson('/api/v1/estabelecimentos', $payload); + + $response->assertStatus(422) + ->assertJsonFragment(['msg' => 'Erro de validação']); + } + + public function test_criar_estabelecimento_com_cpf_valido() + { + $cpf = $this->gerarCpfValido(); + + $payload = [ + 'tipo' => 'cpf', + 'documento' => $cpf, + 'nome' => 'Joana Teste', + 'contato' => 'Joana', + 'email' => 'joana@testecpf.com', + 'telefone' => '11988887777', + ]; + + $response = $this->postJson('/api/v1/estabelecimentos', $payload); + + $response->assertStatus(201) + ->assertJsonFragment(['nome' => 'Joana Teste']); + + $this->assertDatabaseHas('estabelecimentos', [ + 'documento' => $cpf, + ]); + } + + public function test_criar_estabelecimento_com_cnpj_valido() + { + $cnpj = $this->gerarCnpjValido(); + + $payload = [ + 'tipo' => 'cnpj', + 'documento' => $cnpj, + 'nome' => 'Empresa CNPJ Válido', + 'contato' => 'Carlos', + 'email' => 'cnpjvalido@empresa.com', + 'telefone' => '11977776666', + ]; + + $response = $this->postJson('/api/v1/estabelecimentos', $payload); + + $response->assertStatus(201) + ->assertJsonFragment(['nome' => 'Empresa CNPJ Válido']); + + $this->assertDatabaseHas('estabelecimentos', [ + 'documento' => $cnpj, + ]); + } + + public function test_nao_criar_estabelecimento_com_cpf_invalido() + { + $cpfInvalido = '12345678900'; + + $payload = [ + 'tipo' => 'cpf', + 'documento' => $cpfInvalido, + 'nome' => 'Teste CPF Inválido', + 'contato' => 'Pessoa Inválida', + 'email' => 'invalido@cpf.com', + 'telefone' => '11988887777', + ]; + + $response = $this->postJson('/api/v1/estabelecimentos', $payload); + + $response->assertStatus(422) + ->assertJsonFragment(['msg' => 'Erro de validação']); + } + + public function test_nao_criar_estabelecimento_com_cnpj_invalido() + { + $cnpjInvalido = '12345678901231'; + + $payload = [ + 'tipo' => 'cnpj', + 'documento' => $cnpjInvalido, + 'nome' => 'Empresa CNPJ Inválido', + 'contato' => 'Carlos Inválido', + 'email' => 'invalido@empresa.com', + 'telefone' => '11977776666', + ]; + + $response = $this->postJson('/api/v1/estabelecimentos', $payload); + + $response->assertStatus(422) + ->assertJsonFragment(['msg' => 'Erro de validação']); + } + + public function test_buscar_estabelecimento_por_uuid() + { + $estabelecimento = Estabelecimento::factory()->create(); + + $response = $this->getJson("/api/v1/estabelecimentos/{$estabelecimento->uuid}"); + + $response->assertStatus(200) + ->assertJsonFragment(['uuid' => $estabelecimento->uuid]); + } + + public function test_atualizar_estabelecimento() + { + $estabelecimento = Estabelecimento::factory()->create(); + + $response = $this->putJson("/api/v1/estabelecimentos/{$estabelecimento->uuid}", [ + 'nome' => 'Nome Atualizado' + ]); + + $response->assertStatus(200) + ->assertJsonFragment(['nome' => 'Nome Atualizado']); + } + + public function test_atualizar_estabelecimento_nome() + { + $estabelecimento = Estabelecimento::factory()->create([ + 'nome' => 'Nome Original' + ]); + + $this->putJson("/api/v1/estabelecimentos/{$estabelecimento->uuid}", [ + 'nome' => 'Nome Atualizado' + ])->assertStatus(200); + + $this->assertDatabaseHas('estabelecimentos', [ + 'uuid' => $estabelecimento->uuid, + 'nome' => 'Nome Atualizado', + ]); + + $this->assertDatabaseMissing('estabelecimentos', [ + 'uuid' => $estabelecimento->uuid, + 'nome' => 'Nome Original', + ]); + } + + + public function test_excluir_estabelecimento() + { + $estabelecimento = Estabelecimento::factory()->create(); + + $response = $this->deleteJson("/api/v1/estabelecimentos/{$estabelecimento->uuid}"); + + $response->assertStatus(200); + $this->assertSoftDeleted('estabelecimentos', [ + 'uuid' => $estabelecimento->uuid + ]); + } + + public function test_listar_estabelecimentos() + { + Estabelecimento::factory()->count(3)->create(); + + $response = $this->getJson('/api/v1/estabelecimentos'); + + $response->assertStatus(200) + ->assertJsonStructure(['data', 'http', 'status', 'msg']); + } + + /** + * Funcoes auxiliares + */ + private function gerarCpfValido(): string + { + $n = []; + for ($i = 0; $i < 9; $i++) { + $n[$i] = mt_rand(0, 9); + } + + // Digito 1 + $n[9] = ((10 * $n[0]) + (9 * $n[1]) + (8 * $n[2]) + (7 * $n[3]) + (6 * $n[4]) + + (5 * $n[5]) + (4 * $n[6]) + (3 * $n[7]) + (2 * $n[8])) % 11; + $n[9] = ($n[9] < 2) ? 0 : 11 - $n[9]; + + // Digito 2 + $n[10] = ((11 * $n[0]) + (10 * $n[1]) + (9 * $n[2]) + (8 * $n[3]) + (7 * $n[4]) + + (6 * $n[5]) + (5 * $n[6]) + (4 * $n[7]) + (3 * $n[8]) + (2 * $n[9])) % 11; + $n[10] = ($n[10] < 2) ? 0 : 11 - $n[10]; + + return implode('', $n); + } + + private function gerarCnpjValido(): string + { + $n = []; + for ($i = 0; $i < 12; $i++) { + $n[$i] = mt_rand(0, 9); + } + + $peso1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + $peso2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + + $soma1 = 0; + for ($i = 0; $i < 12; $i++) { + $soma1 += $n[$i] * $peso1[$i]; + } + $n[12] = ($soma1 % 11 < 2) ? 0 : 11 - ($soma1 % 11); + + $soma2 = 0; + for ($i = 0; $i < 13; $i++) { + $soma2 += $n[$i] * $peso2[$i]; + } + $n[13] = ($soma2 % 11 < 2) ? 0 : 11 - ($soma2 % 11); + + return implode('', $n); + } + + +} diff --git a/api/tests/TestCase.php b/api/tests/TestCase.php new file mode 100644 index 00000000..fe1ffc2f --- /dev/null +++ b/api/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/api/vite.config.js b/api/vite.config.js new file mode 100644 index 00000000..29fbfe9a --- /dev/null +++ b/api/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + tailwindcss(), + ], +}); diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..8a5be7dc --- /dev/null +++ b/build.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +ENV_TYPE=${ENV_TYPE:-development} + +echo "Ambiente: $ENV_TYPE" + +# Lista de diretórios de volume necessários +DIRS=( + "./data/${ENV_TYPE}/pg" + "./data/${ENV_TYPE}/logs/redis" + "./data/${ENV_TYPE}/logs/postgres" + "./data/${ENV_TYPE}/logs/php" + "./data/${ENV_TYPE}/logs/nginx" + "./data/${ENV_TYPE}/logs/laravel" + "./data/${ENV_TYPE}/views/laravel" +) + +echo "Criando diretórios necessários para volumes..." +for dir in "${DIRS[@]}"; do + if [ ! -d "$dir" ]; then + mkdir -p "$dir" + echo "✔️ Criado: $dir" + else + echo "↪️ Já existe: $dir" + fi +done + +echo "Evitando problemas em tempo de execucao..." +docker-compose stop +if [ -n "$(docker ps -aq)" ]; then + echo "Removendo containers parados..." + docker rm $(docker ps -aq) +fi + +echo "Buildando containers..." +docker-compose build + +echo "Subindo ambiente..." +docker-compose up + + diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..ca00dff7 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,137 @@ +version: '3.9' + +networks: + laravel-net: + driver: bridge + laravel-net-db: + driver: bridge + +services: + + api_redis: + container_name: api_redis + image: redis:alpine + restart: always + user: "1000:1000" + environment: + TZ: ${TZ} + volumes: + - ./.docker/redis.conf:/usr/local/etc/redis/redis.conf:ro + - vol_api_redis:/data + networks: + - laravel-net-db + command: ["redis-server", "--stop-writes-on-bgsave-error", "no"] + + + api_db: + container_name: api_db + image: postgres:17 + restart: unless-stopped + shm_size: 1GB # 1GB shared_buffers ⇒ /dev/shm 256 MB + ports: + - "5432:5432" + environment: + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE} + TZ: ${TZ} + PGTZ: ${TZ} + command: > + -c wal_level=replica + -c max_wal_senders=10 + -c wal_keep_size=512MB + -c shared_buffers=512MB + -c hot_standby=on + -c hba_file=/etc/postgresql/pg_hba.conf + volumes: + - ./.docker/pg17.pg_hba.conf:/etc/postgresql/pg_hba.conf:ro + - vol_api_pgdata:/var/lib/postgresql/data + - vol_api_pglog:/var/log/postgresql + networks: + - laravel-net-db + + app: + container_name: app + build: + context: . + dockerfile: ./.docker/Dockerfile.api.${ENV_TYPE} + volumes: + - .docker/build.sh:/var/www/build.sh + - ./api:/var/www + - vol_api_php-logs:/var/log/php + - vol_api_laravel-logs:/var/www/storage/logs + - vol_api_laravel-views:/var/www/storage/views + user: "1000:1000" + depends_on: + - api_db + - api_redis + networks: + - laravel-net + - laravel-net-db + environment: + - DB_HOST=api_db + - DB_PORT=5432 + - DB_DATABASE=${DB_DATABASE} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + expose: + - "9000" + command: sh /build.sh + + nginx: + container_name: nginx + image: nginx:alpine + ports: + - "8080:80" + volumes: + - ./api:/var/www + - ./.docker/nginx.conf:/etc/nginx/nginx.conf:ro + - vol_api_nginx-logs:/var/log/nginx + depends_on: + - app + networks: + - laravel-net + +volumes: + vol_api_redis: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/logs/redis + o: bind + vol_api_pgdata: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/pg + o: bind + vol_api_pglog: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/logs/postgres + o: bind + vol_api_php-logs: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/logs/laravel + o: bind + vol_api_laravel-logs: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/logs/php + o: bind + vol_api_laravel-views: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/views/laravel + o: bind + vol_api_nginx-logs: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/logs/nginx + o: bind diff --git a/postman.json b/postman.json new file mode 100644 index 00000000..cef1750c --- /dev/null +++ b/postman.json @@ -0,0 +1,426 @@ +{ + "info": { + "_postman_id": "88a88892-30a7-4a42-929b-6af88443f9ea", + "name": "Fornecedor API - teste", + "description": "Endpoints da API para gerenciar Fornecedores.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "14182462" + }, + "item": [ + { + "name": "Fornecedores", + "item": [ + { + "name": "Listar Estabelecimentos", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/v1/estabelecimentos?sortBy=created_at&sortDir=desc&perPage=15", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "estabelecimentos" + ], + "query": [ + { + "key": "sortBy", + "value": "created_at", + "description": "Campo para ordenação (padrão: created_at)" + }, + { + "key": "sortDir", + "value": "desc", + "description": "Direção da ordenação: 'asc' ou 'desc' (padrão: desc)" + }, + { + "key": "perPage", + "value": "15", + "description": "Número de itens por página (padrão: 15)" + }, + { + "key": "filter[nome]", + "value": "Nome Exemplo", + "description": "Exemplo de filtro: filter[nome]=Valor. Adicione mais conforme necessário.", + "disabled": true + }, + { + "key": "filter[cpf_cnpj]", + "value": "12345678000199", + "description": "Exemplo de filtro: filter[cpf_cnpj]=Valor.", + "disabled": true + } + ] + }, + "description": "Recupera uma lista paginada de fornecedores. Suporta filtros, ordenação e paginação.\n\nMétodo da Interface: `getAll(array $filters = [], string $sortBy = 'created_at', string $sortDir = 'desc', int $perPage = 15)`" + }, + "response": [] + }, + { + "name": "Busca por ID", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/v1/estabelecimentos/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "estabelecimentos", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "1", + "description": "O ID do Fornecedor" + } + ] + }, + "description": "Recupera um fornecedor específico pelo seu ID.\n\nMétodo da Interface: `findById(int $id)`" + }, + "response": [] + }, + { + "name": "Busca por Documento", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/v1/estabelecimentos/by-cpf-cnpj/:cpfCnpj", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "estabelecimentos", + "by-cpf-cnpj", + ":cpfCnpj" + ], + "variable": [ + { + "key": "cpfCnpj", + "value": "12345678000199", + "description": "O CPF ou CNPJ do Fornecedor" + } + ] + }, + "description": "Recupera um fornecedor específico pelo seu CPF/CNPJ.\n\nMétodo da Interface: `findByCpfCnpj(string $cpfCnpj)`" + }, + "response": [] + }, + { + "name": "Criar Estabelecimento", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"nome\": \"Novo Fornecedor SA\",\n \"tipo\": \"cnpj\",\n \"documento\": \"98765432000100\",\n \"email\": \"contato@novofornecedor.com\",\n \"telefone\": \"21988887777\",\n \"endereco\": {\n \"logradouro\": \"Rua Exemplo, 123\",\n \"cidade\": \"Cidade Exemplo\",\n \"estado\": \"EX\",\n \"cep\": \"00000-000\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/v1/estabelecimentos", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "estabelecimentos" + ] + }, + "description": "Cria um novo fornecedor.\n\nMétodo da Interface: `create(array $data)`" + }, + "response": [] + }, + { + "name": "Atualizar Estabelecimentos", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"nome\": \"Fornecedor Atualizado Ltda\",\n \"email\": \"novo_email@fornecedor.com\",\n \"telefone\": \"(11) 97777-6666\",\n \"ativo\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/v1/estabelecimentos/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "estabelecimentos", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "3e8b90c2-04e1-4895-8df5-530776e63a53", + "description": "O ID do Fornecedor a ser atualizado" + } + ] + }, + "description": "Atualiza um fornecedor existente pelo seu ID.\n\nMétodo da Interface: `update(int $id, array $data)`" + }, + "response": [] + }, + { + "name": "Deletar Fornecedor", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/v1/estabelecimentos/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "estabelecimentos", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "3e8b90c2-04e1-4895-8df5-530776e63a53", + "description": "O ID do Fornecedor a ser deletado" + } + ] + }, + "description": "Deleta um fornecedor pelo seu ID.\n\nMétodo da Interface: `delete(int $id)`" + }, + "response": [] + } + ], + "description": "Operações relacionadas a Fornecedores" + }, + { + "name": "External", + "item": [ + { + "name": "CEP - Brasil API", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{baseUrl}}/api/v1/external/brasilapi/cep/:cep", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "external", + "brasilapi", + "cep", + ":cep" + ], + "variable": [ + { + "key": "cep", + "value": "81330140" + } + ] + } + }, + "response": [] + }, + { + "name": "CNPJ - Brasil API", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{baseUrl}}/api/v1/external/brasilapi/cnpj/:cnpj", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "external", + "brasilapi", + "cnpj", + ":cnpj" + ], + "variable": [ + { + "key": "cnpj", + "value": "19676475000120" + } + ] + } + }, + "response": [] + }, + { + "name": "CEP - ViaCEP", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{baseUrl}}/api/v1/external/viacep/cep/:cep", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "external", + "viacep", + "cep", + ":cep" + ], + "variable": [ + { + "key": "cep", + "value": "81330140" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Sistema", + "item": [ + { + "name": "SIstema - Cache", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{baseUrl}}/api/sistema/config/cache", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "sistema", + "config", + "cache" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8000", + "type": "string" + } + ] +} \ No newline at end of file