# Social Media App — Backend API 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:** Build a complete Laravel 11 REST API with Sanctum SPA auth, posts/comments/replies CRUD with image upload, and polymorphic likes.

**Architecture:** Service-layer pattern — thin controllers delegate to service classes, responses shaped by API Resources, authorization via Policies, validation via Form Requests.

**Tech Stack:** Laravel 11, MySQL (Laragon), Laravel Sanctum, Pest

**Working directory:** `C:/laragon/www/assesment-backend`

---

### Task 1: Laravel API + Sanctum + CORS Setup

**Files:**
- Modify: `.env`
- Modify: `config/cors.php`
- Modify: `bootstrap/app.php`
- Create: `routes/api.php` (via artisan)

- [ ] **Step 1: Update `.env` for MySQL and Sanctum**

```ini
APP_NAME="Social Media"
APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=assesment_backend
DB_USERNAME=root
DB_PASSWORD=

SANCTUM_STATEFUL_DOMAINS=localhost:5173
SESSION_DOMAIN=localhost
SESSION_DRIVER=cookie
```

- [ ] **Step 2: Create the MySQL database**

```bash
# Run from any terminal with MySQL access (Laragon shell or cmd)
mysql -u root -e "CREATE DATABASE IF NOT EXISTS assesment_backend CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
```

Expected: no output (success).

- [ ] **Step 3: Install Laravel API routes + Sanctum**

```bash
cd "C:/laragon/www/assesment-backend"
php artisan install:api
```

Expected: `routes/api.php` created, Sanctum config published.

- [ ] **Step 4: Update `config/cors.php`**

Replace the entire file:

```php
<?php

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://localhost:5173'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];
```

- [ ] **Step 5: Update `bootstrap/app.php` to add API middleware**

```php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->statefulApi();
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();
```

- [ ] **Step 6: Create storage symlink**

```bash
php artisan storage:link
```

Expected: `public/storage` -> `storage/app/public` link created.

- [ ] **Step 7: Verify setup**

```bash
php artisan route:list | grep sanctum
```

Expected: `GET|HEAD  sanctum/csrf-cookie` listed.

- [ ] **Step 8: Commit**

```bash
git add -A
git commit -m "chore: configure Sanctum SPA auth and CORS for frontend"
```

---

### Task 2: Update Users Migration & User Model

**Files:**
- Modify: `database/migrations/0001_01_01_000000_create_users_table.php`
- Modify: `app/Models/User.php`

- [ ] **Step 1: Modify the users migration**

Replace `database/migrations/0001_01_01_000000_create_users_table.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(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('first_name', 100);
            $table->string('last_name', 100);
            $table->string('email')->unique();
            $table->string('password');
            $table->string('avatar')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });

        Schema::create('password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        Schema::create('sessions', function (Blueprint $table) {
            $table->string('id')->primary();
            $table->foreignId('user_id')->nullable()->index();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->longText('payload');
            $table->integer('last_activity')->index();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
        Schema::dropIfExists('password_reset_tokens');
        Schema::dropIfExists('sessions');
    }
};
```

