# Limit Journey Versions to 5 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Cap each custom journey's version history at 5, soft-deleting older versions on save.

**Architecture:** Add a `deleted_at` column to `custom_journey_versions` and the `SoftDeletes` trait to the model, then prune to the 5 newest rows inside `CustomJourneyVersion::record()` — the single funnel every non-blast save already flows through. Existing history queries exclude soft-deleted rows automatically via the trait, so no controller changes are needed.

**Tech Stack:** Laravel (Eloquent, migrations), PHP.

## Global Constraints

- Backend only. No frontend, no asset rebuild.
- No new test-DB infrastructure — this repo's automated tests are pure-logic (no DB, no factories, sqlite commented out in `phpunit.xml`). Verify the DB-bound prune with a `tinker` snippet against the dev DB.
- `const MAX_VERSIONS = 5;` is the retention cap.
- Prune orders by `id` (monotonic autoincrement), never `created_at` (`saveOtherJourney` can insert multiple rows with the same timestamp in one request).

---

### Task 1: Cap version history at 5 with soft delete

**Files:**
- Create: `database/migrations/2026_07_07_000001_add_soft_deletes_to_custom_journey_versions.php`
- Modify: `app/Models/CustomJourneyVersion.php`

**Interfaces:**
- Consumes: existing `CustomJourneyVersion::record(CustomJourney $journey, $editedBy): ?self` and the `custom_journey_versions` table (columns `id`, `custom_journey_id`, `payload`, `save_payload`, `edited_by`, `created_at`).
- Produces: `CustomJourneyVersion::MAX_VERSIONS` constant; `record()` keeps behavior/signature but now soft-deletes all but the 5 newest rows per journey. `SoftDeletes` on the model makes `where(...)`/`find()` exclude trashed rows.

- [ ] **Step 1: Write the migration**

Create `database/migrations/2026_07_07_000001_add_soft_deletes_to_custom_journey_versions.php`:

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::table('custom_journey_versions', function (Blueprint $table) {
            $table->softDeletes();
        });
    }

    public function down()
    {
        Schema::table('custom_journey_versions', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }
};
```

- [ ] **Step 2: Run the migration**

Run: `php artisan migrate`
Expected: migration `2026_07_07_000001_add_soft_deletes_to_custom_journey_versions` runs; `custom_journey_versions` now has a nullable `deleted_at` column.

- [ ] **Step 3: Add the SoftDeletes trait, constant, and prune to the model**

In `app/Models/CustomJourneyVersion.php`, add the trait import and use it, add the constant, and prune after `create()`.

Change the top of the file:

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class CustomJourneyVersion extends Model
{
    use SoftDeletes;

    // Immutable snapshots: only created_at, set explicitly on insert.
    const UPDATED_AT = null;

    // Retain only the 5 newest versions per journey; older ones are soft-deleted.
    const MAX_VERSIONS = 5;
```

Replace the `record()` body's `return static::create([...]);` so it captures the new row, prunes, then returns:

```php
        $version = static::create([
            'custom_journey_id' => $journey->id,
            'payload' => $journey->payload,
            'save_payload' => $journey->save_payload,
            'edited_by' => $editedBy,
        ]);

        // Keep only the MAX_VERSIONS newest rows for this journey; soft-delete the rest.
        $keep = static::where('custom_journey_id', $journey->id)
            ->orderByDesc('id')
            ->limit(self::MAX_VERSIONS)
            ->pluck('id');

        static::where('custom_journey_id', $journey->id)
            ->whereNotIn('id', $keep)
            ->delete(); // soft delete via SoftDeletes trait

        return $version;
```

- [ ] **Step 4: Verify the prune against the dev DB**

Run: `php artisan tinker`, then paste:

```php
$j = \App\Models\CustomJourney::where('is_blast', false)->firstOrFail();
$before = \App\Models\CustomJourneyVersion::where('custom_journey_id', $j->id)->count();
for ($i = 0; $i < 6; $i++) { \App\Models\CustomJourneyVersion::record($j, null); }
$live = \App\Models\CustomJourneyVersion::where('custom_journey_id', $j->id)->count();
$trashed = \App\Models\CustomJourneyVersion::onlyTrashed()->where('custom_journey_id', $j->id)->count();
echo "live={$live} trashed={$trashed}\n";
// Clean up the 6 test rows this snippet created:
\App\Models\CustomJourneyVersion::withTrashed()->where('custom_journey_id', $j->id)->orderByDesc('id')->limit(6 + max(0, $before - 5))->forceDelete();
```

Expected: `live=5` and `trashed>=1`. (`live` caps at 5 regardless of how many existed before.)

- [ ] **Step 5: Commit**

Do NOT commit — the user has explicitly requested no commits. Leave the changes in the working tree for the user to review and commit themselves.

---

## Self-Review

- **Spec coverage:** SoftDeletes trait (spec §1) → Task 1 Step 3. `MAX_VERSIONS = 5` (§1) → Step 3. Prune by `id` after create (§1) → Step 3. Migration `softDeletes()`, no backfill (§2) → Steps 1–2. Backend-only, no rebuild (Scope) → no frontend tasks. Verification "6 saves → 5 live, 1 trashed" → Step 4. Covered.
- **Placeholder scan:** none — all code and commands are concrete.
- **Type consistency:** `record()` signature unchanged (`?self`); `MAX_VERSIONS` referenced as `self::MAX_VERSIONS`; `$keep`/`$version` used consistently. OK.
- **Commit note:** commit step intentionally instructs no commit per user's standing "no commits" directive.
