# Phase 6 — Verification Runbook

This refactor is **destructive** (drops columns, renames pivots, re-points FKs). Code changes assume the migrations have run. Run the steps below in order.

> **Before you start:** take a database dump. The dropped columns and renames are reversible at the schema level (every migration has `down()`), but historical values in dropped columns are not recoverable from the down migration alone.

---

## Step 1 — Backup

```bash
mysqldump -uroot -p numu_angels > backup-pre-refactor-$(date +%Y%m%d-%H%M).sql
```

---

## Step 2 — Apply the new migrations

11 new migrations are queued (run `php artisan migrate:status | grep 2026_05_12_` to confirm). Apply them:

```bash
php artisan migrate
```

Expected output: 11 `Migrated:` lines. If any FAIL:
- The migrations are split per-concern → you can rollback only the failed one with `php artisan migrate:rollback --step=N`.
- The most likely failure points are 2026_05_12_100002 (city/country backfill — non-fatal warnings on unmatched rows are OK) and 2026_05_12_100004 (group_id re-point — see step 3 below if any row was left orphaned).

---

## Step 3 — Verify the new schema

```bash
php artisan tinker
```

In tinker:

```php
// Cities + countries
\App\Models\Country::count();           // expect 8
\App\Models\City::count();              // expect ~28

// Groups table refactor
\App\Models\Group::ofType('startup')->count();   // expect 9
\App\Models\Group::ofType('investor')->count();  // expect 7

// Drop confirmation — should all be null/throw
Schema::hasColumn('investors', 'solvency_option_id');   // false
Schema::hasColumn('investors', 'tier_option_id');       // false
Schema::hasColumn('startups',  'event_link');           // false

// New columns
Schema::hasColumn('investors', 'previous_investments_details');  // true
Schema::hasColumn('investors', 'demo_date');                     // true
Schema::hasColumn('investors', 'template_name_option_id');       // true
Schema::hasColumn('investors', 'investor_bio');                  // true
Schema::hasColumn('investors', 'linkedin_profile');              // true
Schema::hasColumn('investors', 'referral_source_option_id');     // true

// Renamed pivots
Schema::hasTable('investor_payment_methods');       // true (was `methods`)
Schema::hasTable('startup_geographic_focuses');     // true (was `startup_locations`)
Schema::hasTable('startup_member_positions');       // true (was `members_positions`)

// Tags table normalized
Schema::hasTable('tags');                                          // true
Schema::hasColumn('investor_tags', 'tag_id');                      // true
Schema::hasColumn('investor_tags', 'monday_tag_id');               // false (dropped)
```

If any of the new columns/tables are missing, that migration failed silently — re-run `php artisan migrate:status` and look for the offending Pending row.

---

## Step 4 — Refresh the seeded label catalog

The label `key` values were re-keyed by migration `2026_05_12_100011`, but if you ran `migrate:fresh --seed` you also need the updated seeder content:

```bash
php artisan db:seed --class=Database\\Seeders\\MondayLabelsSeeder
php artisan db:seed --class=Database\\Seeders\\MondayLabelOptionsSeeder   # if present
```

Then verify:

```php
\App\Models\Label::where('type', 'investor')->where('key', 'payment_method')->exists();         // true
\App\Models\Label::where('type', 'investor')->where('key', 'referral_source')->exists();        // true
\App\Models\Label::where('type', 'investor')->where('key', 'exit_experience')->exists();        // true
\App\Models\Label::where('type', 'investor')->where('key', 'expected_annual_investments')->exists(); // true
\App\Models\Label::where('type', 'investor')->where('key', 'investment_experience_level')->exists(); // true
\App\Models\Label::where('type', 'investor')->where('key', 'template_name')->exists();           // true
\App\Models\Label::where('type', 'startup' )->where('key', 'previously_received_funding')->exists(); // true
\App\Models\Label::where('type', 'startup' )->where('key', 'referral_source')->exists();         // true
```

---

## Step 5 — Sanity-check the column allow-list

The single source of truth is `config/monday_columns.php`. Confirm the registry exposes the right counts:

```bash
php -r "
require 'vendor/autoload.php';
\$app = require_once 'bootstrap/app.php';
\$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();
use App\\Services\\Monday\\Mapping\\ColumnRegistry;
echo 'Startups item:    '.count(ColumnRegistry::columnIds('startups')).PHP_EOL;
echo 'Startups subitem: '.count(ColumnRegistry::subitemColumnIds('startups')).PHP_EOL;
echo 'Investors:        '.count(ColumnRegistry::columnIds('investors')).PHP_EOL;
"
```

Expected: **47 / 8 / 32** (Phase 1 baseline).

---

## Step 6 — End-to-end Monday sync smoke test

Run a small batch of each board to verify the GraphQL `column_values(ids: $columnIds)` filter is in place:

```bash
php artisan monday:preview-startups  --limit=2
php artisan monday:preview-investors --limit=2
```

These commands now go through the same `StartupItemMapper` / `InvestorItemMapper` the real sync uses. They should print clean rows with the new column names and produce **zero 429s** because the column surface is now narrow.