- [ ] **Step 2: Update `app/Models/User.php`**

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'first_name',
        'last_name',
        'email',
        'password',
        'avatar',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'password' => 'hashed',
        ];
    }

    public function getFullNameAttribute(): string
    {
        return "{$this->first_name} {$this->last_name}";
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    public function replies()
    {
        return $this->hasMany(Reply::class);
    }

    public function likes()
    {
        return $this->hasMany(Like::class);
    }
}
```

- [ ] **Step 3: Commit**

```bash
git add database/migrations/0001_01_01_000000_create_users_table.php app/Models/User.php
git commit -m "feat: update users table with first_name, last_name, avatar"
```

---

### Task 3: Create Posts, Comments, Replies, Likes Migrations

**Files:**
- Create: `database/migrations/2026_07_14_000001_create_posts_table.php`
- Create: `database/migrations/2026_07_14_000002_create_comments_table.php`
- Create: `database/migrations/2026_07_14_000003_create_replies_table.php`
- Create: `database/migrations/2026_07_14_000004_create_likes_table.php`

- [ ] **Step 1: Generate migration files**

```bash
php artisan make:migration create_posts_table
php artisan make:migration create_comments_table
php artisan make:migration create_replies_table
php artisan make:migration create_likes_table
```

- [ ] **Step 2: Fill in the posts migration**

Find the newly created `create_posts_table` migration and replace its content:

```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(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->text('content');
            $table->string('image_path')->nullable();
            $table->enum('visibility', ['public', 'private'])->default('public');
            $table->timestamps();
            $table->softDeletes();

            $table->index(['user_id', 'created_at']);
            $table->index(['visibility', 'created_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
```

- [ ] **Step 3: Fill in the comments migration**

```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(): void
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->id();
            $table->foreignId('post_id')->constrained()->cascadeOnDelete();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->text('content');
            $table->timestamps();
            $table->softDeletes();

            $table->index(['post_id', 'created_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('comments');
    }
};
```

- [ ] **Step 4: Fill in the replies migration**

```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(): void
    {
        Schema::create('replies', function (Blueprint $table) {
            $table->id();
            $table->foreignId('comment_id')->constrained()->cascadeOnDelete();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->text('content');
            $table->timestamps();
            $table->softDeletes();

            $table->index(['comment_id', 'created_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('replies');
    }
};
```

- [ ] **Step 5: Fill in the likes migration**

```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(): void
    {
        Schema::create('likes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->morphs('likeable'); // adds likeable_type + likeable_id + index
            $table->timestamp('created_at')->useCurrent();

            $table->unique(['user_id', 'likeable_type', 'likeable_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('likes');
    }
};
```

- [ ] **Step 6: Run migrations**

```bash
php artisan migrate
```

Expected output includes: `create_posts_table`, `create_comments_table`, `create_replies_table`, `create_likes_table` — all "DONE".

- [ ] **Step 7: Commit**

```bash
git add database/migrations/
git commit -m "feat: add posts, comments, replies, likes migrations"
```

---

### Task 4: Create Models with Relationships

**Files:**
- Create: `app/Models/Post.php`
- Create: `app/Models/Comment.php`
- Create: `app/Models/Reply.php`
- Create: `app/Models/Like.php`

- [ ] **Step 1: Create `app/Models/Post.php`**

```php
<?php

namespace App\Models;

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

class Post extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['user_id', 'content', 'image_path', 'visibility'];

    protected $casts = [
        'visibility' => 'string',
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class)->latest();
    }

    public function likes()
    {
        return $this->morphMany(Like::class, 'likeable');
    }

    public function scopeVisibleTo($query, User $user)
    {
        return $query->where(function ($q) use ($user) {
            $q->where('visibility', 'public')
              ->orWhere('user_id', $user->id);
        });
    }
}
```

- [ ] **Step 2: Create `app/Models/Comment.php`**

```php
<?php

namespace App\Models;

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

class Comment extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['post_id', 'user_id', 'content'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function post()
    {
        return $this->belongsTo(Post::class);
    }

    public function replies()
    {
        return $this->hasMany(Reply::class)->latest();
    }

    public function likes()
    {
        return $this->morphMany(Like::class, 'likeable');
    }
}
```

- [ ] **Step 3: Create `app/Models/Reply.php`**

```php
<?php

namespace App\Models;

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

class Reply extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['comment_id', 'user_id', 'content'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comment()
    {
        return $this->belongsTo(Comment::class);
    }

    public function likes()
    {
        return $this->morphMany(Like::class, 'likeable');
    }
}
```

- [ ] **Step 4: Create `app/Models/Like.php`**

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Like extends Model
{
    public $timestamps = false;

    protected $fillable = ['user_id', 'likeable_type', 'likeable_id'];

    protected $casts = [
        'created_at' => 'datetime',
    ];

    public function likeable()
    {
        return $this->morphTo();
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
```

- [ ] **Step 5: Commit**

```bash
git add app/Models/
git commit -m "feat: add Post, Comment, Reply, Like models with relationships"
```

---

### Task 5: Auth Endpoints (Register, Login, Logout, Me)

**Files:**
- Create: `app/Http/Requests/RegisterRequest.php`
- Create: `app/Http/Requests/LoginRequest.php`
- Create: `app/Http/Controllers/Api/AuthController.php`

- [ ] **Step 1: Create `app/Http/Requests/RegisterRequest.php`**

```bash
php artisan make:request RegisterRequest
```

Replace contents:

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class RegisterRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'first_name' => ['required', 'string', 'max:100'],
            'last_name'  => ['required', 'string', 'max:100'],
            'email'      => ['required', 'email', 'unique:users,email'],
            'password'   => ['required', 'string', 'min:8', 'confirmed'],
        ];
    }
}
```

- [ ] **Step 2: Create `app/Http/Requests/LoginRequest.php`**

```bash
php artisan make:request LoginRequest
```

Replace contents:

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class LoginRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'email'    => ['required', 'email'],
            'password' => ['required', 'string'],
        ];
    }
}
```

- [ ] **Step 3: Create `app/Http/Controllers/Api/AuthController.php`**

```bash
mkdir -p app/Http/Controllers/Api
```

Create file `app/Http/Controllers/Api/AuthController.php`:

```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use App\Http\Requests\RegisterRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function register(RegisterRequest $request): JsonResponse
    {
        $user = User::create([
            'first_name' => $request->first_name,
            'last_name'  => $request->last_name,
            'email'      => $request->email,
            'password'   => Hash::make($request->password),
        ]);

        Auth::login($user);

        $request->session()->regenerate();

        return response()->json([
            'message' => 'Registration successful.',
            'user'    => new UserResource($user),
        ], 201);
    }

    public function login(LoginRequest $request): JsonResponse
    {
        if (! Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        $request->session()->regenerate();

        return response()->json([
            'message' => 'Login successful.',
            'user'    => new UserResource(Auth::user()),
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        Auth::guard('web')->logout();

        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return response()->json(['message' => 'Logged out.']);
    }

    public function me(Request $request): JsonResponse
    {
        return response()->json([
            'user' => new UserResource($request->user()),
        ]);
    }
}
```

- [ ] **Step 4: Write a Pest feature test**

Create `tests/Feature/AuthTest.php`:

```php
<?php

use App\Models\User;

test('user can register', function () {
    $response = $this->postJson('/api/register', [
        'first_name'            => 'John',
        'last_name'             => 'Doe',
        'email'                 => 'john@example.com',
        'password'              => 'password123',
        'password_confirmation' => 'password123',
    ]);

    $response->assertStatus(201)
             ->assertJsonPath('user.email', 'john@example.com');

    $this->assertDatabaseHas('users', ['email' => 'john@example.com']);
});

test('user can login', function () {
    $user = User::factory()->create([
        'first_name' => 'Jane',
        'last_name'  => 'Doe',
        'password'   => bcrypt('password123'),
    ]);

    $response = $this->postJson('/api/login', [
        'email'    => $user->email,
        'password' => 'password123',
    ]);

    $response->assertOk()
             ->assertJsonPath('user.email', $user->email);
});

test('login fails with wrong password', function () {
    $user = User::factory()->create(['password' => bcrypt('correct')]);

    $this->postJson('/api/login', [
        'email'    => $user->email,
        'password' => 'wrong',
    ])->assertStatus(422);
});

test('authenticated user can get their profile', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
         ->getJson('/api/me')
         ->assertOk()
         ->assertJsonPath('user.email', $user->email);
});
```

- [ ] **Step 5: Update User factory to use first_name/last_name**

Open `database/factories/UserFactory.php` and replace definition:

```php
public function definition(): array
{
    return [
        'first_name' => fake()->firstName(),
        'last_name'  => fake()->lastName(),
        'email'      => fake()->unique()->safeEmail(),
        'password'   => 'password',
        'avatar'     => null,
    ];
}
```

Remove the `email_verified_at` and `remember_token` lines if present.

- [ ] **Step 6: Run tests (they will fail — routes not yet added)**

```bash
php artisan test tests/Feature/AuthTest.php
```

Expected: FAIL — "404 not found" (routes not wired yet, added in Task 10).

- [ ] **Step 7: Commit**

```bash
git add app/Http/Requests/ app/Http/Controllers/Api/AuthController.php tests/Feature/AuthTest.php database/factories/UserFactory.php
git commit -m "feat: auth endpoints (register, login, logout, me)"
```

---

### Task 6: Post Feature (Request, Policy, Service, Controller, Resource)

**Files:**
- Create: `app/Http/Requests/StorePostRequest.php`
- Create: `app/Http/Requests/UpdatePostRequest.php`
- Create: `app/Policies/PostPolicy.php`
- Create: `app/Services/PostService.php`
- Create: `app/Http/Controllers/Api/PostController.php`
- Create: `app/Http/Resources/PostResource.php` (partial — completed in Task 10)

- [ ] **Step 1: Create `app/Http/Requests/StorePostRequest.php`**

```bash
php artisan make:request StorePostRequest
```

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'content'    => ['required', 'string', 'max:5000'],
            'image'      => ['nullable', 'image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
            'visibility' => ['required', 'in:public,private'],
        ];
    }
}
```

- [ ] **Step 2: Create `app/Http/Requests/UpdatePostRequest.php`**

```bash
php artisan make:request UpdatePostRequest
```

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'content'    => ['sometimes', 'string', 'max:5000'],
            'visibility' => ['sometimes', 'in:public,private'],
        ];
    }
}
```

- [ ] **Step 3: Create `app/Policies/PostPolicy.php`**

```bash
php artisan make:policy PostPolicy --model=Post
```

Replace contents:

```php
<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}
```

- [ ] **Step 4: Create `app/Services/PostService.php`**

```bash
mkdir -p app/Services
```

Create `app/Services/PostService.php`:

```php
<?php

namespace App\Services;

use App\Models\Post;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Storage;

class PostService
{
    public function paginate(User $user, int $perPage = 10): LengthAwarePaginator
    {
        return Post::with(['user', 'likes.user'])
            ->withCount(['comments', 'likes'])
            ->visibleTo($user)
            ->latest()
            ->paginate($perPage);
    }

    public function create(User $user, array $data, ?UploadedFile $image = null): Post
    {
        $imagePath = null;
        if ($image) {
            $imagePath = $image->store('posts', 'public');
        }

        return Post::create([
            'user_id'    => $user->id,
            'content'    => $data['content'],
            'image_path' => $imagePath,
            'visibility' => $data['visibility'],
        ]);
    }

    public function update(Post $post, array $data): Post
    {
        $post->update($data);
        return $post->fresh();
    }

    public function delete(Post $post): void
    {
        if ($post->image_path) {
            Storage::disk('public')->delete($post->image_path);
        }
        $post->delete();
    }
}
```

- [ ] **Step 5: Create `app/Http/Controllers/Api/PostController.php`**

```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Post;
use App\Services\PostService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class PostController extends Controller
{
    public function __construct(private PostService $postService) {}

    public function index(Request $request): AnonymousResourceCollection
    {
        $posts = $this->postService->paginate($request->user());

        return PostResource::collection($posts);
    }

    public function store(StorePostRequest $request): JsonResponse
    {
        $post = $this->postService->create(
            $request->user(),
            $request->validated(),
            $request->file('image')
        );

        $post->load('user');
        $post->loadCount(['comments', 'likes']);

        return response()->json([
            'message' => 'Post created.',
            'post'    => new PostResource($post),
        ], 201);
    }

    public function show(Request $request, Post $post): JsonResponse
    {
        $this->authorize('view', $post);

        $post->load(['user', 'likes.user']);
        $post->loadCount(['comments', 'likes']);

        return response()->json(['post' => new PostResource($post)]);
    }

    public function update(UpdatePostRequest $request, Post $post): JsonResponse
    {
        $this->authorize('update', $post);

        $updated = $this->postService->update($post, $request->validated());
        $updated->load('user');
        $updated->loadCount(['comments', 'likes']);

        return response()->json([
            'message' => 'Post updated.',
            'post'    => new PostResource($updated),
        ]);
    }

    public function destroy(Request $request, Post $post): JsonResponse
    {
        $this->authorize('delete', $post);

        $this->postService->delete($post);

        return response()->json(['message' => 'Post deleted.']);
    }
}
```

- [ ] **Step 6: Create `app/Http/Resources/PostResource.php`**

```bash
php artisan make:resource PostResource
```

Replace contents with temporary stub:

```php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'           => $this->id,
            'content'      => $this->content,
            'image_url'    => $this->image_path ? asset('storage/' . $this->image_path) : null,
            'visibility'   => $this->visibility,
            'created_at'   => $this->created_at->toISOString(),
            'user'         => new UserResource($this->whenLoaded('user')),
            'like_count'   => $this->likes_count ?? $this->likes()->count(),
            'liked_by_me'  => $this->whenLoaded('likes', fn() =>
                $this->likes->contains('user_id', $request->user()?->id)
            ),
            'likers'       => UserResource::collection($this->whenLoaded('likes',
                fn() => $this->likes->take(5)->map(fn($l) => $l->user)->filter()
            )),
            'comment_count' => $this->comments_count ?? $this->comments()->count(),
        ];
    }
}
```

- [ ] **Step 7: Create stub `app/Http/Resources/UserResource.php`**

```bash
php artisan make:resource UserResource
```

```php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'         => $this->id,
            'first_name' => $this->first_name,
            'last_name'  => $this->last_name,
            'full_name'  => $this->full_name,
            'email'      => $this->email,
            'avatar_url' => $this->avatar ? asset('storage/' . $this->avatar) : null,
        ];
    }
}
```

- [ ] **Step 8: Write Post feature test**

Create `tests/Feature/PostTest.php`:

```php
<?php

use App\Models\Post;
use App\Models\User;

test('authenticated user can create a post', function () {
    $user = User::factory()->create();

    $response = $this->actingAs($user)->postJson('/api/posts', [
        'content'    => 'Hello world',
        'visibility' => 'public',
    ]);

    $response->assertStatus(201)
             ->assertJsonPath('post.content', 'Hello world');

    $this->assertDatabaseHas('posts', ['content' => 'Hello world']);
});

test('guest cannot create a post', function () {
    $this->postJson('/api/posts', ['content' => 'test', 'visibility' => 'public'])
         ->assertStatus(401);
});

test('user cannot update another user\'s post', function () {
    $owner = User::factory()->create();
    $other = User::factory()->create();
    $post  = Post::factory()->create(['user_id' => $owner->id]);

    $this->actingAs($other)
         ->putJson("/api/posts/{$post->id}", ['content' => 'hacked'])
         ->assertStatus(403);
});

test('private post is not visible to other users', function () {
    $owner = User::factory()->create();
    $other = User::factory()->create();
    Post::factory()->create([
        'user_id'    => $owner->id,
        'visibility' => 'private',
    ]);

    $response = $this->actingAs($other)->getJson('/api/posts');

    $response->assertOk();
    $this->assertCount(0, $response->json('data'));
});
```

- [ ] **Step 9: Add Post factory**

Create `database/factories/PostFactory.php`:

```php
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id'    => User::factory(),
            'content'    => fake()->paragraph(),
            'image_path' => null,
            'visibility' => 'public',
        ];
    }
}
```

- [ ] **Step 10: Commit**

```bash
git add app/Http/Requests/StorePostRequest.php app/Http/Requests/UpdatePostRequest.php
git add app/Policies/PostPolicy.php app/Services/PostService.php
git add app/Http/Controllers/Api/PostController.php
git add app/Http/Resources/PostResource.php app/Http/Resources/UserResource.php
git add tests/Feature/PostTest.php database/factories/PostFactory.php
git commit -m "feat: post CRUD with service layer, policy, and resource"
```

---

### Task 7: Comment Feature (Service, Controller, Policy)

**Files:**
- Create: `app/Http/Requests/StoreCommentRequest.php`
- Create: `app/Policies/CommentPolicy.php`
- Create: `app/Services/CommentService.php`
- Create: `app/Http/Controllers/Api/CommentController.php`
- Create: `app/Http/Resources/CommentResource.php`

- [ ] **Step 1: Create `app/Http/Requests/StoreCommentRequest.php`**

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreCommentRequest extends FormRequest
{
    public function authorize(): bool { return true; }

    public function rules(): array
    {
        return ['content' => ['required', 'string', 'max:2000']];
    }
}
```

- [ ] **Step 2: Create `app/Policies/CommentPolicy.php`**

```php
<?php

namespace App\Policies;

use App\Models\Comment;
use App\Models\User;

class CommentPolicy
{
    public function update(User $user, Comment $comment): bool
    {
        return $user->id === $comment->user_id;
    }

    public function delete(User $user, Comment $comment): bool
    {
        return $user->id === $comment->user_id;
    }
}
```

- [ ] **Step 3: Create `app/Services/CommentService.php`**

```php
<?php

namespace App\Services;

use App\Models\Comment;
use App\Models\Post;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;

class CommentService
{
    public function forPost(Post $post): Collection
    {
        return $post->comments()
            ->with(['user', 'likes.user', 'replies.user', 'replies.likes.user'])
            ->withCount('likes')
            ->get();
    }

    public function create(User $user, Post $post, string $content): Comment
    {
        return Comment::create([
            'post_id' => $post->id,
            'user_id' => $user->id,
            'content' => $content,
        ]);
    }

    public function update(Comment $comment, string $content): Comment
    {
        $comment->update(['content' => $content]);
        return $comment->fresh();
    }

    public function delete(Comment $comment): void
    {
        $comment->delete();
    }
}
```

- [ ] **Step 4: Create `app/Http/Controllers/Api/CommentController.php`**

```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreCommentRequest;
use App\Http\Resources\CommentResource;
use App\Models\Comment;
use App\Models\Post;
use App\Services\CommentService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class CommentController extends Controller
{
    public function __construct(private CommentService $commentService) {}

    public function index(Post $post): JsonResponse
    {
        $comments = $this->commentService->forPost($post);
        return response()->json(['comments' => CommentResource::collection($comments)]);
    }

    public function store(StoreCommentRequest $request, Post $post): JsonResponse
    {
        $comment = $this->commentService->create(
            $request->user(),
            $post,
            $request->validated('content')
        );
        $comment->load(['user', 'likes.user', 'replies']);
        $comment->loadCount('likes');

        return response()->json([
            'message' => 'Comment added.',
            'comment' => new CommentResource($comment),
        ], 201);
    }

    public function update(StoreCommentRequest $request, Comment $comment): JsonResponse
    {
        $this->authorize('update', $comment);

        $updated = $this->commentService->update($comment, $request->validated('content'));
        $updated->load(['user', 'likes.user']);
        $updated->loadCount('likes');

        return response()->json([
            'message' => 'Comment updated.',
            'comment' => new CommentResource($updated),
        ]);
    }

    public function destroy(Request $request, Comment $comment): JsonResponse
    {
        $this->authorize('delete', $comment);
        $this->commentService->delete($comment);
        return response()->json(['message' => 'Comment deleted.']);
    }
}
```

- [ ] **Step 5: Create `app/Http/Resources/CommentResource.php`**

```php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class CommentResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'content'     => $this->content,
            'created_at'  => $this->created_at->toISOString(),
            'user'        => new UserResource($this->whenLoaded('user')),
            'like_count'  => $this->likes_count ?? $this->likes()->count(),
            'liked_by_me' => $this->whenLoaded('likes', fn() =>
                $this->likes->contains('user_id', $request->user()?->id)
            ),
            'replies'     => ReplyResource::collection($this->whenLoaded('replies')),
        ];
    }
}
```

- [ ] **Step 6: Commit**

```bash
git add app/Http/Requests/StoreCommentRequest.php app/Policies/CommentPolicy.php
git add app/Services/CommentService.php app/Http/Controllers/Api/CommentController.php
git add app/Http/Resources/CommentResource.php
git commit -m "feat: comment CRUD with service, policy, and resource"
```

---

### Task 8: Reply Feature (Service, Controller, Policy, Resource)

**Files:**
- Create: `app/Http/Requests/StoreReplyRequest.php`
- Create: `app/Policies/ReplyPolicy.php`
- Create: `app/Services/ReplyService.php`
- Create: `app/Http/Controllers/Api/ReplyController.php`
- Create: `app/Http/Resources/ReplyResource.php`

- [ ] **Step 1: Create `app/Http/Requests/StoreReplyRequest.php`**

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreReplyRequest extends FormRequest
{
    public function authorize(): bool { return true; }

    public function rules(): array
    {
        return ['content' => ['required', 'string', 'max:2000']];
    }
}
```

