diff --git a/.agents/skills/translations/SKILL.md b/.agents/skills/translations/SKILL.md new file mode 100644 index 0000000000..532b1c1b10 --- /dev/null +++ b/.agents/skills/translations/SKILL.md @@ -0,0 +1,64 @@ +--- +name: translations +description: Frontend translation workflow using Lingui - extracting, adding, and compiling translations for all supported languages +--- + +# Frontend Translation Workflow (Lingui) + +IMPORTANT: Always update translations as you develop features. When adding new translatable strings, immediately add translations for all supported languages. + +## Core Commands + +```bash +cd frontend + +# Extract translatable strings (use --clean for accurate counts) +yarn messages:extract --clean + +# Compile translations for production +yarn messages:compile + +# Check for untranslated strings +cd scripts && ./list_untranslated_strings.sh +``` + +## Process + +1. **Extract**: `yarn messages:extract --clean` +2. **Check**: Look at the output table for missing translation counts +3. **Add translations**: Update the `.po` files for each language +4. **Verify**: Run extract again to confirm 0 missing +5. **Compile**: `yarn messages:compile` + +## Adding Translations + +Add entries to each locale's `.po` file in `frontend/src/locales/`: + +```po +#: src/path/to/component.tsx:123 +msgid "Your English String" +msgstr "Translated String" +``` + +## Supported Languages + +| Code | Language | +|------|----------| +| en | English (source - no translation needed) | +| de | Deutsch | +| es | Espanol | +| fr | Francais | +| pt | Portugues | +| pt-br | Portugues do Brasil | +| it | Italiano | +| nl | Nederlands | +| zh-cn | Simplified Chinese | +| zh-hk | Traditional Chinese (HK) | +| vi | Tieng Viet | +| ru | Russian (currently untranslated) | + +## Troubleshooting + +- **Counts seem wrong**: Use `--clean` flag to remove obsolete entries +- **Translation not appearing**: Run `yarn messages:compile` after adding +- **Syntax errors**: Check for proper escaping of quotes in `.po` files diff --git a/.claude/skills/translations/SKILL.md b/.claude/skills/translations/SKILL.md index 532b1c1b10..cb9ffeb320 100644 --- a/.claude/skills/translations/SKILL.md +++ b/.claude/skills/translations/SKILL.md @@ -1,64 +1 @@ ---- -name: translations -description: Frontend translation workflow using Lingui - extracting, adding, and compiling translations for all supported languages ---- - -# Frontend Translation Workflow (Lingui) - -IMPORTANT: Always update translations as you develop features. When adding new translatable strings, immediately add translations for all supported languages. - -## Core Commands - -```bash -cd frontend - -# Extract translatable strings (use --clean for accurate counts) -yarn messages:extract --clean - -# Compile translations for production -yarn messages:compile - -# Check for untranslated strings -cd scripts && ./list_untranslated_strings.sh -``` - -## Process - -1. **Extract**: `yarn messages:extract --clean` -2. **Check**: Look at the output table for missing translation counts -3. **Add translations**: Update the `.po` files for each language -4. **Verify**: Run extract again to confirm 0 missing -5. **Compile**: `yarn messages:compile` - -## Adding Translations - -Add entries to each locale's `.po` file in `frontend/src/locales/`: - -```po -#: src/path/to/component.tsx:123 -msgid "Your English String" -msgstr "Translated String" -``` - -## Supported Languages - -| Code | Language | -|------|----------| -| en | English (source - no translation needed) | -| de | Deutsch | -| es | Espanol | -| fr | Francais | -| pt | Portugues | -| pt-br | Portugues do Brasil | -| it | Italiano | -| nl | Nederlands | -| zh-cn | Simplified Chinese | -| zh-hk | Traditional Chinese (HK) | -| vi | Tieng Viet | -| ru | Russian (currently untranslated) | - -## Troubleshooting - -- **Counts seem wrong**: Use `--clean` flag to remove obsolete entries -- **Translation not appearing**: Run `yarn messages:compile` after adding -- **Syntax errors**: Check for proper escaping of quotes in `.po` files +see @../../../.agents/skills/translations/SKILL.md diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..f82f5eec0a --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,12 @@ +# Revisions listed here are ignored by `git blame` (bulk reformatting, etc.). +# +# Enable locally with: +# git config blame.ignoreRevsFile .git-blame-ignore-revs +# +# GitHub applies this file automatically. + +# style: apply Pint formatting across backend +b145fed4d0b0f82bf29f48145672f04bca6b5170 + +# style: remove explanatory comments across backend and frontend +00ba8bbb388d05e7d6498a25b049a47661d84861 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2d19756c06..be4d021e85 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,7 +33,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.5 tools: composer:v2 coverage: none diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000000..014e169507 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,118 @@ +name: E2E Tests + +on: + push: + branches: [main, develop] + paths: + - 'backend/**' + - 'frontend/**' + - 'e2e/**' + - 'docker/e2e/**' + - '.github/workflows/e2e.yml' + pull_request: + types: [opened, synchronize, reopened, labeled] + paths: + - 'backend/**' + - 'frontend/**' + - 'e2e/**' + - 'docker/e2e/**' + - '.github/workflows/e2e.yml' + schedule: + - cron: '0 3 * * *' + +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true + +jobs: + plan: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + steps: + - id: set + env: + FULL: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-e2e') }} + run: | + if [ "$FULL" = "true" ]; then + echo 'matrix={"include":[{"name":"full-1","pw_args":"--shard=1/2"},{"name":"full-2","pw_args":"--shard=2/2"}]}' >> "$GITHUB_OUTPUT" + else + echo 'matrix={"include":[{"name":"smoke","pw_args":"--grep @smoke"}]}' >> "$GITHUB_OUTPUT" + fi + + e2e: + needs: plan + runs-on: ubuntu-latest + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.plan.outputs.matrix) }} + + env: + COMPOSE_FILE: docker/e2e/docker-compose.e2e.yml + E2E_IMAGE_TAG: ci + STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_TEST_PUBLIC_KEY }} + STRIPE_SECRET_KEY: ${{ secrets.STRIPE_TEST_SECRET_KEY }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build backend image + uses: docker/build-push-action@v6 + with: + context: ./backend + file: ./backend/Dockerfile + load: true + tags: hievents-e2e-backend:ci + cache-from: type=gha,scope=e2e-backend + cache-to: type=gha,scope=e2e-backend,mode=max + + - name: Build frontend image + uses: docker/build-push-action@v6 + with: + context: ./frontend + file: ./frontend/Dockerfile.ssr + load: true + tags: hievents-e2e-frontend:ci + cache-from: type=gha,scope=e2e-frontend + cache-to: type=gha,scope=e2e-frontend,mode=max + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: e2e/package-lock.json + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/package-lock.json') }} + + - name: Run E2E suite + run: ./e2e/run-e2e.sh -- ${{ matrix.pw_args }} + + - name: Collect stack logs on failure + if: failure() + run: docker compose -f "$COMPOSE_FILE" logs --no-color > e2e/compose-logs.txt + + - name: Upload report and artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report-${{ matrix.name }} + path: | + e2e/playwright-report/ + e2e/test-results/ + e2e/compose-logs.txt + retention-days: 14 + + - name: Tear down the stack + if: always() + run: docker compose -f "$COMPOSE_FILE" down -v diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..5473efb0a4 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,108 @@ +name: Backend Tests + +on: + push: + branches: [main, develop, 'v[0-9]+.[0-9]+.[0-9]+-*'] + paths: + - 'backend/**' + - '.github/workflows/tests.yml' + pull_request: + paths: + - 'backend/**' + - '.github/workflows/tests.yml' + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + matrix: + php-versions: ['8.3', '8.4', '8.5'] + + services: + # Postgres is started for the Feature suite. The Unit suite does not need + # a live connection — it only reads DB_DATABASE for the _test guard check + # in CreatesApplication — so it runs in parallel with Postgres warming up. + postgres: + image: postgres:15 + env: + POSTGRES_DB: hievents_test + POSTGRES_USER: hievents + POSTGRES_PASSWORD: hievents + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U hievents -d hievents_test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + # Job-level env. .env.testing supplies the rest, but the DB host on a CI + # runner is 127.0.0.1 (service container exposes its port on the runner), + # not the docker network alias used locally — override here. + env: + DB_HOST: 127.0.0.1 + DB_PORT: 5432 + DB_DATABASE: hievents_test + DB_USERNAME: hievents + DB_PASSWORD: hievents + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, xml, ctype, iconv, intl, pdo, pdo_mysql, pdo_pgsql, pgsql, tokenizer, gd + ini-values: post_max_size=256M, upload_max_filesize=256M + coverage: none + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Create Laravel bootstrap cache directory + run: mkdir -p ./backend/bootstrap/cache && chmod -R 777 ./backend/bootstrap/cache + + - name: Install dependencies + run: cd backend && composer install --prefer-dist --no-progress --no-interaction + + - name: Stage .env for testing + # Laravel auto-loads .env.testing when APP_ENV=testing, but artisan + # commands run outside that flow read .env directly. Copy .env.testing + # to .env so both paths see the same config. + run: cp backend/.env.testing backend/.env + + - name: Run Unit test suite + # Pure unit tests — no DB connection, no migrations. The CreatesApplication + # bootstrap detects the absence of DatabaseTransactions / RefreshDatabase + # traits and skips migrate:fresh entirely. Runs in parallel with the + # Postgres service container coming up. + run: cd backend && ./vendor/bin/phpunit --testsuite=Unit --no-coverage + + - name: Wait for Postgres + run: | + for i in {1..30}; do + if pg_isready -h 127.0.0.1 -p 5432 -U hievents -d hievents_test; then + exit 0 + fi + sleep 1 + done + echo "Postgres did not become ready in time" >&2 + exit 1 + + - name: Run Feature test suite + # Integration tests against the real PostgreSQL test database. The first + # test that boots Laravel triggers migrate:fresh once per process via + # CreatesApplication::ensureTestDatabaseIsMigrated. + run: cd backend && ./vendor/bin/phpunit --testsuite=Feature --no-coverage diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml deleted file mode 100644 index 5e4cdf1c6e..0000000000 --- a/.github/workflows/unit-tests.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Run Unit Tests - -on: - push: - branches: [main, develop] - paths: - - 'backend/**' - pull_request: - paths: - - 'backend/**' - -jobs: - run-tests: - runs-on: ubuntu-latest - - strategy: - matrix: - php-versions: ['8.2', '8.3', '8.4'] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: mbstring, xml, ctype, iconv, intl, pdo, pdo_mysql, tokenizer - ini-values: post_max_size=256M, upload_max_filesize=256M - coverage: none - - - name: Get Composer Cache Directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- - - - name: Create Laravel bootstrap cache directory - run: mkdir -p ./backend/bootstrap/cache && chmod -R 777 ./backend/bootstrap/cache - - - name: Install dependencies - run: cd backend && composer install --prefer-dist --no-progress --no-interaction - - - name: Run PHPUnit Tests - run: cd backend && ./vendor/bin/phpunit tests/Unit --no-coverage diff --git a/.gitignore b/.gitignore index eda9758bce..0ed72ecbb1 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ prompts/ /plans/** /plans + +.claude/worktrees/ +.claude/scheduled_tasks.lock +tmp_translate/ diff --git a/CLAUDE.md b/CLAUDE.md index ed1c6a0eb4..7141894ff6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,8 +43,35 @@ cd docker/development ./start-dev.sh --certs=signed # Signed certs with mkcert ``` +### API smoke-testing (after backend changes) + +Unit tests miss wiring bugs — exercise changed endpoints against the dev stack. Base URL `https://localhost:8443/api` (self-signed — `curl -sk`). Endpoints are defined in `backend/routes/api.php`. + +```bash +# Verified SUPERADMIN account + organizer + LIVE single/recurring events + products +# (paid has waitlist) + promo + affiliate; prints ids and a Bearer token: +docker compose -f docker-compose.dev.yml exec backend php artisan dev:bootstrap + +# Manual token (`token` field → `Authorization: Bearer `): +curl -sk -X POST https://localhost:8443/api/auth/login -H "Content-Type: application/json" -d '{"email":"","password":""}' +``` + +Gotchas: +- Product upsert requires `product_type` (TICKET/GENERAL), `type` (FREE/PAID/DONATION/TIERED) and `product_category_id` +- Promo create requires `applicable_product_ids: []` (empty = all products) +- Public order complete requires `email_confirmation` on the order and on each product entry +- Publishing needs `accounts.account_verified_at`; `/admin/*` needs role `SUPERADMIN` (`dev:bootstrap` handles both) +- Emails land in Mailpit at `http://localhost:8025` (`/api/v1/search?query=to:`) + ## Development Guidelines +### Comments — hard rule for all code (backend, frontend, SCSS) +- **DON'T** add explanatory comments. The code must speak for itself. +- This includes "why" comments justifying a design choice ("X is intentionally omitted because…", "matches the rest of the section rhythm…", "Views live on the per-event-per-day table…", "Online events satisfy the where requirement…"). If you'd write that, rename a variable / extract a function / restructure the code instead, or just leave it implicit. +- Functional annotations are fine: PHPDoc `@throws` / `@return` / `@param`, `// TODO(handle:owner)` linked to a tracked task, schema comments inside SQL migrations that future migrations depend on. +- Never restate what the next line does. If a reviewer can read the diff and understand it, the comment is noise. +- If you're tempted to leave a comment "for the next agent", **don't** — write it as a CLAUDE.md note instead. + ### Backend #### Architecture Flow @@ -58,7 +85,8 @@ cd docker/development - **ALWAYS** wrap all translatable strings in `__()` helper - Domain Objects are auto-generated via `php artisan generate-domain-objects` - never edit manually - **Always** create unit tests for new features in `backend/tests/Unit/` -- **DON'T** add comments unless absolutely necessary +- **DON'T** add comments — see the comments rule above. No exceptions for "this seems useful context". +- **NEVER leave dead code.** Code that has no production callers — unused methods, unused DTO fields, unused constants, columns that are written but never read, classes only called from tests — must be deleted, not left "for future use". This applies to both backend and frontend. If you add a method speculatively, wire it to a real caller in the same change or remove it. The same rule applies after refactors: if something becomes unreferenced, it goes. Confirm with grep before claiming a method or class is reachable. - **ALWAYS** sanitize user-provided content with `HtmlPurifierService` before storing, especially content rendered as HTML #### DTOs @@ -80,6 +108,12 @@ cd docker/development #### Repository Pattern - Favour existing repository methods over creating bespoke ones. E.g., use `findFirstWhere(['event_id' => $eventId])` instead of creating `findByEventId` +- Bespoke repository methods must wrap their query in `runQuery()` — it is the single point that resets `$this->model`/`$this->eagerLoads` after each call +- `increment()`/`decrement()` are `findOrFail`-based and throw for soft-deleted rows; use the where-based `incrementEach()`/`decrementEach()` when the target row may have been deleted (e.g. a promo code deleted after orders used it) + +#### Mail & side effects +- `BaseMail` is queued **and** `afterCommit()` — a mail sent inside a DB transaction that rolls back is silently discarded. Chain `->beforeCommit()` on the mailable when the send must survive a deliberate rollback (e.g. refund-and-reject webhook paths) +- Promo usage, `products.sales_volume` and affiliate sales counters increment only when an order **completes** — any decrement must be gated on `isOrderCompleted()` (or equivalent) to stay symmetric #### Database & Migrations - **DO** use auto-incrementing integer IDs (`$table->id()`), not UUIDs @@ -93,6 +127,9 @@ cd docker/development - **DON'T** use `RefreshDatabase` - use `DatabaseTransactions` instead - Unit tests extend Laravel's TestCase, not PHPUnit's TestCase - Use Mockery for mocking +- **Unit suite (`tests/Unit/`) is for pure isolation tests** — no DB, no HTTP, no real container resolution. If a test uses `DatabaseTransactions`, hits the DB (raw `DB::` calls, factories that persist, repository methods that query), or boots significant framework state, it's an integration test and belongs in `tests/Feature/` (mirror the path, e.g. `tests/Feature/Repository/Eloquent/`). Running `--testsuite=Unit` must stay fast and DB-free. +- Tests run against a dedicated `hievents_test` database, configured via `backend/.env.testing` and enforced by `phpunit.xml`. The local docker-compose creates this database automatically via `docker/development/pgsql-init/`. If your existing pgsql volume predates this script, create the DB once with: `docker compose -f docker-compose.dev.yml exec pgsql psql -U username -d backend -c 'CREATE DATABASE hievents_test OWNER username;'` +- Database name **must end in `_test`**. Enforced globally by a `final` guard in `tests/TestCase.php::guardAgainstNonTestDatabase()` which runs on every test that boots Laravel — no per-test opt-in needed and no way to bypass. ### Frontend @@ -112,6 +149,21 @@ cd docker/development - Use Mantine UI components for UI elements - Prefer SCSS modules over Mantine layout components for layout styling +#### E2E Tests +- There is a Playwright E2E suite in `e2e/` (see `e2e/README.md`). It runs the real stack (Laravel + SSR frontend + Postgres + Redis + Mailpit) in Docker. +- **To test uncommitted changes, run specs against the dev stack** — the hermetic e2e stack bakes source into images and `docker compose up` never rebuilds them. From `e2e/`: + ```bash + E2E_BASE_URL=https://localhost:8443 MAILPIT_URL=http://localhost:8025 E2E_SAAS_MODE=true npx playwright test + ``` + `E2E_SAAS_MODE=true` is required (the dev stack requires email verification; the fixture only confirms via Mailpit in SaaS mode), a queue worker must be running to deliver the verification emails, and superadmin-dependent specs need a one-time `php artisan dev:bootstrap --email=superadmin@e2e.test --password='SuperAdminPass123!'`. See "Against the running dev stack" in `e2e/README.md`. +- **When you add or meaningfully change a user-facing flow, add or update an E2E spec for it where practical.** Follow the existing pattern: arrange data via the API/`factory`, drive only the flow under test through the UI with a thin page object, and assert on real page content (the created/edited item appears), not just a URL change. Tag fast, load-bearing checks with `@smoke`. +- Not everything needs E2E — reserve it for real user journeys (create/edit/complete flows). Pure logic belongs in backend unit/feature tests instead. + +#### Test IDs (E2E) +- Add a `data-testid` to interactive elements the E2E suite needs to drive — primarily **buttons** (open-modal triggers, submit/save), **menu items**, and **custom widgets with no accessible label** (e.g. `CustomSelect`, which takes a `dataTestId` prop that lands on its target and options). This is not required for every element: text inputs with a unique `