# CLAUDE.md — WhatsApp Cloud API Project Tracker

> This file is maintained by Claude. **Update it every time files are created, modified, or deleted.**
> Last updated: 2026-03-27 (API token system, user panel, REST API)

---

## Project Overview

- **Framework**: Laravel 13 (PHP 8.3+)
- **Project root**: `/Applications/XAMPP/xamppfiles/htdocs/WhatsApp/whatsappcloudapi/`
- **DB host**: `157.180.4.221` — `databaseoca_whatsapp` (user + db name same)
- **DB password**: set in `.env`
- **App URL**: `http://localhost:8000` — run with `php artisan serve`
- **Test login**: `admin@admin.com` / `admin123`
- **Human README**: see `README.md` in same folder

---

## Critical Rules (never break these)

1. **FK order when creating Company + User**: always create `User` first (company_id null), then `Company` with user_id, then update User's company_id. Reverse order = SQLSTATE[23000] FK violation.
2. **Read before Edit/Write**: always read a file with the Read tool before using Edit or Write on it.
3. **Period ENUM values**: `1month`, `3months`, `6months`, `1year` — NOT `monthly`/`annually`.
4. **Settings save**: always updates both `settings` table AND `.env` via `SettingsController::syncEnv()`.
5. **Plugin chips** in pricing plans: use `data-key` attribute + `e.preventDefault()` to avoid double-toggle from label/checkbox default behavior.

---

## All Files

### Controllers (`app/Http/Controllers/`)

| File | Key Methods | Notes |
|------|-------------|-------|
| `Auth/LoginController.php` | `showLoginForm`, `login`, `logout` | redirects to dashboard if already logged in |
| `Auth/RegisterController.php` | `showRegistrationForm`, `register` | DB::transaction — User first, then Company; auto-sets role='user' + api_token |
| `DashboardController.php` | `index` | returns admin dashboard view with $user |
| `CompaniesController.php` | `index`, `store`, `destroy` | search+filter+paginate; store uses DB::transaction; auto-sets role='user' + api_token |
| `PricingPlansController.php` | `index`, `store`, `destroy`, `toggleActive` | passes $apps to view for live plugin chips |
| `AppsController.php` | `index`, `store`, `destroy`, `toggle` | freeApps + premiumApps queries |
| `SettingsController.php` | `index`, `update`, `syncEnv` | index parses laravel.log into structured entries; update writes to DB + .env |
| `UserDashboardController.php` | `index`, `whatsappSetup`, `saveWhatsappSetup`, `apiDocs` | User panel — auto-generates api_token if null |
| `WebhookController.php` | `verify` (GET), `handle` (POST) | Public — no auth session; verified via ?token= query param; uses netflie/whatsapp-cloud-api |
| `Api/MessageController.php` | `sendText`, `sendTemplate` | Sends via WhatsApp Cloud API using user's access_token + phone_number_id |
| `Api/ContactController.php` | `index`, `store` | Stub — contacts table not built yet |
| `Api/CampaignController.php` | `index`, `store` | Stub — campaigns table not built yet |

### Middleware (`app/Http/Middleware/`)

| File | Purpose |
|------|---------|
| `AdminMiddleware.php` | alias `admin` — allows superadmin + admin roles; redirects user role to user.dashboard |
| `UserMiddleware.php` | alias `role.user` — allows user role only |
| `ApiTokenMiddleware.php` | alias `api.token` — reads `?token=` query param, finds user by api_token, returns 401 JSON if invalid |

### Models (`app/Models/`)

| File | Key Details |
|------|-------------|
| `User.php` | 25+ fillable columns incl. phone_number_id, waba_id, verify_token, api_token; hidden: password, token, 2FA |
| `Company.php` | SoftDeletes; belongsTo(User); fillable excludes subdomain |
| `PricingPlan.php` | `getFeaturesArrayAttribute()` splits comma features; price cast decimal:2 |
| `App.php` | app_key unique; is_paid/is_active boolean cast; price decimal:2 |
| `Setting.php` | Primary key = `key` (string); `get($key, $default)` + `set($key, $value)` static helpers; `updateOrCreate` |

### Views (`resources/views/`)