- [ ] **Step 2: Create `app/Policies/ReplyPolicy.php`**

```php
<?php

namespace App\Policies;

use App\Models\Reply;
use App\Models\User;

class ReplyPolicy
{
    public function update(User $user, Reply $reply): bool
    {
        return $user->id === $reply->user_id;
    }

    public function delete(User $user, Reply $reply): bool
    {
        return $user->id === $reply->user_id;
    }
}
```

- [ ] **Step 3: Create `app/Services/ReplyService.php`**

```php
<?php

namespace App\Services;

use App\Models\Comment;
use App\Models\Reply;
use App\Models\User;

class ReplyService
{
    public function create(User $user, Comment $comment, string $content): Reply
    {
        return Reply::create([
            'comment_id' => $comment->id,
            'user_id'    => $user->id,
            'content'    => $content,
        ]);
    }

    public function update(Reply $reply, string $content): Reply
    {
        $reply->update(['content' => $content]);
        return $reply->fresh();
    }

    public function delete(Reply $reply): void
    {
        $reply->delete();
    }
}
```

- [ ] **Step 4: Create `app/Http/Controllers/Api/ReplyController.php`**

```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreReplyRequest;
use App\Http\Resources\ReplyResource;
use App\Models\Comment;
use App\Models\Reply;
use App\Services\ReplyService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ReplyController extends Controller
{
    public function __construct(private ReplyService $replyService) {}

    public function store(StoreReplyRequest $request, Comment $comment): JsonResponse
    {
        $reply = $this->replyService->create(
            $request->user(),
            $comment,
            $request->validated('content')
        );
        $reply->load(['user', 'likes.user']);
        $reply->loadCount('likes');

        return response()->json([
            'message' => 'Reply added.',
            'reply'   => new ReplyResource($reply),
        ], 201);
    }

    public function update(StoreReplyRequest $request, Reply $reply): JsonResponse
    {
        $this->authorize('update', $reply);

        $updated = $this->replyService->update($reply, $request->validated('content'));
        $updated->load(['user', 'likes.user']);
        $updated->loadCount('likes');

        return response()->json([
            'message' => 'Reply updated.',
            'reply'   => new ReplyResource($updated),
        ]);
    }

    public function destroy(Request $request, Reply $reply): JsonResponse
    {
        $this->authorize('delete', $reply);
        $this->replyService->delete($reply);
        return response()->json(['message' => 'Reply deleted.']);
    }
}
```

