From 0608d29b0e8411fbb8a84c0b946cdb86941c566a Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Wed, 4 Jun 2025 22:01:28 -0300 Subject: [PATCH 01/21] feat (System) : Projeto --- api/.env.example | 65 +++++ api/app/Http/Controllers/Controller.php | 8 + api/app/Models/User.php | 48 ++++ api/app/Providers/AppServiceProvider.php | 24 ++ api/artisan | 18 ++ api/bootstrap/app.php | 18 ++ api/bootstrap/cache/.gitignore | 2 + api/bootstrap/providers.php | 5 + api/composer.json | 75 ++++++ api/config/app.php | 126 +++++++++ api/config/auth.php | 115 ++++++++ api/config/cache.php | 108 ++++++++ api/config/database.php | 174 ++++++++++++ api/config/filesystems.php | 80 ++++++ api/config/logging.php | 132 ++++++++++ api/config/mail.php | 118 +++++++++ api/config/queue.php | 112 ++++++++ api/config/services.php | 38 +++ api/config/session.php | 217 +++++++++++++++ api/database/.gitignore | 1 + api/database/factories/UserFactory.php | 44 ++++ api/database/seeders/DatabaseSeeder.php | 23 ++ api/package.json | 17 ++ api/phpunit.xml | 33 +++ api/public/.htaccess | 25 ++ api/public/favicon.ico | 0 api/public/index.php | 20 ++ api/public/robots.txt | 2 + api/resources/css/app.css | 11 + api/resources/js/app.js | 1 + api/resources/js/bootstrap.js | 4 + api/resources/views/welcome.blade.php | 277 ++++++++++++++++++++ api/routes/console.php | 8 + api/routes/web.php | 7 + api/storage/app/.gitignore | 4 + api/storage/app/private/.gitignore | 2 + api/storage/app/public/.gitignore | 2 + api/storage/framework/.gitignore | 9 + api/storage/framework/cache/.gitignore | 3 + api/storage/framework/cache/data/.gitignore | 2 + api/storage/framework/sessions/.gitignore | 2 + api/storage/framework/testing/.gitignore | 2 + api/storage/framework/views/.gitignore | 2 + api/storage/logs/.gitignore | 2 + api/tests/Feature/ExampleTest.php | 19 ++ api/tests/TestCase.php | 10 + api/tests/Unit/ExampleTest.php | 16 ++ api/vite.config.js | 13 + 48 files changed, 2044 insertions(+) create mode 100644 api/.env.example create mode 100644 api/app/Http/Controllers/Controller.php create mode 100644 api/app/Models/User.php create mode 100644 api/app/Providers/AppServiceProvider.php create mode 100755 api/artisan create mode 100644 api/bootstrap/app.php create mode 100644 api/bootstrap/cache/.gitignore create mode 100644 api/bootstrap/providers.php create mode 100644 api/composer.json create mode 100644 api/config/app.php create mode 100644 api/config/auth.php create mode 100644 api/config/cache.php create mode 100644 api/config/database.php create mode 100644 api/config/filesystems.php create mode 100644 api/config/logging.php create mode 100644 api/config/mail.php create mode 100644 api/config/queue.php create mode 100644 api/config/services.php create mode 100644 api/config/session.php create mode 100644 api/database/.gitignore create mode 100644 api/database/factories/UserFactory.php create mode 100644 api/database/seeders/DatabaseSeeder.php create mode 100644 api/package.json create mode 100644 api/phpunit.xml create mode 100644 api/public/.htaccess create mode 100644 api/public/favicon.ico create mode 100644 api/public/index.php create mode 100644 api/public/robots.txt create mode 100644 api/resources/css/app.css create mode 100644 api/resources/js/app.js create mode 100644 api/resources/js/bootstrap.js create mode 100644 api/resources/views/welcome.blade.php create mode 100644 api/routes/console.php create mode 100644 api/routes/web.php create mode 100644 api/storage/app/.gitignore create mode 100644 api/storage/app/private/.gitignore create mode 100644 api/storage/app/public/.gitignore create mode 100644 api/storage/framework/.gitignore create mode 100644 api/storage/framework/cache/.gitignore create mode 100644 api/storage/framework/cache/data/.gitignore create mode 100644 api/storage/framework/sessions/.gitignore create mode 100644 api/storage/framework/testing/.gitignore create mode 100644 api/storage/framework/views/.gitignore create mode 100644 api/storage/logs/.gitignore create mode 100644 api/tests/Feature/ExampleTest.php create mode 100644 api/tests/TestCase.php create mode 100644 api/tests/Unit/ExampleTest.php create mode 100644 api/vite.config.js 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/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 @@ + */ + 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 @@ +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..38b258d1 --- /dev/null +++ b/api/bootstrap/providers.php @@ -0,0 +1,5 @@ + 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..925f7d2e --- /dev/null +++ b/api/config/cache.php @@ -0,0 +1,108 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | 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/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/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/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 @@ +get('/'); + + $response->assertStatus(200); + } +} 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(), + ], +}); From c1bb196dd226aafd65bb347c7a3cc6c9240c9191 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Wed, 4 Jun 2025 22:16:08 -0300 Subject: [PATCH 02/21] chore (config) : Arquivos de configuracao do projeto --- .editorconfig | 18 ++++++++++++++++++ .gitattributes | 24 ++++++++++++++++++++++++ .gitignore | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore 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 From 85b05a00793351e8e4825dd356185e7685f32c86 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Thu, 5 Jun 2025 22:04:14 -0300 Subject: [PATCH 03/21] feat (Docker) : Docker --- .docker/build.sh | 22 +++++++++ build.sh | 34 ++++++++++++++ docker-compose.yaml | 106 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 .docker/build.sh create mode 100755 build.sh create mode 100644 docker-compose.yaml diff --git a/.docker/build.sh b/.docker/build.sh new file mode 100644 index 00000000..a62450f6 --- /dev/null +++ b/.docker/build.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +APP_DIR="/var/www" + +cd "$APP_DIR" || exit 1 + +# Espera até o volume estar montado +while [ ! -d "$APP_DIR" ]; do + echo "Aguardando montagem de volume..." + sleep 1 +done + +# Verifica se o diretório vendor existe +if [ -d "vendor" ]; then + echo "Diretório 'vendor' já existe. Pulando composer install." +else + echo "Diretório 'vendor' não encontrado. Executando composer install..." + composer install --no-interaction --prefer-dist --optimize-autoloader +fi + +# Mantém o container em execução +exec php-fpm diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..67589262 --- /dev/null +++ b/build.sh @@ -0,0 +1,34 @@ +#!/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" +) + +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 "Buildando containers..." +docker-compose build + +echo "Subindo ambiente..." +docker-compose up -d + +echo "Ambiente pronto!" diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..1251abd0 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,106 @@ +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 + volumes: + - vol_api_redis:/data + networks: + - laravel-net-db + + api_db: + container_name: api_db + image: postgres:17 + restart: always + environment: + POSTGRES_DB: laravel + POSTGRES_USER: laravel + POSTGRES_PASSWORD: secret + user: 999:999 + ports: + - "5432:5432" + volumes: + - 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_laravel-php-logs:/var/log/php + depends_on: + - api_db + - api_redis + networks: + - laravel-net + - laravel-net-db + environment: + - DB_HOST=postgres + - DB_PORT=5432 + - DB_DATABASE=laravel + - DB_USERNAME=laravel + - DB_PASSWORD=secret + expose: + - "9000" + command: sh /build.sh + + nginx: + container_name: nginx + image: nginx:alpine + ports: + - "8080:80" + volumes: + - .:/var/www + - ./.docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - vol_api_laravel-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_laravel-php-logs: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/logs/php + o: bind + vol_api_laravel-nginx-logs: + driver: local + driver_opts: + type: none + device: ./data/${ENV_TYPE}/logs/nginx + o: bind From b581a826ababbb1affb62b439391a328f9fb782f Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Thu, 5 Jun 2025 22:04:37 -0300 Subject: [PATCH 04/21] feat (Docker) : Docker --- .docker/Dockerfile.api.development | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .docker/Dockerfile.api.development diff --git a/.docker/Dockerfile.api.development b/.docker/Dockerfile.api.development new file mode 100644 index 00000000..471ddb01 --- /dev/null +++ b/.docker/Dockerfile.api.development @@ -0,0 +1,22 @@ +FROM composer:latest AS composer_stage + +FROM php:8.3-fpm + +# Instala dependências de sistema +RUN apt-get update && apt-get install -y \ + git curl libpq-dev zip unzip libzip-dev libonig-dev \ + && docker-php-ext-install pdo pdo_pgsql + +# 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"] From 80da8fcb93b1b355deaf41fc55207ef911f152a6 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Thu, 5 Jun 2025 22:06:44 -0300 Subject: [PATCH 05/21] feat (DB:Estabelecimentos) : Tabela Estabelecimentos --- ...5_000648_create_estabelecimentos_table.php | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 api/database/migrations/2025_06_05_000648_create_estabelecimentos_table.php 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..82d7ba0c --- /dev/null +++ b/api/database/migrations/2025_06_05_000648_create_estabelecimentos_table.php @@ -0,0 +1,41 @@ +id(); + $table->uuid('uuid')->unique('idx_estabelecimentos_uuid'); + + $table->enum('tipo', ['cpf', 'cnpj']); + $table->string('documento', 14)->unique('idx_estabelecimentos_documento'); + $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_nome'); + $table->index('telefone', 'idx_estabelecimentos_telefone'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('estabelecimentos'); + } +}; From 1962d045db6897b58328da3cfd61796c349b1602 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sat, 7 Jun 2025 20:32:57 -0300 Subject: [PATCH 06/21] feat (API) : Criacao da API gateway servicos BrasilApi e ViaCEP --- api/app/Helpers/ApiResponse.php | 94 ++++++++++++++++++ .../Controllers/Api/ApiExternalController.php | 95 +++++++++++++++++++ .../Api/Sistema/ApiSistemaController.php | 34 +++++++ .../Api/V1/ApiBrasilApiController.php | 88 +++++++++++++++++ .../Api/V1/ApiViaCepController.php | 53 +++++++++++ api/app/Rules/Cnpj.php | 45 +++++++++ api/app/Rules/Cpf.php | 35 +++++++ api/config/cache.php | 2 +- api/routes/api.php | 17 ++++ 9 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 api/app/Helpers/ApiResponse.php create mode 100644 api/app/Http/Controllers/Api/ApiExternalController.php create mode 100644 api/app/Http/Controllers/Api/Sistema/ApiSistemaController.php create mode 100644 api/app/Http/Controllers/Api/V1/ApiBrasilApiController.php create mode 100644 api/app/Http/Controllers/Api/V1/ApiViaCepController.php create mode 100644 api/app/Rules/Cnpj.php create mode 100644 api/app/Rules/Cpf.php create mode 100644 api/routes/api.php 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..613bee71 --- /dev/null +++ b/api/app/Http/Controllers/Api/ApiExternalController.php @@ -0,0 +1,95 @@ +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); + + 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 15 segundos + Cache::put($cacheKey, $dados, now()->addSeconds(15)); + + 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/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 @@ + env('CACHE_STORE', 'database'), + 'default' => env('CACHE_STORE', 'redis'), /* |-------------------------------------------------------------------------- diff --git a/api/routes/api.php b/api/routes/api.php new file mode 100644 index 00000000..6dfaeb78 --- /dev/null +++ b/api/routes/api.php @@ -0,0 +1,17 @@ +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::prefix('sistema')->group(function () { + Route::get('/config/cache', [ApiSistemaController::class, 'cacheConfig']); +}); + From 3546c8a93bbb6547e7e744b9e6912e32a291d2f6 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 17:41:30 -0300 Subject: [PATCH 07/21] feat (Docker) : atualizacao do ambiente Docker --- .docker/Dockerfile.api.development | 8 +- .docker/build.sh | 22 ++-- .docker/nginx.conf | 41 +++++++ .docker/pg17.pg_hba.conf | 135 +++++++++++++++++++++++ .docker/redis.conf | 147 +++++++++++++++++++++++++ .dockerignore | 1 + api/storage/framework/views/.gitignore | 0 build.sh | 13 ++- docker-compose.yaml | 53 ++++++--- 9 files changed, 393 insertions(+), 27 deletions(-) create mode 100644 .docker/nginx.conf create mode 100644 .docker/pg17.pg_hba.conf create mode 100644 .docker/redis.conf create mode 100644 .dockerignore mode change 100644 => 100755 api/storage/framework/views/.gitignore diff --git a/.docker/Dockerfile.api.development b/.docker/Dockerfile.api.development index 471ddb01..eedf2d91 100644 --- a/.docker/Dockerfile.api.development +++ b/.docker/Dockerfile.api.development @@ -2,10 +2,12 @@ FROM composer:latest AS composer_stage FROM php:8.3-fpm -# Instala dependências de sistema +# Instala dependencias RUN apt-get update && apt-get install -y \ - git curl libpq-dev zip unzip libzip-dev libonig-dev \ - && docker-php-ext-install pdo pdo_pgsql + 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 diff --git a/.docker/build.sh b/.docker/build.sh index a62450f6..093d3f84 100644 --- a/.docker/build.sh +++ b/.docker/build.sh @@ -2,21 +2,25 @@ APP_DIR="/var/www" -cd "$APP_DIR" || exit 1 - -# Espera até o volume estar montado +# Aguarda volume montado while [ ! -d "$APP_DIR" ]; do echo "Aguardando montagem de volume..." sleep 1 done -# Verifica se o diretório vendor existe -if [ -d "vendor" ]; then - echo "Diretório 'vendor' já existe. Pulando composer install." -else - echo "Diretório 'vendor' não encontrado. Executando composer install..." +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 -# Mantém o container em execução +# 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..36631481 --- /dev/null +++ b/.docker/pg17.pg_hba.conf @@ -0,0 +1,135 @@ +# 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 \ No newline at end of file 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/api/storage/framework/views/.gitignore b/api/storage/framework/views/.gitignore old mode 100644 new mode 100755 diff --git a/build.sh b/build.sh index 67589262..8a5be7dc 100755 --- a/build.sh +++ b/build.sh @@ -13,6 +13,8 @@ DIRS=( "./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..." @@ -25,10 +27,17 @@ for dir in "${DIRS[@]}"; do 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 -d +docker-compose up + -echo "Ambiente pronto!" diff --git a/docker-compose.yaml b/docker-compose.yaml index 1251abd0..40642c57 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,7 +12,11 @@ services: 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 @@ -20,15 +24,23 @@ services: api_db: container_name: api_db image: postgres:17 - restart: always + restart: unless-stopped + shm_size: 1GB # 1GB shared_buffers ⇒ /dev/shm 256 MB environment: - POSTGRES_DB: laravel - POSTGRES_USER: laravel - POSTGRES_PASSWORD: secret - user: 999:999 - ports: - - "5432:5432" + 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: @@ -42,7 +54,10 @@ services: volumes: - .docker/build.sh:/var/www/build.sh - ./api:/var/www - - vol_api_laravel-php-logs:/var/log/php + - 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 @@ -65,9 +80,9 @@ services: ports: - "8080:80" volumes: - - .:/var/www - - ./.docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - vol_api_laravel-nginx-logs:/var/log/nginx + - ./api:/var/www + - ./.docker/nginx.conf:/etc/nginx/nginx.conf:ro + - vol_api_nginx-logs:/var/log/nginx depends_on: - app networks: @@ -92,13 +107,25 @@ volumes: type: none device: ./data/${ENV_TYPE}/logs/postgres o: bind - vol_api_laravel-php-logs: + 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-nginx-logs: + 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 From 3019c3e9645a860a900d757e0c5525e724382f29 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 17:42:17 -0300 Subject: [PATCH 08/21] feat (API) : Validacao do cache para servicos BrasilApi e ViaCEP --- api/app/Http/Controllers/Api/ApiExternalController.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/app/Http/Controllers/Api/ApiExternalController.php b/api/app/Http/Controllers/Api/ApiExternalController.php index 613bee71..ce11786d 100644 --- a/api/app/Http/Controllers/Api/ApiExternalController.php +++ b/api/app/Http/Controllers/Api/ApiExternalController.php @@ -40,6 +40,7 @@ protected function request( // 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; @@ -65,8 +66,9 @@ protected function request( throw new \RuntimeException('Resposta inválida do serviço externo (JSON malformado)', 502); } - // Armazena no Redis por 15 segundos - Cache::put($cacheKey, $dados, now()->addSeconds(15)); + // Armazena no Redis por 30 segundos + $dados['caches'] = Cache::put($cacheKey, $dados, now()->addSeconds(30)); + $dados['cache'] = $cacheKey; return $dados; From 07837fd1314b305df66a14abd7468fb2531c97cb Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 19:30:48 -0300 Subject: [PATCH 09/21] feat (Docker) : atualizacao do ambiente Docker --- .docker/pg17.pg_hba.conf | 15 ++++++++++++--- docker-compose.yaml | 10 ++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.docker/pg17.pg_hba.conf b/.docker/pg17.pg_hba.conf index 36631481..99103a4b 100644 --- a/.docker/pg17.pg_hba.conf +++ b/.docker/pg17.pg_hba.conf @@ -126,10 +126,19 @@ 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 +#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 \ No newline at end of file +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-compose.yaml b/docker-compose.yaml index 40642c57..2732ffe5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,6 +26,8 @@ services: 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} @@ -65,11 +67,11 @@ services: - laravel-net - laravel-net-db environment: - - DB_HOST=postgres + - DB_HOST=api_db - DB_PORT=5432 - - DB_DATABASE=laravel - - DB_USERNAME=laravel - - DB_PASSWORD=secret + - DB_DATABASE=${DB_DATABASE} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} expose: - "9000" command: sh /build.sh From 590a4a398697f00b7fcee626b57aeb2d40105b17 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 19:31:58 -0300 Subject: [PATCH 10/21] feat (API:estabelecimentos) : CRUD API Estabelecimentos --- .../Api/V1/EstabelecimentoController.php | 108 ++++++++++++++++ .../Requests/StoreEstabelecimentoRequest.php | 38 ++++++ .../Requests/UpdateEstabelecimentoRequest.php | 0 api/app/Models/Estabelecimento.php | 31 +++++ api/app/Providers/RouteServiceProvider.php | 29 +++++ api/app/Repositories/FornecedorRepository.php | 16 +++ api/app/Services/EstabelecimentoService.php | 117 ++++++++++++++++++ api/app/Services/brasilAPI.php | 37 ++++++ api/bootstrap/providers.php | 1 + .../0001_01_01_000000_create_users_table.php | 49 ++++++++ .../0001_01_01_000001_create_cache_table.php | 35 ++++++ .../0001_01_01_000002_create_jobs_table.php | 57 +++++++++ ...5_000648_create_estabelecimentos_table.php | 3 +- api/routes/api.php | 3 + 14 files changed, 523 insertions(+), 1 deletion(-) create mode 100644 api/app/Http/Controllers/Api/V1/EstabelecimentoController.php create mode 100644 api/app/Http/Requests/StoreEstabelecimentoRequest.php create mode 100644 api/app/Http/Requests/UpdateEstabelecimentoRequest.php create mode 100644 api/app/Models/Estabelecimento.php create mode 100644 api/app/Providers/RouteServiceProvider.php create mode 100644 api/app/Repositories/FornecedorRepository.php create mode 100644 api/app/Services/EstabelecimentoService.php create mode 100644 api/app/Services/brasilAPI.php create mode 100644 api/database/migrations/0001_01_01_000000_create_users_table.php create mode 100644 api/database/migrations/0001_01_01_000001_create_cache_table.php create mode 100644 api/database/migrations/0001_01_01_000002_create_jobs_table.php 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..6c6bdb4a --- /dev/null +++ b/api/app/Http/Controllers/Api/V1/EstabelecimentoController.php @@ -0,0 +1,108 @@ +response = new ApiResponse(); + } + + public function index() + { + $data = Estabelecimento::paginate(10); + return $this->response->success('Lista de estabelecimentos carregada', $data); + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'tipo' => 'required|in:cpf,cnpj', + 'documento' => 'required|string|size:14|unique:estabelecimentos,documento,NULL,id,deleted_at,NULL', + 'nome' => 'required|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 = Estabelecimento::create($request->all()); + return $this->response->success('Estabelecimento criado com sucesso', $estabelecimento, 201); + } + + 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/Requests/StoreEstabelecimentoRequest.php b/api/app/Http/Requests/StoreEstabelecimentoRequest.php new file mode 100644 index 00000000..d3a82608 --- /dev/null +++ b/api/app/Http/Requests/StoreEstabelecimentoRequest.php @@ -0,0 +1,38 @@ + '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..7b641117 --- /dev/null +++ b/api/app/Models/Estabelecimento.php @@ -0,0 +1,31 @@ +uuid = (string) Str::uuid(); + }); + } +} diff --git a/api/app/Providers/RouteServiceProvider.php b/api/app/Providers/RouteServiceProvider.php new file mode 100644 index 00000000..fb4b6a4e --- /dev/null +++ b/api/app/Providers/RouteServiceProvider.php @@ -0,0 +1,29 @@ +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/Repositories/FornecedorRepository.php b/api/app/Repositories/FornecedorRepository.php new file mode 100644 index 00000000..f825e99c --- /dev/null +++ b/api/app/Repositories/FornecedorRepository.php @@ -0,0 +1,16 @@ +generateCacheKey($value); + + // Busca primeiro no cache + $cached = Cache::get($cacheKey); + if ($cached) { + return new Estabelecimento($cached); + } + + // Busca no banco por uuid, documento ou id + $est = Estabelecimento::where('uuid', $value) + ->orWhere('documento', $value) + ->orWhere('id', $value) + ->first(); + + if (!$est) { + throw new ModelNotFoundException('Registro não localizado'); + } + + // Armazena no cache - 15 minutos + Cache::put($cacheKey, $est->toArray(), now()->addMinutes(15)); + + return $est; + } + + private function generateCacheKey(string|int $value): string + { + return "establishment:{$value}"; + } + + public function create(array $data): Estabelecimento + { + $validator = Validator::make($data, [ + 'tipo' => 'required|in:cpf,cnpj', + 'documento' => 'required|string|size:14|unique:estabelecimentos,documento', + 'nome' => 'required|string|max:100', + 'contato' => 'nullable|string|max:100', + 'email' => 'nullable|email|max:100', + 'telefone' => 'nullable|string|max:14', + ]); + + if ($validator->fails()) { + throw new ValidationException($validator); + } + + $est = Estabelecimento::create($validator->validated()); + + // Armazena no cache após criação + $this->updateCache($est); + + return $est; + } + + public function update(string $uuid, array $data): Estabelecimento + { + $est = $this->find($uuid); + + $validator = Validator::make($data, [ + 'tipo' => 'in:cpf,cnpj', + 'documento' => 'string|size:14|unique:estabelecimentos,documento,' . $est->id, + 'nome' => 'string|max:100', + 'contato' => 'nullable|string|max:100', + 'email' => 'nullable|email|max:100', + 'telefone' => 'nullable|string|max:14', + ]); + + if ($validator->fails()) { + throw new ValidationException($validator); + } + + $est->update($validator->validated()); + + // Atualiza os valores no cache (por todas as chaves possíveis) + $this->updateCache($est); + + return $est; + } + + public function delete(string $uuid): void + { + $est = $this->find($uuid); + $est->delete(); + + // Limpa o cache relacionado + Cache::forget($this->generateCacheKey($uuid)); + Cache::forget($this->generateCacheKey($est->documento)); + Cache::forget($this->generateCacheKey($est->id)); + } + + private function updateCache(Estabelecimento $est): void + { + $ttl = now()->addMinutes(15); + + Cache::put($this->generateCacheKey($est->uuid), $est->toArray(), $ttl); + Cache::put($this->generateCacheKey($est->documento), $est->toArray(), $ttl); + Cache::put($this->generateCacheKey($est->id), $est->toArray(), $ttl); + } +} diff --git a/api/app/Services/brasilAPI.php b/api/app/Services/brasilAPI.php new file mode 100644 index 00000000..ca97e5a2 --- /dev/null +++ b/api/app/Services/brasilAPI.php @@ -0,0 +1,37 @@ +get("{$this->baseUrl}/cnpj/v1/{$cnpj}"); + + if ($response->successful()) { + return $response->json(); + } + + Log::error('BrasilAPI CNPJ lookup failed: ' . $response->status(), [ + 'cnpj' => $cnpj, + 'response_body' => $response->body() + ]); + return null; + } catch (\Exception $e) { + Log::error('Exception during BrasilAPI CNPJ lookup: ' . $e->getMessage(), ['cnpj' => $cnpj]); + return null; + } + } +} \ No newline at end of file diff --git a/api/bootstrap/providers.php b/api/bootstrap/providers.php index 38b258d1..f4ecddf9 100644 --- a/api/bootstrap/providers.php +++ b/api/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\RouteServiceProvider::class, ]; 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 index 82d7ba0c..0a5a8d65 100644 --- a/api/database/migrations/2025_06_05_000648_create_estabelecimentos_table.php +++ b/api/database/migrations/2025_06_05_000648_create_estabelecimentos_table.php @@ -16,7 +16,7 @@ public function up(): void $table->uuid('uuid')->unique('idx_estabelecimentos_uuid'); $table->enum('tipo', ['cpf', 'cnpj']); - $table->string('documento', 14)->unique('idx_estabelecimentos_documento'); + $table->string('documento', 14); $table->string('nome', 100); $table->string('contato', 100)->nullable(); $table->string('email', 100)->nullable(); @@ -26,6 +26,7 @@ public function up(): void $table->softDeletes(); //Indices + $table->index('nome', 'idx_estabelecimentos_documento'); $table->index('nome', 'idx_estabelecimentos_nome'); $table->index('telefone', 'idx_estabelecimentos_telefone'); }); diff --git a/api/routes/api.php b/api/routes/api.php index 6dfaeb78..597d00b2 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -4,11 +4,14 @@ use App\Http\Controllers\Api\V1\ApiBrasilApiController; use App\Http\Controllers\Api\V1\ApiViaCepController; use App\Http\Controllers\Api\Sistema\ApiSistemaController; +use App\Http\Controllers\Api\V1\EstabelecimentoController; Route::prefix('v1')->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 () { From 982dd3a25422de7eb9064bbdb3591c5bc49bea2c Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 19:44:50 -0300 Subject: [PATCH 11/21] feat (env:development) : env desenvolvimento docker --- api/.env.development | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 api/.env.development 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}" From 2eaf67b3ea358668bf095879f110992d41b7433a Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 19:50:16 -0300 Subject: [PATCH 12/21] feat (postman) : Arquivo do postman --- postman.json | 426 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 postman.json 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 From 9b14c05044f203c3f1d3ef64888c67c59665a873 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 20:18:45 -0300 Subject: [PATCH 13/21] test (Estabelecimento) : Implementacao de testes --- api/app/Models/Estabelecimento.php | 3 +- .../factories/EstabelecimentoFactory.php | 22 ++++ .../Feature/EstabelecimentoFeatureTest.php | 117 ++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 api/database/factories/EstabelecimentoFactory.php create mode 100644 api/tests/Feature/EstabelecimentoFeatureTest.php diff --git a/api/app/Models/Estabelecimento.php b/api/app/Models/Estabelecimento.php index 7b641117..5765f1ed 100644 --- a/api/app/Models/Estabelecimento.php +++ b/api/app/Models/Estabelecimento.php @@ -5,10 +5,11 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Factories\HasFactory; class Estabelecimento extends Model { - use SoftDeletes; + use HasFactory, SoftDeletes; protected $table = 'estabelecimentos'; 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/tests/Feature/EstabelecimentoFeatureTest.php b/api/tests/Feature/EstabelecimentoFeatureTest.php new file mode 100644 index 00000000..f8a271d9 --- /dev/null +++ b/api/tests/Feature/EstabelecimentoFeatureTest.php @@ -0,0 +1,117 @@ + 'cnpj', + 'documento' => '12345678000199', + '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' => '12345678000199', + ]); + } + + public function test_criar_estabelecimento_duplicado() + { + Estabelecimento::factory()->create([ + 'documento' => '12345678000199', + ]); + + $payload = [ + 'tipo' => 'cnpj', + 'documento' => '12345678000199', + 'nome' => 'Empresa Teste 2', + ]; + + $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']); + } +} From 9f65ae277875d8ce3e73cfda3441d2f29d6f6249 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 20:34:14 -0300 Subject: [PATCH 14/21] fix (Docker) : Redis - Mantendo todos os dados em memoria --- docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 2732ffe5..ca00dff7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -20,6 +20,8 @@ services: - vol_api_redis:/data networks: - laravel-net-db + command: ["redis-server", "--stop-writes-on-bgsave-error", "no"] + api_db: container_name: api_db From 65b8c1656aade531e49f5de735a80563ac19ae97 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 20:34:54 -0300 Subject: [PATCH 15/21] chore (readme.md) : Readme de instalacao e funcionalidades --- api/README.md | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 api/README.md diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000..6a6b5136 --- /dev/null +++ b/api/README.md @@ -0,0 +1,179 @@ +## Teste para Desenvolvedor PHP/Laravel + +### Backend (API Laravel): + +Optei por utilizar o conceito de estabelecimentos, uma vez que temos clientes e fornecedores como um único cadastro. + +#### 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. + +- **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 From 8107a74e753f21b7642a51774a9faea4157afa1d Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 21:00:51 -0300 Subject: [PATCH 16/21] feat (API) : Implementacao do Strategie Pattern --- .../Api/V1/EstabelecimentoController.php | 27 ++-- api/app/Repositories/FornecedorRepository.php | 16 --- .../EstabelecimentoContext.php | 27 ++++ .../EstabelecimentoStrategyCNPJ.php | 29 +++++ .../Strategies/EstabelecimentoStrategyCPF.php | 29 +++++ .../EstabelecimentoStrategyInterface.php | 10 ++ api/app/Services/EstabelecimentoService.php | 117 ------------------ api/app/Services/brasilAPI.php | 37 ------ api/tests/Feature/ExampleTest.php | 19 --- 9 files changed, 109 insertions(+), 202 deletions(-) delete mode 100644 api/app/Repositories/FornecedorRepository.php create mode 100644 api/app/Services/Estabelecimento/EstabelecimentoContext.php create mode 100644 api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyCNPJ.php create mode 100644 api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyCPF.php create mode 100644 api/app/Services/Estabelecimento/Strategies/EstabelecimentoStrategyInterface.php delete mode 100644 api/app/Services/EstabelecimentoService.php delete mode 100644 api/app/Services/brasilAPI.php delete mode 100644 api/tests/Feature/ExampleTest.php diff --git a/api/app/Http/Controllers/Api/V1/EstabelecimentoController.php b/api/app/Http/Controllers/Api/V1/EstabelecimentoController.php index 6c6bdb4a..e28a8e07 100644 --- a/api/app/Http/Controllers/Api/V1/EstabelecimentoController.php +++ b/api/app/Http/Controllers/Api/V1/EstabelecimentoController.php @@ -7,6 +7,7 @@ use App\Models\Estabelecimento; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; +use App\Services\Estabelecimento\EstabelecimentoContext; class EstabelecimentoController extends Controller { @@ -23,23 +24,23 @@ public function index() return $this->response->success('Lista de estabelecimentos carregada', $data); } + public function store(Request $request) { - $validator = Validator::make($request->all(), [ - 'tipo' => 'required|in:cpf,cnpj', - 'documento' => 'required|string|size:14|unique:estabelecimentos,documento,NULL,id,deleted_at,NULL', - 'nome' => 'required|string|max:100', - 'contato' => 'nullable|string|max:100', - 'email' => 'nullable|email|max:100', - 'telefone' => 'nullable|string|max:14', - ]); + $tipo = $request->input('tipo'); - if ($validator->fails()) { - return $this->response->error('Erro de validação', $validator->errors(), 422); - } + try { + $context = new EstabelecimentoContext($tipo); + $result = $context->handle($request); - $estabelecimento = Estabelecimento::create($request->all()); - return $this->response->success('Estabelecimento criado com sucesso', $estabelecimento, 201); + 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) diff --git a/api/app/Repositories/FornecedorRepository.php b/api/app/Repositories/FornecedorRepository.php deleted file mode 100644 index f825e99c..00000000 --- a/api/app/Repositories/FornecedorRepository.php +++ /dev/null @@ -1,16 +0,0 @@ -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 @@ +generateCacheKey($value); - - // Busca primeiro no cache - $cached = Cache::get($cacheKey); - if ($cached) { - return new Estabelecimento($cached); - } - - // Busca no banco por uuid, documento ou id - $est = Estabelecimento::where('uuid', $value) - ->orWhere('documento', $value) - ->orWhere('id', $value) - ->first(); - - if (!$est) { - throw new ModelNotFoundException('Registro não localizado'); - } - - // Armazena no cache - 15 minutos - Cache::put($cacheKey, $est->toArray(), now()->addMinutes(15)); - - return $est; - } - - private function generateCacheKey(string|int $value): string - { - return "establishment:{$value}"; - } - - public function create(array $data): Estabelecimento - { - $validator = Validator::make($data, [ - 'tipo' => 'required|in:cpf,cnpj', - 'documento' => 'required|string|size:14|unique:estabelecimentos,documento', - 'nome' => 'required|string|max:100', - 'contato' => 'nullable|string|max:100', - 'email' => 'nullable|email|max:100', - 'telefone' => 'nullable|string|max:14', - ]); - - if ($validator->fails()) { - throw new ValidationException($validator); - } - - $est = Estabelecimento::create($validator->validated()); - - // Armazena no cache após criação - $this->updateCache($est); - - return $est; - } - - public function update(string $uuid, array $data): Estabelecimento - { - $est = $this->find($uuid); - - $validator = Validator::make($data, [ - 'tipo' => 'in:cpf,cnpj', - 'documento' => 'string|size:14|unique:estabelecimentos,documento,' . $est->id, - 'nome' => 'string|max:100', - 'contato' => 'nullable|string|max:100', - 'email' => 'nullable|email|max:100', - 'telefone' => 'nullable|string|max:14', - ]); - - if ($validator->fails()) { - throw new ValidationException($validator); - } - - $est->update($validator->validated()); - - // Atualiza os valores no cache (por todas as chaves possíveis) - $this->updateCache($est); - - return $est; - } - - public function delete(string $uuid): void - { - $est = $this->find($uuid); - $est->delete(); - - // Limpa o cache relacionado - Cache::forget($this->generateCacheKey($uuid)); - Cache::forget($this->generateCacheKey($est->documento)); - Cache::forget($this->generateCacheKey($est->id)); - } - - private function updateCache(Estabelecimento $est): void - { - $ttl = now()->addMinutes(15); - - Cache::put($this->generateCacheKey($est->uuid), $est->toArray(), $ttl); - Cache::put($this->generateCacheKey($est->documento), $est->toArray(), $ttl); - Cache::put($this->generateCacheKey($est->id), $est->toArray(), $ttl); - } -} diff --git a/api/app/Services/brasilAPI.php b/api/app/Services/brasilAPI.php deleted file mode 100644 index ca97e5a2..00000000 --- a/api/app/Services/brasilAPI.php +++ /dev/null @@ -1,37 +0,0 @@ -get("{$this->baseUrl}/cnpj/v1/{$cnpj}"); - - if ($response->successful()) { - return $response->json(); - } - - Log::error('BrasilAPI CNPJ lookup failed: ' . $response->status(), [ - 'cnpj' => $cnpj, - 'response_body' => $response->body() - ]); - return null; - } catch (\Exception $e) { - Log::error('Exception during BrasilAPI CNPJ lookup: ' . $e->getMessage(), ['cnpj' => $cnpj]); - return null; - } - } -} \ No newline at end of file diff --git a/api/tests/Feature/ExampleTest.php b/api/tests/Feature/ExampleTest.php deleted file mode 100644 index 8364a84e..00000000 --- a/api/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get('/'); - - $response->assertStatus(200); - } -} From b0654884bd2d866e85e523fc31189022d66432c6 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 21:01:06 -0300 Subject: [PATCH 17/21] test (API) : Implementacao do Strategie Pattern --- .../Feature/EstabelecimentoFeatureTest.php | 142 +++++++++++++++++- 1 file changed, 138 insertions(+), 4 deletions(-) diff --git a/api/tests/Feature/EstabelecimentoFeatureTest.php b/api/tests/Feature/EstabelecimentoFeatureTest.php index f8a271d9..7eec915c 100644 --- a/api/tests/Feature/EstabelecimentoFeatureTest.php +++ b/api/tests/Feature/EstabelecimentoFeatureTest.php @@ -14,7 +14,7 @@ public function test_criar_estabelecimento() { $payload = [ 'tipo' => 'cnpj', - 'documento' => '12345678000199', + 'documento' => '12345678901230', 'nome' => 'Empresa Teste Ltda', 'contato' => 'Joao da Silva', 'email' => 'joao@empresa.com', @@ -27,19 +27,19 @@ public function test_criar_estabelecimento() ->assertJsonFragment(['nome' => 'Empresa Teste Ltda']); $this->assertDatabaseHas('estabelecimentos', [ - 'documento' => '12345678000199', + 'documento' => '12345678901230', ]); } public function test_criar_estabelecimento_duplicado() { Estabelecimento::factory()->create([ - 'documento' => '12345678000199', + 'documento' => '12345678901230', ]); $payload = [ 'tipo' => 'cnpj', - 'documento' => '12345678000199', + 'documento' => '12345678901230', 'nome' => 'Empresa Teste 2', ]; @@ -49,6 +49,90 @@ public function test_criar_estabelecimento_duplicado() ->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(); @@ -114,4 +198,54 @@ public function test_listar_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); + } + + } From e3bb8be797fe8f37c6de5920563b8d95494c348b Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 21:09:13 -0300 Subject: [PATCH 18/21] chore (README.md) : Atualizacao do readme.md para incluir o objetivo do Strategi Pattern --- api/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/README.md b/api/README.md index 6a6b5136..a898d764 100644 --- a/api/README.md +++ b/api/README.md @@ -4,10 +4,19 @@ 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. From c11f0596f147e71dbf5dc49ebb58ec4ac6f32dcd Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 21:10:24 -0300 Subject: [PATCH 19/21] chore (README.md) : Atualizacao do readme.md para incluir o objetivo do Strategi Pattern --- api/README.md => INSTALL.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api/README.md => INSTALL.md (100%) diff --git a/api/README.md b/INSTALL.md similarity index 100% rename from api/README.md rename to INSTALL.md From d455d3af3599f5358f50ecdecf7bea293d88ce84 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 21:12:32 -0300 Subject: [PATCH 20/21] chore (README.md) : Atualizacao do readme.md para incluir o objetivo do Strategi Pattern --- INSTALL.md | 188 ---------------------------------------------------- README.md | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 189 deletions(-) delete mode 100644 INSTALL.md diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index a898d764..00000000 --- a/INSTALL.md +++ /dev/null @@ -1,188 +0,0 @@ -## Teste para Desenvolvedor PHP/Laravel - -### 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/README.md b/README.md index ff000371..3701f549 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. @@ -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 From dc22c515a5db651d0387709692ac2b5851221d38 Mon Sep 17 00:00:00 2001 From: Rodrigo Dittmar Date: Sun, 8 Jun 2025 21:15:28 -0300 Subject: [PATCH 21/21] chore (README.md) : Atualizacao do readme.md para incluir o objetivo do Strategi Pattern --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3701f549..3b46248f 100644 --- a/README.md +++ b/README.md @@ -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.