docs: sync OpenSpec archives and propose foundation parity

Archive completed public-site and production-provider changes into main specs, remove duplicate active changes, and add complete-foundation-parity so Phase 0 staging and remaining foundation gaps block Phase 2 cleanly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-01 21:55:04 -03:00
parent cafb1167ac
commit 9dd6fcf409
53 changed files with 885 additions and 828 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-29

View File

@@ -0,0 +1,80 @@
## Context
Fase 01 entregaram CMS com uploads de mídia via `PublicImageUploadRules`, que hoje seleciona disco `public` localmente e `s3` quando `FILESYSTEM_DISK=s3`. Laravel 13 já inclui mailer `resend` em [`config/mail.php`](../../config/mail.php) e chave em [`config/services.php`](../../config/services.php), mas faltam dependências (`resend/resend-php`, `league/flysystem-aws-s3-v3`) e configuração explícita para Cloudflare R2.
Produção usará **Resend** para e-mail transacional e **Cloudflare R2** para mídia pública via domínio customizado. Local e CI continuam com `MAIL_MAILER=log`/`array` e disco `public`.
## Goals / Non-Goals
**Goals:**
- Resend como transporte nativo Laravel em produção (`MAIL_MAILER=resend`, `RESEND_API_KEY`).
- Disco dedicado `r2` com variáveis explícitas (`R2_*`) e URL pública via `R2_URL` (domínio customizado).
- Seleção de disco de mídia alinhada a `FILESYSTEM_DISK=r2`.
- Documentação de env vars e testes automatizados de config/seleção (sem chamadas live).
**Non-Goals:**
- Templates de e-mail, filas de notificação de leads, ou fluxos CRM.
- Buckets privados, signed URLs, ou automação de DNS/CDN.
- Mudança de queue driver ou provisionamento Cloudflare via código.
## Decisions
### 1. Resend via transporte nativo Laravel
**Decisão:** usar mailer `resend` já presente em `config/mail.php` + pacote `resend/resend-php`. Credencial em `config/services.php``RESEND_API_KEY`.
**Alternativa rejeitada:** SMTP genérico — menos idiomático; perde integração nativa Laravel 13.
### 2. Disco dedicado `r2` (não reutilizar nome `s3`)
**Decisão:** adicionar disco `r2` em `config/filesystems.php` com driver `s3`, endpoint R2 (`https://<account_id>.r2.cloudflarestorage.com`), `use_path_style_endpoint=true`, bucket e credenciais via `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_ENDPOINT`, `R2_URL` (domínio público customizado).
**Alternativa rejeitada:** reutilizar disco `s3` com vars `AWS_*` — funciona tecnicamente, mas obscurece provedor real e conflita com futuro uso de AWS S3.
### 3. `FILESYSTEM_DISK=r2` em produção
**Decisão:** default disk de produção = `r2`. Local/test = `local` ou `public`. `PublicImageUploadRules::disk()` retorna `r2` quando default é `r2`, `s3` quando default é `s3`, senão `public`.
**Alternativa rejeitada:** mapear `r2` para disco `s3` internamente — confunde operadores e quebra intenção explícita do env.
### 4. URLs públicas via domínio customizado
**Decisão:** `R2_URL` aponta para domínio customizado (ex.: `https://media.example.com`) mapeado ao bucket via Cloudflare. Disco `r2` define `url => env('R2_URL')` e `visibility => public`.
**Alternativa rejeitada:** URL `r2.dev` gerenciada — menos controle de marca e SEO; usuário escolheu custom domain.
### 5. Testes sem integração live
**Decisão:** testes usam `Mail::fake`, `Storage::fake('r2')`, e asserts de config (`config('mail.default')`, `config('filesystems.disks.r2')`, `PublicImageUploadRules::disk()`). Nenhuma chamada HTTP a Resend/R2 no CI.
### 6. Defaults seguros por ambiente
**Decisão:**
| Ambiente | `MAIL_MAILER` | `FILESYSTEM_DISK` | Disco de upload CMS |
|---|---|---|---|
| Local dev | `log` | `local` | `public` |
| Testes (phpunit) | `array` | (unset → local) | `public` |
| Produção | `resend` | `r2` | `r2` |
## Risks / Trade-offs
- **[R2 credentials missing in prod]** → app boot ok, upload fails at runtime; document required vars in `.env.example` and README; config test asserts disk definition exists.
- **[Custom domain not configured]** → broken public image URLs; `R2_URL` documented as required for production media.
- **[Resend API key missing]** → mail send fails; lead creation must not depend on mail (future phases per SPEC).
- **[Package version drift]** → pin compatible versions in `composer.json`; run `composer security-audit` in CI.
## Migration Plan
1. Merge change; run `composer install` in production image build.
2. Create R2 bucket, API token, and custom domain in Cloudflare dashboard (manual).
3. Create Resend API key and verify sending domain.
4. Set production env: `MAIL_MAILER=resend`, `RESEND_API_KEY`, `FILESYSTEM_DISK=r2`, `R2_*`, `R2_URL`, `MAIL_FROM_*`.
5. Deploy; upload test image via Filament; verify public URL resolves.
6. **Rollback:** revert env to `MAIL_MAILER=log`, `FILESYSTEM_DISK=public`; existing DB paths remain valid for local disk until re-upload.
## Open Questions
- _(none — custom domain confirmed; Resend as sole production mail provider for this change)_