- [ ] **Step 5: Create `app/Http/Resources/ReplyResource.php`**

```php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ReplyResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'content'     => $this->content,
            'created_at'  => $this->created_at->toISOString(),
            'user'        => new UserResource($this->whenLoaded('user')),
            'like_count'  => $this->likes_count ?? $this->likes()->count(),
            'liked_by_me' => $this->whenLoaded('likes', fn() =>
                $this->likes->contains('user_id', $request->user()?->id)
            ),
        ];
    }
}
```

- [ ] **Step 6: Commit**

```bash
git add app/Http/Requests/StoreReplyRequest.php app/Policies/ReplyPolicy.php
git add app/Services/ReplyService.php app/Http/Controllers/Api/ReplyController.php
git add app/Http/Resources/ReplyResource.php
git commit -m "feat: reply CRUD with service, policy, and resource"
```

---

### Task 9: Like Feature (Polymorphic Toggle)

**Files:**
- Create: `app/Services/LikeService.php`
- Create: `app/Http/Controllers/Api/LikeController.php`

- [ ] **Step 1: Create `app/Services/LikeService.php`**

```php
<?php

namespace App\Services;

use App\Models\Like;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;

class LikeService
{
    public function toggle(User $user, Model $model): array
    {
        $existing = Like::where([
            'user_id'       => $user->id,
            'likeable_type' => get_class($model),
            'likeable_id'   => $model->id,
        ])->first();

        if ($existing) {
            $existing->delete();
            $liked = false;
        } else {
            Like::create([
                'user_id'       => $user->id,
                'likeable_type' => get_class($model),
                'likeable_id'   => $model->id,
            ]);
            $liked = true;
        }

        $likeCount = Like::where([
            'likeable_type' => get_class($model),
            'likeable_id'   => $model->id,
        ])->count();

        return ['liked' => $liked, 'like_count' => $likeCount];
    }
}
```

