# ARB — AI-Driven Marketing Automation

Describe a campaign in chat and let AI generate the ad creative, write the copy, and launch it across Meta, Google, TikTok, Taboola, and LinkedIn. See `docs/TICKETS.md` for the full product spec and `docs/SERP-1827-PLAN.md` for the implementation plan and phase status.

## Tech Stack

| Layer | Choice | Notes |
|---|---|---|
| Language / Framework | PHP 8.5, Laravel 13 | See [Prerequisites](#project-setup-dev) below for the `php8.5`/Composer caveat on this box |
| Frontend interactivity | [Livewire 3](https://livewire.laravel.com/) + [Volt](https://livewire.laravel.com/docs/volt) | Auth pages (`resources/views/livewire/pages/auth/*`) are Volt single-file components; everything else is classic Livewire class + Blade view pairs under `app/Livewire` / `resources/views/livewire` |
| Templating | Blade | `.blade.php` views in `resources/views`, no JSX/Vue/React anywhere |
| CSS/JS | **Bootstrap 5.3.3 via CDN only** | Loaded as plain `<link>`/`<script>` tags in `resources/views/layouts/app.blade.php` and `welcome.blade.php`. **There is no Vite/webpack/npm build step and no `node_modules`.** `package.json` intentionally has no dependencies — don't add a JS bundler or introduce `resources/js`/`resources/css` build entries without discussing it first. Style pages with Bootstrap utility/component classes directly in Blade; use plain `<script>` blocks for any bespoke JS |
| Database | MySQL (`arb` database) | `DB_CONNECTION=mysql`; sessions/cache/queue also use it (`SESSION_DRIVER=database`, `CACHE_STORE=database`, `QUEUE_CONNECTION=database`) |
| AI provider | [`laravel/ai`](https://laravel.com/docs/ai) (OpenAI by default, `config/ai.php`) | Text agents (structured output), `Image::of()->generate()`, `Audio::of()->generate()` (TTS). It does **not** do video generation |
| Auth scaffolding | [Laravel Breeze](https://laravel.com/docs/starter-kits) (Livewire + Volt variant) | Login/register/password-reset/email-verification |
| Queue | Database queue driver | Jobs in `app/Jobs` (e.g. image/video generation) are processed by a short-lived `queue:work --stop-when-empty` invocation kicked off every minute by the scheduler (`routes/console.php`) — there's no Redis/Supervisor/long-running worker daemon in this environment, so `php artisan schedule:run` must be wired to a cron entry (or `schedule:work` in local dev) for jobs to actually process |
| Testing | PHPUnit (`tests/Feature`, `tests/Unit`) | `laravel/ai` ships fakes (`Image::fake()`, `StructuredAnonymousAgent::fake()`) used in `tests/Feature/GenerateImageCreativeJobTest.php` |

## Architecture: contracts, services, and providers

The core design principle is that **every external capability (AI generation, ad-platform API) sits behind a PHP interface (`app/Contracts`)**, with the concrete implementation swappable via config or a service provider binding — never referenced directly by controllers/jobs/Livewire components. This is what lets stub/unimplemented platforms and not-yet-chosen vendors coexist with real ones without special-casing call sites.

```
app/
├── Contracts/          Interfaces only — the extensibility seam (PlatformAdapter, ImageCreativeGenerator, ...)
├── Services/
│   ├── AI/              laravel/ai-backed implementations of the AI contracts (Laravel*Generator/*Resolver/*Synthesizer)
│   └── Platforms/       One adapter per ad platform (MetaAdapter, GoogleAdapter, ...) + PlatformAdapterFactory
├── DTOs/                Plain data objects passed between layers (CreativeBrief, CompetitorAd, VideoRenderResult)
├── Models/              Eloquent models (Creative, Campaign, CampaignCreative, AdCopy, Avatar, Voice, CompetitorAdReference, User)
├── Jobs/                Queued work (GenerateImageCreativeJob, GenerateAdCopyJob) — catch failures, set status=failed + failure_reason, safe to retry
├── Livewire/            Livewire component classes (Forms/, Actions/); Volt pages live only under resources/views/livewire/pages
├── Http/Controllers/    Thin controllers (CreativeController) — validate, delegate to models/jobs, return a view
├── Providers/
│   ├── AppServiceProvider.php      Framework-wide bootstrapping (e.g. Paginator::useBootstrapFive())
│   └── CreativeServiceProvider.php Binds every Contract to its implementation (see below)
└── Exceptions/          PlatformNotImplementedException, ProviderNotConfiguredException, etc.
```

`app/Providers/CreativeServiceProvider.php` binds contracts to implementations, some hard-coded and some config-driven:

```php
$this->app->bind(ImageCreativeGenerator::class, LaravelAiImageGenerator::class);
$this->app->bind(VideoAvatarProvider::class, fn ($app) => $app->make(config('creative.video_provider')));
```

`config/creative.php` is the single place that maps a platform name (`meta`, `google`, ...) to its adapter class, and which vendor backs `video_provider`/`competitor_ad_provider` when one hasn't been chosen yet (currently `Null*Provider` stubs that throw `ProviderNotConfiguredException`). `App\Services\Platforms\PlatformAdapterFactory` resolves `Campaign::platform` to a bound `PlatformAdapter` at runtime via the container.

Video rendering is routed by requested duration via `App\Services\AI\VideoAvatarProviderFactory::forDuration()`: durations at/under `config('creative.veo_max_duration')` (8s) use Google Veo (`video_provider`), longer requests use OpenAI Sora (`video_provider_long`), since Veo hard-caps a single clip at 8s while Sora can reach further with a base clip + one extension call.

## Adding a new module

**New ad platform (e.g. "Snapchat"):**
1. Create `app/Services/Platforms/SnapchatAdapter.php` implementing `App\Contracts\PlatformAdapter` (`createCampaign`, `attachCreative`, `publish`). Until the real API integration is ready, extend `UnimplementedPlatformAdapter` so it throws `PlatformNotImplementedException` cleanly.
2. Register it in `config/creative.php` under `platform_adapters`.
3. No controller/job/Livewire changes needed — `PlatformAdapterFactory::forPlatform('snapchat')` picks it up automatically.
4. Add feature tests mocking the HTTP client (see `MetaAdapter` + its tests for the pattern) — never hit a real ad platform API in tests.

**New AI capability or provider swap:**
1. Define (or reuse) an interface in `app/Contracts/`.
2. Implement it in `app/Services/AI/` (e.g. `LaravelAi*` for `laravel/ai`-backed, or a vendor-named class for a direct HTTP integration).
3. Bind it in `CreativeServiceProvider` (hard binding) or via a new `config/creative.php` key (if the vendor is expected to change).

**New Eloquent model / data:** add a migration under `database/migrations`, the model in `app/Models`, and wire relationships on `User`/`Campaign`/`Creative` as needed — see `docs/SERP-1827-PLAN.md` for the current schema.

**New page/feature (UI):**
- Prefer a Livewire component (`php artisan make:livewire Feature/Name`) for anything with state/interactivity; class goes in `app/Livewire`, view in `resources/views/livewire/...`.
- Use a Volt single-file component only for simple, self-contained pages (follow the existing `resources/views/livewire/pages/auth/*.blade.php` pattern) — otherwise prefer the class-based Livewire style already used elsewhere.
- Style with Bootstrap classes only; extend `layouts/app.blade.php` (authenticated) or `layouts/guest.blade.php` for the shell. Do not add a JS build step.
- Add the route in `routes/web.php` (or a new `routes/*.php` file required from it) behind `['auth', 'verified']` middleware unless it's public.

**New background job:** `php artisan make:job YourJob`; mirror `GenerateImageCreativeJob`'s pattern — wrap provider calls in try/catch, persist a `failed` status + `failure_reason` on the owning model instead of letting the job die silently, and keep it idempotent (safe to re-run on retry).

## Project Setup (Dev)

Steps to get the app running locally after pulling latest changes. This project runs on **PHP 8.5**, and the system's default `composer` binary resolves to an incompatible PHP version — always invoke Composer as `php8.5 /usr/bin/composer` (not plain `composer`).

**No Node/npm, ever.** Styling is [Bootstrap](https://getbootstrap.com/) loaded via CDN `<link>`/`<script>` tags — there is no JS build step, no `package.json` dependencies to install, and nothing to compile. `git pull` and go.

### Quick setup

Run the included script from the project root:

```bash
./setup.sh
```

It installs Composer dependencies, creates `.env`, runs migrations against the MySQL `arb` database, fixes `storage`/`bootstrap/cache` permissions, and clears cached bootstrap files. Overrides:
- `PHP_BIN=/path/to/php8.5 ./setup.sh` if your PHP 8.5 binary isn't named `php8.5`.
- `COMPOSER_BIN=/path/to/composer ./setup.sh` if Composer isn't at `/usr/bin/composer`.
- DB credentials are auto-read from the sysadmin-provided `/usr/share/php/arb_oci.ini` (`[database]` section: `host`/`user`/`pass`/`db`) if present — override the path with `DB_INI=/path/to/file.ini ./setup.sh`.
- Pass `APP_URL`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `GEMINI_API_KEY`, and/or `OPENAI_API_KEY` as environment variables to have the script write them into `.env` for you (explicit shell variables win over the ini file; the MySQL `arb` database must already exist — this script does not create it), e.g.:
  ```bash
  APP_URL="http://localhost:8000" DB_DATABASE=arb DB_USERNAME=dev DB_PASSWORD=secret \
  GEMINI_API_KEY=... OPENAI_API_KEY=... ./setup.sh
  ```

### Manual setup

If you'd rather run the steps yourself (or need to debug a failure in the script):

1. **Install PHP dependencies**
   ```bash
   php8.5 /usr/bin/composer install
   ```

2. **Environment file**
   ```bash
   cp .env.example .env
   php8.5 artisan key:generate
   ```
   Update `APP_URL` in `.env` to match your local dev URL.

3. **Database (MySQL)**
   This project uses MySQL (`DB_CONNECTION=mysql`). Set `DB_HOST`/`DB_PORT`/`DB_DATABASE`/`DB_USERNAME`/`DB_PASSWORD` in `.env` to point at an existing `arb` database (this project does not create the database for you), then migrate:
   ```bash
   php8.5 artisan migrate
   ```

4. **Storage & cache permissions**
   Laravel needs write access to `storage/` and `bootstrap/cache/`. On shared servers the PHP-FPM/web server user (e.g. `nginx`) is often **not** in your user's group, so `755`/`644` isn't enough — the web server can't read or write those paths. If you hit a blank/500 error, run:
   ```bash
   chmod -R 777 storage bootstrap/cache
   ```

5. **Clear stale caches**
   After pulling changes that touch config, routes, or providers, clear cached bootstrap files:
   ```bash
   php8.5 artisan optimize:clear
   ```

6. **Queue worker (scheduler-driven)**
   Queued jobs (image/video generation, ad copy) only run when the scheduler fires — there's no `queue:work` daemon or Supervisor here. In local dev, run the scheduler in the foreground:
   ```bash
   php8.5 artisan schedule:work
   ```
   On a real server, add a cron entry instead:
   ```cron
   * * * * * cd /path/to/project && php8.5 artisan schedule:run >> /dev/null 2>&1
   ```
   See `routes/console.php` for the `withoutOverlapping()`/`max-time` tuning and the known single-worker-serializes-everything limitation.

7. **Verify**
   Visit the app URL in your browser. If it 500s, check `storage/logs/laravel.log` first — with `APP_DEBUG=true` in `.env`, Laravel also renders the error directly in the browser.

## Agentic Development

Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:

```bash
php8.5 /usr/bin/composer require laravel/boost --dev

php8.5 artisan boost:install
```

Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.

## Testing

```bash
php8.5 /usr/bin/composer test
# or directly:
php8.5 artisan test
```

`phpunit.xml` overrides `APP_URL=http://localhost` for the test environment — the real `.env`'s `APP_URL` includes a shared-hosting subpath that breaks relative-path resolution in HTTP feature tests, so don't remove that override. `laravel/ai` fakes (`Image::fake()`, agent fakes) are used to test AI-backed jobs without hitting real providers; do the same for any new `Services/AI` or `Services/Platforms` code — never call a real external API from a test.

## Further Reading

- `docs/TICKETS.md` — full product spec (SERP-1827)
- `docs/SERP-1827-PLAN.md` — architecture and phased build plan, with phase completion status
- `docs/TASKS.md` — longer-term task backlog beyond this ticket