View File

@@ -0,0 +1,43 @@
## Why
The application already supports CMS media uploads and will soon send transactional email (lead notifications, password reset), but production still lacks concrete provider wiring. SPEC §9.1 and §15.4 require S3-compatible object storage in production and transactional email when configured. Resend and Cloudflare R2 are the chosen providers; this change adds the configuration, dependencies, and selection rules so production can send mail and persist public media outside the container filesystem.
## What Changes
- Add **Resend** as the production mail transport via Laravel's native `resend` mailer and `RESEND_API_KEY`.
- Add a dedicated **`r2` filesystem disk** for Cloudflare R2 (S3-compatible API) with explicit R2 env vars and public URLs served through a **custom domain** (`R2_URL`).
- Install required dependencies: `resend/resend-php` and `league/flysystem-aws-s3-v3`.
- Update `.env.example`, README, and deployment guidance with production vs local/test defaults.
- Fix media disk selection in `PublicImageUploadRules` so `FILESYSTEM_DISK=r2` uses the R2 disk instead of falling back to local `public`.
- Add automated config and media-disk selection tests; no live calls to Resend or R2 in CI.
## Non-Goals
Conforme [SPEC.md §4.2](../../SPEC.md), **não** fazem parte desta change:
- Lead notification emails, briefing confirmation flows, or CRM email templates (future phases).
- Private/signed URL access to media; production media is public via custom domain.
- Cloudflare Workers, CDN provisioning, or DNS automation (manual infra setup).
- Redis, queue driver changes, or FrankenPHP worker mode.
- Replacing local `public` disk behavior for development and tests.
## Capabilities
### New Capabilities
- `transactional-email`: Resend-backed transactional mail transport with safe local/test defaults and production configuration via environment variables (SPEC §9.1, §15.4, §12.112.2).
- `object-storage`: Cloudflare R2 object storage via dedicated `r2` disk, custom-domain public URLs, and environment-driven disk selection (SPEC §6.4, §9.1, §15.4).
### Modified Capabilities
- `content-media`: extend disk selection so production `FILESYSTEM_DISK=r2` stores and serves public content images from R2 instead of local `public`.
## Impact
- **Dependencies**: `resend/resend-php`, `league/flysystem-aws-s3-v3` in `composer.json`.
- **Config**: `config/mail.php`, `config/services.php`, `config/filesystems.php`, `.env.example`.
- **Application**: `app/Support/PublicImageUploadRules.php` disk selection logic.
- **Tests**: new Pest feature/unit tests for mail and filesystem config; media disk selection regression test.
- **Docs**: `README.md` production provider section.
- **Infra (manual)**: Resend API key, R2 bucket, R2 API token, and custom domain mapped to the bucket.
- **No breaking change** for local development: defaults remain `MAIL_MAILER=log` and `FILESYSTEM_DISK=local`/`public`.

View File

@@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: CMS public image uploads select production object storage disk
Public content image uploads (Filament FileUpload via `PublicImageUploadRules`) MUST store files on the disk matching the configured default filesystem (SPEC §6.4). When `FILESYSTEM_DISK` is `r2`, uploads MUST use the `r2` disk. When `FILESYSTEM_DISK` is `s3`, uploads MUST use the `s3` disk. Otherwise uploads MUST use the local `public` disk.
#### Scenario: Production R2 disk is used for CMS uploads
- **WHEN** `FILESYSTEM_DISK=r2`
- **THEN** `PublicImageUploadRules::disk()` MUST return `r2`
#### Scenario: Legacy S3 default still supported
- **WHEN** `FILESYSTEM_DISK=s3`
- **THEN** `PublicImageUploadRules::disk()` MUST return `s3`
#### Scenario: Local development uses public disk
- **WHEN** `FILESYSTEM_DISK` is `local`, unset, or any value other than `r2`/`s3`
- **THEN** `PublicImageUploadRules::disk()` MUST return `public`

View File