- [ ] **Step 2: Create `app/Http/Controllers/Api/LikeController.php`**

```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Comment;
use App\Models\Post;
use App\Models\Reply;
use App\Services\LikeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class LikeController extends Controller
{
    public function __construct(private LikeService $likeService) {}

    public function likePost(Request $request, Post $post): JsonResponse
    {
        $result = $this->likeService->toggle($request->user(), $post);
        return response()->json($result);
    }

    public function likeComment(Request $request, Comment $comment): JsonResponse
    {
        $result = $this->likeService->toggle($request->user(), $comment);
        return response()->json($result);
    }

    public function likeReply(Request $request, Reply $reply): JsonResponse
    {
        $result = $this->likeService->toggle($request->user(), $reply);
        return response()->json($result);
    }
}
```

- [ ] **Step 3: Write like test**

Add to `tests/Feature/PostTest.php`:

```php
test('user can like and unlike a post', function () {
    $user = User::factory()->create();
    $post = Post::factory()->create();

    // Like
    $response = $this->actingAs($user)->postJson("/api/posts/{$post->id}/like");
    $response->assertOk()->assertJsonPath('liked', true)->assertJsonPath('like_count', 1);

    // Unlike
    $response = $this->actingAs($user)->postJson("/api/posts/{$post->id}/like");
    $response->assertOk()->assertJsonPath('liked', false)->assertJsonPath('like_count', 0);
});
```

