# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

Laravel 13 (PHP 8.3) application that powers the **QuitSure Partner** product — a partner-facing admin panel plus a `/partnerApi/*` REST surface. Partners (e.g. Gympass, Bajaj Health) use the API to provision/cancel users; an internal admin team uses the web UI to inspect users, programs, and subscriptions.

## Development Environment

See `CLAUDE.local.md` for developer-specific paths, container commands, and setup instructions.

## Common commands

```bash
# install
composer install
npm install
php artisan key:generate

# all-in-one dev stack (server + queue listener + pail + vite)
composer dev

# individual processes
php artisan serve
npm run dev                 # vite dev
npm run build               # vite production build
node copy-tinymce-assets.js # one-time copy of TinyMCE skins/models/icons to public/vendor/tinymce

# tests
php artisan test                                 # all suites
php artisan test --testsuite=Unit                # one suite (Unit | Feature)
php artisan test --filter=ExampleTest::test_xyz  # one test

# style + static analysis
./vendor/bin/pint                       # Laravel Pint formatter
./vendor/bin/phpstan analyse            # Larastan, level 5, scopes app/ + routes/ (see phpstan.neon)
```

Tests run with `APP_ENV=testing`, `QUEUE_CONNECTION=sync`, `MAIL_MAILER=array`, `CACHE_STORE=array` (see `phpunit.xml`). The DB connection is **not** overridden to sqlite — tests hit whatever `DB_CONNECTION` resolves to in the env.

## Architecture

### Request flow: Controller → Service → Query → Model

The codebase enforces a strict layering that is unusual enough to call out:

- `app/Http/Controllers/{,Api/}` — thin; mostly parse request, delegate to a service, return JSON via `App\Traits\ApiResponse` or a Blade view.
- `app/Services/*` — business logic. Services are constructor-injected with the Query objects they need (see `UserService` — it takes ~17 Query classes). Don't bypass services to call models from controllers.
- `app/Http/Queries/*Query.php` — Eloquent query builders that own all DB access for a given model/table. New DB reads/writes belong in a Query class, not inline in a service or controller.
- `app/Models/*` — Eloquent models over a **legacy schema**: hungarian-prefix columns (`iUserID`, `vEmail`, `dLastLogin`, `bDeleted`), tables prefixed `tbl_`, custom `$primaryKey`, `$timestamps = false`. Don't expect Laravel's `id` / `created_at` conventions.
- `app/Http/Controllers/Api/BaseApiController.php` — API controllers extend this; it provides `getPostData()` (reads raw `php://input`, lowercases `vEmail`/`email`) and `getPostHeaders()`.

### Per-program "days" tables

Programs each have their own day table named `tbl_{iProgramID}Days` (e.g. `tbl_1Days`, `tbl_2Days`). To read days for a user, the code instantiates the `Day` model and rebinds the table at runtime — see `UserQuery::getUserDetail()`. Anything touching day data must do the same.

### Multiple database connections

Defined in `config/database.php`:

- default (`mysql`) — partner app DB
- `logs` — separate DB for the `partner_logs` table written by `App\Logging\DatabaseLogger`
- `qsuserinfo` — shared cross-app user info DB

When writing queries, pick the connection deliberately. Default Eloquent calls hit the default connection.

### Routing & middleware

Middleware aliases are registered in **`bootstrap/app.php`** (Laravel 11 style). `app/Http/Kernel.php` also exists with the same aliases but is the legacy form — `bootstrap/app.php` is authoritative.

Custom auth middleware, used in different combinations per route group in `routes/api.php`:

| Alias | Purpose |
|---|---|
| `digest.auth` | HTTP Digest, users from `config/digest.php` (env `DIGEST_USERS=user:pass,user2:pass2`) |
| `api.auth` | Validates `accesskey` + `platform` headers via `UserService::validateAccessToken` against `tbl_Partner` |
| `api.key.auth` | Per-partner API key |
| `bearer.token.auth` | Per-partner OAuth-style bearer token (issued by `GympassApiController::getAccessToken`) |
| `gympass.signature` | Verifies Gympass webhook HMAC signature |
| `user.language` | Auto-sets app locale from the user's program — see "Localization" below |
| `admin` | Web admin guard (`auth('admin')`) |

Web fallback route redirects authenticated admins to `/dashboard`, others to `/login`.

### Localization

`SetUserLanguageMiddleware` runs after auth, fetches the user's program language (`tbl_Programs.vLanguage`), and calls `app('translator')->setLocale(...)`. Once that middleware is on the route, **call `trans('form_lang.X')` directly** — do not re-fetch the language inside services. Default fallback is `'eng'`. See `docs/global-language-system.md`.

### Logging

Default channel is `stack` = `[database, single, daily, error_file]`. The `database` channel is the custom `App\Logging\DatabaseLogger` which:

- Writes every record to `partner_logs` on the `logs` connection.
- Adds a `source` context entry with class/function/file/line. For exceptions it captures the **original throw site** (via `$exception->getFile()/getLine()`), not the `Log::error()` call site — pass the exception in context: `Log::error($e->getMessage(), ['exception' => $e])`. Details in `docs/enhanced-error-logging.md` and `docs/enhanced-logging.md`.

### Email (Postmark)

Use the `Postmark` facade or `sendTemplateEmailPostMark()` / `sendWelcomeEmail()` helpers. Named templates live in `config/postmark.php` under `templates`. `PostmarkService::sendPredefinedTemplate('welcome', [...])` is the preferred entry point. See `docs/postmark-email-service.md`.

### Helpers (auto-loaded)

`app/Helpers/CommonHelper.php` is registered in `composer.json` `autoload.files` and is always available. Notable functions:

- `safe_env($key, $default)` — like `env()` but logs + emails `config('constants.error_email_recipient')` when the key is missing. Prefer it for any required config read at runtime.
- `getActiveDayEnv($iProgramID)` — looks up `config('constants.active_days')`, then falls back to `safe_env("ACTIVE{$iProgramID}DAYID")`.
- `front_url`, `front_asset`, `front_css`, `front_js`, `front_image` — wrappers over `FrontAssetService` for cross-app asset/URL building (the partner app links to assets owned by the main QuitSure web app).

### Frontend

Vite + Bootstrap 5 + jQuery + DataTables + Chart.js + Highcharts + Fancyapps. Theme is **Gentelella** (Bootstrap admin theme). Manual chunking is configured in `vite.config.js`; if you add a heavy vendor lib, add it to `manualChunks`. Blade views live in `resources/views`; lang files in `resources/lang`.

## Conventions to preserve

- **Always use a Query class for DB access — never call an Eloquent model directly from a service or controller.** Any `Model::create()`, `Model::where()`, `Model::find()`, etc. belongs in an `app/Http/Queries/*Query.php` method (e.g. `DeepLinkQuery::createDeepLink()`, `DeepLinkQuery::codeExists()`). Inject the Query class via the constructor and call it; do not `use App\Models\...` inside a service for querying.
- New business logic goes in a `Service`, with `*Query` deps injected via the constructor.
- API responses use `App\Traits\ApiResponse` (`response()`, `successResponse()`, `errorResponse()`).
- Don't introduce `created_at`/`updated_at` to existing legacy tables — most models set `$timestamps = false`.
- Production-only / non-prod-only routes are guarded with `app()->isProduction()` checks (see `routes/web.php` and `routes/api.php`). Mirror that pattern for any debug endpoints.
- When logging exceptions, always pass `['exception' => $e]` so the database logger captures the real throw site.