@@ -0,0 +1,44 @@
## ADDED Requirements
### Requirement: Cloudflare R2 is available as dedicated filesystem disk
The system SHALL provide an `r2` filesystem disk using Laravel's S3-compatible driver pointed at Cloudflare R2 (SPEC §6.4, §9.1, §15.4). Configuration MUST use explicit `R2_*` environment variables: `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_ENDPOINT`, and `R2_URL`.
#### Scenario: R2 disk is defined in filesystem config
- **WHEN** the application boots
- **THEN** `config('filesystems.disks.r2')` MUST exist
- **AND** the disk driver MUST be `s3`
- **AND** credentials MUST resolve from `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY`
#### Scenario: R2 endpoint uses path-style addressing
- **WHEN** the `r2` disk is configured
- **THEN** `use_path_style_endpoint` MUST be `true`
- **AND** `endpoint` MUST resolve from `R2_ENDPOINT`
#### Scenario: Public URLs use custom domain
- **WHEN** a file is stored on the `r2` disk with public visibility
- **THEN** generated URLs MUST use `R2_URL` as base
- **AND** MUST NOT require signed URLs for browser-facing media
### Requirement: Production default filesystem disk is R2
Production deployments MUST set `FILESYSTEM_DISK=r2` so application code using the default disk stores files on R2.
#### Scenario: Default disk resolves to R2 in production
- **WHEN** `FILESYSTEM_DISK=r2` is set
- **THEN** `config('filesystems.default')` MUST be `r2`
#### Scenario: Local development keeps local disk
- **WHEN** `FILESYSTEM_DISK` is unset or set to `local`/`public` in development
- **THEN** the default disk MUST NOT require R2 credentials to boot
#### Scenario: Tests do not call R2
- **WHEN** the test suite runs
- **THEN** tests MUST use `Storage::fake('r2')` or local fakes
- **AND** no live HTTP request to Cloudflare R2 MUST occur during CI

View File

@@ -0,0 +1,32 @@
## ADDED Requirements
### Requirement: Production transactional email uses Resend
The system SHALL support Resend as the production mail transport via Laravel's native `resend` mailer (SPEC §9.1, §15.4). The Resend API key MUST be read from `RESEND_API_KEY` through `config/services.php`. Production deployments MUST set `MAIL_MAILER=resend` when transactional email is enabled.
#### Scenario: Resend mailer is configured
- **WHEN** the application boots with valid configuration
- **THEN** a `resend` mailer MUST exist in `config/mail.php`
- **AND** `config('services.resend.key')` MUST resolve from `RESEND_API_KEY`
#### Scenario: Local development uses safe mail default
- **WHEN** `APP_ENV` is `local` and `MAIL_MAILER` is unset
- **THEN** the default mailer MUST be `log`
- **AND** no outbound email MUST be sent to external providers
#### Scenario: Tests do not call Resend
- **WHEN** the test suite runs
- **THEN** `MAIL_MAILER` MUST be `array` (or tests MUST use `Mail::fake`)
- **AND** no live HTTP request to Resend MUST occur during CI
### Requirement: Global from address is configurable
The system SHALL read the global sender address and name from `MAIL_FROM_ADDRESS` and `MAIL_FROM_NAME` (SPEC §15.4).
#### Scenario: From address applied to outbound mail
- **WHEN** the application sends mail through the configured mailer
- **THEN** the message MUST use the configured from address and name

View File

@@ -0,0 +1,33 @@
## 1. Dependencies
- [x] 1.1 Add `resend/resend-php` to `composer.json` require
- [x] 1.2 Add `league/flysystem-aws-s3-v3` to `composer.json` require
- [x] 1.3 Run `composer update resend/resend-php league/flysystem-aws-s3-v3 --with-all-dependencies` and commit lockfile
## 2. Transactional email (transactional-email)
- [x] 2.1 Confirm `config/mail.php` resend mailer and `config/services.php` `RESEND_API_KEY` binding
- [x] 2.2 Add `RESEND_API_KEY` and production mail vars to `.env.example` with safe local defaults (`MAIL_MAILER=log`)
- [x] 2.3 Write feature test asserting resend mailer config and services key resolution
## 3. Object storage (object-storage)
- [x] 3.1 Add dedicated `r2` disk to `config/filesystems.php` with `R2_*` env vars, path-style endpoint, and `R2_URL`
- [x] 3.2 Add R2 env vars to `.env.example` with comments for custom domain setup
- [x] 3.3 Write feature test asserting `r2` disk config (driver, endpoint, path-style, url from `R2_URL`)
## 4. CMS media disk selection (content-media)
- [x] 4.1 Update `PublicImageUploadRules::disk()` to return `r2` when `FILESYSTEM_DISK=r2`, keep `s3` and `public` fallbacks
- [x] 4.2 Write unit/feature test for disk selection matrix: `r2`, `s3`, local/unset → `public`
## 5. Documentation
- [x] 5.1 Update `README.md` with production provider section (Resend + R2 + custom domain)
- [x] 5.2 Document production env checklist: `MAIL_MAILER=resend`, `FILESYSTEM_DISK=r2`, required secrets
## 6. Verification
- [x] 6.1 Run `composer pint:check` and `composer phpstan`
- [x] 6.2 Run `composer test:unit` and `composer test:feature` for new provider tests
- [x] 6.3 Run `composer quality` and fix any failures