- [ ] **Step 4: Commit**

```bash
git add app/Services/LikeService.php app/Http/Controllers/Api/LikeController.php
git commit -m "feat: polymorphic like toggle for posts, comments, replies"
```

---

### Task 10: Wire Up All Routes + Run Tests

**Files:**
- Modify: `routes/api.php`

- [ ] **Step 1: Replace `routes/api.php` with all routes**

```php
<?php

use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\CommentController;
use App\Http\Controllers\Api\LikeController;
use App\Http\Controllers\Api\PostController;
use App\Http\Controllers\Api\ReplyController;
use Illuminate\Support\Facades\Route;

// Auth (unauthenticated)
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login',    [AuthController::class, 'login']);

// Protected routes
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/logout', [AuthController::class, 'logout']);
    Route::get('/me',      [AuthController::class, 'me']);

    // Posts
    Route::apiResource('posts', PostController::class);
    Route::get('posts/{post}/comments', [CommentController::class, 'index']);

    // Comments
    Route::post('posts/{post}/comments', [CommentController::class, 'store']);
    Route::put('comments/{comment}',     [CommentController::class, 'update']);
    Route::delete('comments/{comment}',  [CommentController::class, 'destroy']);

    // Replies
    Route::post('comments/{comment}/replies', [ReplyController::class, 'store']);
    Route::put('replies/{reply}',             [ReplyController::class, 'update']);
    Route::delete('replies/{reply}',          [ReplyController::class, 'destroy']);

    // Likes (toggle)
    Route::post('posts/{post}/like',       [LikeController::class, 'likePost']);
    Route::post('comments/{comment}/like', [LikeController::class, 'likeComment']);
    Route::post('replies/{reply}/like',    [LikeController::class, 'likeReply']);
});
```