| File | Description |
|------|-------------|
| `auth/login.blade.php` | Split layout: dark left (blobs + features), white right (form) |
| `auth/register.blade.php` | Same split layout; 2-column grid form; body overflow:hidden |
| `dashboard.blade.php` | 12 stat cards (3 rows×4), 5 Chart.js charts, sidebar with exit animation — admin panel |
| `companies.blade.php` | Table + search + paginate + 780px slide drawer (6 tabs) + add/delete modals |
| `pricing-plans.blade.php` | Cards grid + summary table; add modal with live app chips from DB; FontAwesome CDN |
| `apps.blade.php` | Free/Premium sections; app cards; add modal with icon preview + color picker |
| `settings.blade.php` | 7-tab settings panel; System/Storage/Language/Notifications/Sending/Logs/Activity; .env sync |
| `user/dashboard.blade.php` | User panel: black header, greeting card, 3-col layout, resource usage grid, metrics grid, search overlay |
| `user/whatsapp-setup.blade.php` | 3-step WhatsApp Business API setup: webhook URL + verify token (Step 1), access_token (Step 2), phone_number_id + waba_id (Step 3) |
| `user/api-docs.blade.php` | API documentation: dark green token card, all 6 endpoints with ?token= auth, colored sections, copy curl examples |
| `user/partials/sidebar.blade.php` | Shared sidebar for all 3 user pages; active state via Route::currentRouteName(); green company badge |

### Migrations (`database/migrations/`)

| File | Table | Key Columns |
|------|-------|-------------|
| `0001_01_01_000000` | users | 25+ columns including company_id FK, plan_id, access_token, 2FA |
| `0001_01_01_000001` | cache | — |
| `0001_01_01_000002` | jobs | — |
| `2026_03_26_172321` | companies | name, logo, cover, lat, lng, address, currency, description, user_id FK, deleted_at |
| `2026_03_26_212107` | pricing_plans | name, description, features, price, period ENUM, payment_processor, 3 limits, plugins, is_active |
| `2026_03_26_213433` | apps | app_key (unique), display_name, description, icon, color, category, page_link, is_paid, price, currency, is_active |
| `2026_03_26_213434` | — | ALTER pricing_plans.period ENUM to 1month/3months/6months/1year |
| `2026_03_26_215843` | settings | key (primary string), value (text), seeded with 26 default rows |
| `2026_03_27_033118` | users (ALTER) | Added phone_number_id, waba_id, verify_token columns |
| `2026_03_27_034522` | users (ALTER) | Added api_token varchar(20) unique nullable; seeds existing users with random tokens |

### Routes (`routes/web.php` + `routes/api.php`)

```
── Public (no auth) ──
GET  /                           → redirect login
GET  /login                      LoginController@showLoginForm        (login)
POST /login                      LoginController@login
POST /logout                     LoginController@logout               (logout)
GET  /register                   RegisterController@showRegistrationForm (register)
POST /register                   RegisterController@register
GET  /webhook/whatsapp           WebhookController@verify             (token via ?token=)
POST /webhook/whatsapp           WebhookController@handle             (no CSRF, token via ?token=)

─── middleware: auth + admin (superadmin/admin) ───
GET    /dashboard                DashboardController@index            (dashboard)
GET    /companies                CompaniesController@index            (companies)
POST   /companies                CompaniesController@store            (companies.store)
DELETE /companies/{company}      CompaniesController@destroy          (companies.destroy)
GET    /pricing-plans            PricingPlansController@index         (pricing-plans)
POST   /pricing-plans            PricingPlansController@store         (pricing-plans.store)
DELETE /pricing-plans/{plan}     PricingPlansController@destroy       (pricing-plans.destroy)
PATCH  /pricing-plans/{plan}/toggle PricingPlansController@toggleActive (pricing-plans.toggle)
GET    /apps                     AppsController@index                 (apps)
POST   /apps                     AppsController@store                 (apps.store)
DELETE /apps/{app}               AppsController@destroy               (apps.destroy)
PATCH  /apps/{app}/toggle        AppsController@toggle                (apps.toggle)
GET    /settings                 SettingsController@index             (settings)
POST   /settings                 SettingsController@update            (settings.update)

─── middleware: auth + role.user (user role) ───
GET  /home                       UserDashboardController@index        (user.dashboard)
GET  /whatsapp-setup             UserDashboardController@whatsappSetup (user.whatsapp-setup)
POST /whatsapp-setup             UserDashboardController@saveWhatsappSetup (user.whatsapp-setup.save)
GET  /api-docs                   UserDashboardController@apiDocs      (user.api-docs)

─── REST API (routes/api.php, prefix /api, middleware: api.token via ?token=) ───
POST /api/v1/messages/send-text      Api\MessageController@sendText
POST /api/v1/messages/send-template  Api\MessageController@sendTemplate
GET  /api/v1/contacts                Api\ContactController@index
POST /api/v1/contacts                Api\ContactController@store
GET  /api/v1/campaigns               Api\CampaignController@index
POST /api/v1/campaigns               Api\CampaignController@store
```

---

## Sidebar Structure

### Admin sidebar (inline in each admin view)
```
MAIN      Dashboard → route('dashboard')
MGMT      Subscriptions (soon) / Companies / Pricing Plans
EXPLORE   Today Messages (soon) / Today Campaigns (soon) / Apps
SYSTEM    Settings
```