Then run a small real sync:

```bash
php artisan monday:sync-investors --limit=5
```

Verify the stats table includes `payment_methods_inserted` / `tags_inserted` (renamed counters).

If clean, kick off the full async sync from the dashboard at `/admin/sync/v2` (or whichever route your project uses) and watch the `sync-fetch` queue. Expect `pages_total` to climb without 429 errors.

---

## Step 7 — File re-download (one-time)

The new path layout is `storage/app/public/startups/{id}/logo.{ext}` / `pitch_deck.{ext}`. Existing files still live at the old random-filename paths. Migrate them:

```bash
# Dry-run first — see what would change
php artisan monday:refetch-startup-files --dry-run

# One startup to validate
php artisan monday:refetch-startup-files --startup=15

# All startups (asks for confirmation)
php artisan monday:refetch-startup-files
```

The command queues `IngestAssetJob` on `sync-files`. Run the worker:

```bash
php artisan queue:work --queue=sync-files --once=false
```

Verify files appear at the new paths:

```bash
ls storage/app/public/startups/15/
# expect: logo.png  pitch_deck.pdf  other/ (if applicable)
```

Open `/admin/startups/15` in a browser and confirm the file preview/download links work.

---

## Step 8 — Public signup forms

These were the riskiest part of the refactor (column renames behind unchanged form inputs). Test both forms end-to-end:

1. Go to `/startup_signup` in a private browser window.
2. Fill out the form completely (logo + pitch deck + at least one team member). Submit.
3. Confirm:
   - Redirect goes to the scheduling page (no 500 error).
   - In `tinker`: the newest `Startup` row has `source='dashboard'`, `group_id` pointing at the "new" startup group, `startup_brief`/`investment_opportunity`/`kpi_targets` populated.
   - `Startup::latest()->first()->geographicFocuses` is empty (form didn't ask for it — fine).
   - `Startup::latest()->first()->files` has 2+ rows (logo + pitch_deck + any other files), paths follow the new deterministic layout.
4. Repeat for `/investor_signup`. Confirm:
   - `Investor::latest()->first()->investor_bio` populated (was `about`).
   - `Investor::latest()->first()->linkedin_profile` populated (was `linkedin_url`).
   - `Investor::latest()->first()->paymentMethods` (when "yes" angel-invested) has the chosen rows.
   - `Investor::latest()->first()->group_id` points at the "new" investor group row.

If submission 500s: tail `storage/logs/laravel.log` for the column name in the error — that's a rename I missed.

---

## Step 9 — Admin screens

Visit:

- `/admin/startups` — list renders, sort by `valuation_amount_sar` works.
- `/admin/startups/{id}` — every section renders (no `null on object` errors).
- `/admin/investors` — list renders.
- `/admin/investors/{id}` — every section renders.
- Inline label edits — change one field on each page, confirm it persists (`PATCH /admin/{type}/{id}/label`).
- `/admin/sync/v2` — kick off a sync, watch progress.

---

## Step 10 — Final grep verification

```bash
# Should return zero hits across app code (only migration history, lang files,
# storage/compiled-views, and FormRequests w/ Q8 form-input names allowed):
grep -RIn -E 'compelling_factors|company_brief|kpi_with_funding|valuation_formula|prior_financing_option_id|heard_about_option_id|how_heard_option_id|has_exit_option_id|investment_knowledge_option_id|expected_yearly_investments_option_id|ticket_size_sar|total_investment_sar|full_en_name|previous_round_count|follow_up_notes|event_(date_text|hour|link)|monday_(investor|deal|demo|nominated|wishlist|portfolio|mentor)_ids|tier_option_id|solvency_option_id' \
  --include='*.php' --include='*.blade.php' \
  app/Models app/Services app/Jobs app/Repositories app/Http/Controllers resources/views/admin
```

A clean grep here is the canonical "done" signal for Phase 6.

---

## Rollback plan

If anything goes wrong:

```bash
php artisan migrate:rollback --step=11
mysql -uroot -p numu_angels < backup-pre-refactor-YYYYMMDD-HHMM.sql
```

Then `git checkout` the previous commit. The 11 new migrations roll back in reverse order via their own `down()` methods.

---

## What's intentionally still in the codebase

These are NOT bugs — they are documented retentions:

- **FormRequest input names** stay as `company_brief`, `compelling_factors`, `previous_round_count`, `how_heard_option_id`, `has_exit_option_id`, `investment_method_option_ids`, etc. The signup controllers translate them to new column names at persistence. Form-input compatibility was a hard user requirement.
- **`investment_method_option_id`** (single-value, FK) — distinct from the renamed multi-select pivot. Both exist by design (single = "primary method", multi = "all methods used").
- **`is_in_other_angel_group_option_id`** — kept verbatim (Q3 user decision: Yes/No semantics preserved).
- **Synthetic group `Label` rows** — soft-deactivated (`status=0`), not hard-deleted. Restorable by clearing `status`.
- **Comment annotations** like `// was company_brief` — kept for one release as documentation; remove in a follow-up cleanup.