- [ ] **Step 2: Add rate limiting to `bootstrap/app.php`**

In `bootstrap/app.php`, add rate limiting inside `withMiddleware`:

```php
->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
    $middleware->throttleApi();
})
```

- [ ] **Step 3: Run all tests**

```bash
php artisan test
```

Expected: All tests pass. If any fail, check the error message and fix.

- [ ] **Step 4: Verify routes list**

```bash
php artisan route:list --path=api
```

Expected: All 18 API routes listed.

- [ ] **Step 5: Final commit**

```bash
git add routes/api.php bootstrap/app.php
git commit -m "feat: wire up all API routes with auth middleware and rate limiting"
```

---

### Task 11: Manual API Smoke Test

- [ ] **Step 1: Start Laragon (Apache + MySQL running)**

Confirm `http://localhost/assesment-backend/public` returns the Laravel welcome page, OR the Laragon virtual host for `assesment-backend.test` is configured.

- [ ] **Step 2: Test registration via curl**

```bash
curl -s -X POST http://localhost/assesment-backend/public/api/register \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"first_name":"John","last_name":"Doe","email":"john@test.com","password":"password123","password_confirmation":"password123"}' \
  | python -m json.tool
```

Expected: `{"message": "Registration successful.", "user": {...}}`

- [ ] **Step 3: Verify DB has the user**

```bash
mysql -u root assesment_backend -e "SELECT id, first_name, last_name, email FROM users;"
```

Expected: Row with `john@test.com`.

- [ ] **Step 4: Final backend commit**

```bash
git add -A
git commit -m "chore: backend API complete and smoke tested"
```