### User sidebar (`user/partials/sidebar.blade.php` — shared partial)
```
MAIN      Dashboard → route('user.dashboard')
MESSAGING Inbox (soon) / Send Message (soon) / Campaigns (soon) / Reply Bot (soon)
TOOLS     Templates (soon) / Contacts (soon) / Reports (soon)
ACCOUNT   API Docs → route('user.api-docs') / WhatsApp Setup → route('user.whatsapp-setup')
          Billing (soon) / Settings (soon)
```
- Active state: `Route::currentRouteName()` match → green background `#25d366`, black text
- Company badge at top: green gradient, shows WhatsApp connected/disconnected dot
- `$waConnected = !empty($user->access_token)` computed inside partial via `@php`

---

## Page Transition Animation

Every page uses exit-then-navigate:
```javascript
document.querySelectorAll('.nav-item').forEach(el => {
    el.addEventListener('click', function(e) {
        const href = this.getAttribute('href');
        if (!href || href === '#') return;
        e.preventDefault();
        document.querySelector('main').style.cssText = 'margin-left:248px;flex:1;padding:1.75rem 2rem;animation:fadeOut .18s ease both';
        setTimeout(() => { window.location.href = href; }, 180);
    });
});
```

Enter animation: `main { animation: fadeIn .18s ease both }` — keyframe from `opacity:0; translateY(8px)` to normal.

---

## Settings → .env Sync Map

`SettingsController::syncEnv()` writes these `.env` keys on every save:

| Setting DB key       | .env key               |
|----------------------|------------------------|
| project_name         | APP_NAME               |
| site_url             | APP_URL                |
| app_debugging        | APP_DEBUG (true/false) |
| timezone             | APP_TIMEZONE           |
| file_storage         | FILESYSTEM_DISK        |
| aws_access_key       | AWS_ACCESS_KEY_ID      |
| aws_secret_key       | AWS_SECRET_ACCESS_KEY  |
| aws_region           | AWS_DEFAULT_REGION     |
| aws_bucket           | AWS_BUCKET             |
| aws_bucket_url       | AWS_URL                |
| pusher_app_id        | PUSHER_APP_ID          |
| pusher_app_key       | PUSHER_APP_KEY         |
| pusher_app_secret    | PUSHER_APP_SECRET      |
| pusher_app_cluster   | PUSHER_APP_CLUSTER     |
| broadcast_driver     | BROADCAST_CONNECTION   |
| msg_sending_mode     | — (DB only)            |
| async_sending_mode   | — (DB only)            |
| campaign_batch_size  | — (DB only)            |

After writing, `@unlink(bootstrap/cache/config.php)` deletes the compiled config cache so changes are live immediately.

---

## Message Sending Settings (CRITICAL — check on every send)

These 3 settings in the `settings` DB table control how ALL messages are sent. **Every message-sending feature must read these before dispatching.**

| Setting DB key        | Values                  | Default    | Meaning |
|-----------------------|-------------------------|------------|---------|
| `msg_sending_mode`    | `cronjob` / `database`  | `database` | Background sending: via server cron or Laravel queue |
| `async_sending_mode`  | `instant` / `database`  | `database` | Per-message dispatch: sync inline or push to queue |
| `campaign_batch_size` | integer 1–150           | `50`       | Messages per batch during campaign processing |

### How to use when implementing a send feature

```php
$sendMode  = Setting::get('msg_sending_mode',    'database'); // 'cronjob' or 'database'
$asyncMode = Setting::get('async_sending_mode',  'database'); // 'instant' or 'database'
$batchSize = (int) Setting::get('campaign_batch_size', 50);   // max 150

// Single message dispatch
if ($asyncMode === 'database') {
    // Push to Laravel queue
    SendMessageJob::dispatch($message);
} else {
    // Instant sync send (blocks request)
    $whatsappService->send($message);
}

// Campaign batch processing
$batches = array_chunk($recipients, $batchSize);
foreach ($batches as $batch) {
    if ($sendMode === 'database') {
        // Queue each batch as a job
        ProcessCampaignBatchJob::dispatch($batch);
    } else {
        // Cronjob: schedule:run will process this
        CampaignBatch::create(['recipients' => json_encode($batch), 'status' => 'pending']);
    }
}
```

### Rules
- **Never hardcode `queue` or `sync`** — always read from settings
- `campaign_batch_size` is server-validated max 150 (clamped in `SettingsController::update`)
- Settings tab: Settings → ✉️ Sending (panel id: `panel-sending`)

---

## Log Parser (`SettingsController::index`)

Reads `storage/logs/laravel.log`. Splits on `[YYYY-MM-DD HH:mm:ss]` boundaries. Per chunk:
- Line 0: regex extracts timestamp, level, message
- Strips `{"exception":...}` JSON context from message
- Lines 1+: stack trace
Returns up to 100 most recent entries, newest first.

