# Ad Engine — Build Status & Setup

Central multi-property ad automation platform: pulls product/listing data
from each site, generates AI ad copy/creative, launches via Meta and Google
Ads APIs, and tracks performance — all from one dashboard.

## What's built so far

- **Schema** (`database/migrations/2026_07_06_000001_create_ad_engine_tables.php`):
  `properties`, `content_sources`, `campaigns`, `creatives`, `performance_snapshots`
- **Models**: `Property`, `ContentSource`, `Campaign`, `Creative`, `PerformanceSnapshot`
- **Adapter interface** (`ContentAdapterInterface`): the contract every property's
  content source implements — `fetch()` returns a normalized array regardless
  of whether the source is flat-file, MySQL, or REST.
- **Three adapters**:
  - `FlatFileAdapter` — **confirmed against real RDC data** (2026-07-06). Reads
    the single `data/listings.json` array file, filters to listings that are
    `active` + `paid` + not expired (per confirmed eligibility decision), and
    normalizes real field names (`title`, `desc`, `price`, `photos`, etc.)
  - `MysqlDirectAdapter` — queries another Laravel app's table directly
    (ConstructionPlex sites) — **not yet confirmed against real table/column names**
  - `RestApiAdapter` — pulls from a REST API, e.g. WooCommerce (not wired to a live property yet)
- **`ContentSyncService`** — upserts an adapter's output into `content_sources`
- **`php artisan content:sync`** — CLI command to run the sync for one or all active properties
- **`PropertySeeder`** — RDC is `active` with confirmed config; the three
  ConstructionPlex sites remain `not_yet_configured` pending the same real-data check RDC just got

- **Web dashboard**:
  - `DashboardController` — properties list, per-property content view, manual sync trigger
  - Blade views (`resources/views/layouts/app.blade.php`, `dashboard/index.blade.php`, `dashboard/show.blade.php`) — plain CSS, no JS framework, responsive down to phone width
  - `routes/web.php` — dashboard routes, gated behind `auth` middleware

## Site-level ("advertise the website itself") campaigns

Per your decision, RDC ads should include both individual listing promotion
*and* general brand/site awareness campaigns not tied to any one listing.
The schema already supports this — `campaigns.content_source_id` is nullable
specifically for this case. Not yet built: the actual UI/flow for creating a
site-level campaign (it'll reuse the same `campaigns`/`creatives` tables, just
without a `content_source_id`, and the AI copy prompt will need brand voice
notes rather than a specific listing's details). This comes together
alongside AI copy generation.

## Not built yet

- **Login itself** — routes are gated behind Laravel's `auth` middleware, but you still need to install an auth package to get an actual login screen and user table. Recommended: **Laravel Breeze** (see step 3 below) — it's the lightest official option and gives you login/logout/password reset in a few commands, styled with plain Blade so it matches what's already built here.
- AI copy generation (Claude API call)
- Meta Marketing API integration (campaign/ad set/ad creation)
- Google Ads API integration
- Performance pull-back into `performance_snapshots`

## Setup on your VPS

1. **New Laravel project** (separate from your existing sites — this is its own app):
   ```
   composer create-project laravel/laravel adengine
   ```
   Suggested location alongside your other sites, e.g. `/home/www/adengine.racedayclassifieds.com/`
   or its own subdomain — your call on naming.

2. **Copy these files into the new project**, preserving paths:
   - `database/migrations/2026_07_06_000001_create_ad_engine_tables.php`
   - `app/Models/*.php`
   - `app/Http/Controllers/DashboardController.php`
   - `app/Services/ContentAdapters/*.php`
   - `app/Services/ContentSyncService.php`
   - `app/Console/Commands/SyncContentSources.php`
   - `database/seeders/PropertySeeder.php`
   - `resources/views/layouts/app.blade.php`
   - `resources/views/dashboard/*.blade.php`
   - `routes/web.php` (merge with the default one Laravel generates)

3. **Install auth (Laravel Breeze)** — gives you an actual login screen,
   session handling, and a `users` table, matching the Posting Desk pattern
   of gated access:
   ```
   composer require laravel/breeze --dev
   php artisan breeze:install blade
   npm install && npm run build
   php artisan migrate
   ```
   Then create your own login by either registering through the generated
   `/register` route once and deleting that route afterward, or seeding a
   user directly:
   ```
   php artisan tinker
   >>> \App\Models\User::create(['name' => 'Paul', 'email' => 'you@example.com', 'password' => bcrypt('your-password')]);
   ```

4. **Configure `.env`** with a database connection for the ad engine's own tables
   (its `properties`/`content_sources`/etc. — separate from RDC's or ConstructionStore's
   own databases).

5. **Add read-only connections** for each ConstructionPlex site's database in
   `config/database.php`, matching the `connection` names used in `PropertySeeder`
   (`construction_store`, `construction_subfinder`, `construction_classifieds`).
   Use a **dedicated read-only MySQL user** per site rather than reusing that
   site's live app credentials — the ad engine should never be able to write
   into those tables.

6. **Run the migration and seeder**:
   ```
   php artisan migrate
   php artisan db:seed --class=PropertySeeder
   ```

7. **Confirm the real field names for the ConstructionPlex sites.** RDC's
   `adapter_config` is already confirmed against real data — the remaining
   `// CONFIRM` markers in `PropertySeeder` are just for ConstructionStore,
   ConstructionSubFinder, and ConstructionClassifieds. Same process as RDC:
   check each site's actual products/listings table and column names, update
   the seeder, then flip that property's `status` to `active`.

8. **Test the RDC sync now** (its config is already `active`):
   ```
   php artisan content:sync --property=1
   ```
   Check the `content_sources` table for rows, then visit the dashboard in a
   browser to see it rendered. Expect only listings marked `active` and `paid`
   with a non-past `expires` date to show up — that's the confirmed
   eligibility filter working as intended, not a bug.

## Deploying so it's reachable from any PC, iPad, or phone

This is a normal Laravel site, so it goes live the same way your other Laravel
apps do — no separate mobile build required:

1. **Point a subdomain at it** on your Hepsia VPS — e.g. `ads.racedayclassifieds.com`
   or a dedicated domain if you'd rather keep it separate from any one property's
   brand. Set the document root to the project's `public/` folder, same as your
   other Laravel sites.
2. **Enable SSL** on that subdomain (Hepsia's panel has free AutoSSL/Let's Encrypt
   built in for domains/subdomains it manages) — needed both for basic security
   and because Meta/Google OAuth callback URLs generally require HTTPS.
3. **Set `APP_ENV=production` and `APP_DEBUG=false`** in `.env` before making it
   publicly reachable — debug mode left on exposes stack traces to anyone who
   hits an error page.
4. Once that's live, `https://ads.yourdomain.com` logs in and works identically
   whether you're on a desktop browser, an iPad, or your phone in the RV — same
   login, same session, same data, just responsive layout adjusting to screen
   width. No app-store install, no separate build per device.

## Recommended next step

Deploy to nascarpenter.net, run `php artisan content:sync --property=1` for
RDC, and confirm real listings (filtered to active+paid+not-expired) show up
correctly in the dashboard. Once that loop is proven end-to-end, move on to
either (a) confirming the ConstructionPlex field names the same way RDC's
were just confirmed, or (b) starting AI copy generation.