View renders as flat colored cards (no dropdown). Level colors: ERROR=red, WARNING=yellow, INFO=green, DEBUG=blue. Filter buttons + search bar filter in JS without page reload.

---

## API Token System

Single `api_token` field (20-char uppercase alphanumeric) on the `users` table. Used for THREE things:

| Use | Where | How |
|-----|-------|-----|
| Webhook URL token | `/webhook/whatsapp?token=` | Meta calls this to verify + deliver messages |
| Webhook verify token | Shown in WhatsApp Setup Step 1 | Must match what you enter in Meta dashboard |
| REST API auth | `/api/v1/*?token=` | All API endpoints read this via `ApiTokenMiddleware` |

**NEVER confuse with `access_token`** — `access_token` is the Facebook Permanent Access Token for WhatsApp Cloud API (stored separately, never exposed in UI).

Auto-generated in `UserDashboardController::whatsappSetup()` and `::apiDocs()` if null. Also auto-set on register and company creation.

---

## Known Issues / TODO

- [ ] User dashboard stat cards show static 0 — wire to real DB queries
- [ ] Admin dashboard stat cards show static data — wire to real DB queries
- [ ] Subscriptions page not built
- [ ] Inbox, Send Message, Campaigns, Reply Bot, Templates, Contacts, Reports, Billing, Settings (user panel) — all "soon"
- [ ] Contacts table not built — Api\ContactController returns stub response
- [ ] Campaigns table not built — Api\CampaignController returns stub response
- [ ] Webhook handle() only logs incoming messages — no processing
- [ ] Edit plan modal shows `alert('coming soon')` — not yet built
- [ ] Edit app functionality not yet built
- [ ] Activity Logs tab is Coming Soon placeholder
- [ ] Companies drawer tabs (Subscriptions, Apps, Payments, Campaigns, Messages) show placeholder content

---

## Changelog

| Date       | Change |
|------------|--------|
| 2026-03-26 | Initial setup: DB connected, migrations ran |
| 2026-03-26 | Login, Register, Dashboard created (split layout, animated blobs) |
| 2026-03-26 | Companies page: table, drawer 6 tabs, add modal, FK order fix |
| 2026-03-26 | Pricing Plans: cards + table, add modal, period ENUM fix |
| 2026-03-26 | Apps: free/premium, add modal, FontAwesome icon preview |
| 2026-03-26 | Pricing plan plugins: hardcoded chips → live from apps DB |
| 2026-03-27 | Settings page: 6 tabs, System/Storage/Locale/Pusher/Logs/Activity |
| 2026-03-27 | Settings syncs to .env on save + clears config cache |
| 2026-03-27 | App Logs: flat card list, color-coded, filter + search |
| 2026-03-27 | Storage section: local info box, S3 info box + composer command |
| 2026-03-27 | README.md created (human-readable), CLAUDE.md fully rewritten |
| 2026-03-27 | App Logo upload: stored in storage/app/public/uploads/, public/uploads symlink, shown in all headers + login/register |
| 2026-03-27 | Header: logo only (no app name text); user avatar initials circle in header |
| 2026-03-27 | Settings save toast popup (bottom-right, auto-dismiss 4s) |
| 2026-03-27 | Sending Messages settings tab: msg_sending_mode, async_sending_mode, campaign_batch_size (max 150) |
| 2026-03-27 | User panel: dashboard redesign (black header, greeting card, 3-col layout, resource/metrics grids) |
| 2026-03-27 | User panel: shared sidebar partial (user/partials/sidebar.blade.php) with route-based active state |
| 2026-03-27 | WhatsApp Setup page: 3-step guide, webhook URL + verify token copy, access_token save, phone_number_id + waba_id |
| 2026-03-27 | Webhook controller: GET verify + POST handle using netflie/whatsapp-cloud-api WebHook class |
| 2026-03-27 | Migrations: added phone_number_id, waba_id, verify_token, api_token to users table |
| 2026-03-27 | Unified api_token: single token for webhook URL, verify token, and API bearer auth |
| 2026-03-27 | API Docs page: dark green token card, 6 documented endpoints, copy curl examples, token show/hide |
| 2026-03-27 | REST API: routes/api.php with 6 endpoints under /api/v1, all protected by ?token= query param |
| 2026-03-27 | ApiTokenMiddleware: verifies ?token= on all /api/v1/* requests, returns 401 JSON if invalid |
| 2026-03-27 | Api controllers: MessageController (sendText, sendTemplate), ContactController (stub), CampaignController (stub) |
| 2026-03-27 | API Docs updated: auth method changed from Bearer header to ?token= query param; paths/params match actual controllers |
