Output: {"output":"Success. Updated the following files:\nM app/Http/Middleware/AuthenticatePlatformApi.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: bootstrap/app.php
@@
 use App\Http\Middleware\AuthenticateAdminApi;
+use App\Http\Middleware\AuthenticatePlatformApi;
 use App\Http\Middleware\CheckStoreRole;
 use App\Http\Middleware\ResolveStore;
@@
         $middleware->alias([
             'admin.api' => AuthenticateAdminApi::class,
+            'platform.api' => AuthenticatePlatformApi::class,
             'role.check' => CheckStoreRole::class,
             'store.resolve' => ResolveStore::class,
         ]);
*** End Patch


Success. Updated the following files:
M bootstrap/app.php

@@ -3,2 +3,3 @@
 use App\Http\Middleware\AuthenticateAdminApi;
+use App\Http\Middleware\AuthenticatePlatformApi;
 use App\Http\Middleware\CheckStoreRole;
@@ -19,2 +20,3 @@
             'admin.api' => AuthenticateAdminApi::class,
+            'platform.api' => AuthenticatePlatformApi::class,
             'role.check' => CheckStoreRole::class,


Output: {"output":"Success. Updated the following files:\nM bootstrap/app.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/StorePlatformOrganizationRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

 use Illuminate\Foundation\Http\FormRequest;

 class StorePlatformOrganizationRequest extends FormRequest
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
         return [
-            //
+            'name' => ['required', 'string', 'max:255'],
+            'billing_email' => ['required', 'email:rfc,dns', 'max:255'],
         ];
     }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Requests/Api/Admin/V1/StorePlatformOrganizationRequest.php

@@ -8,8 +8,5 @@
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }
@@ -17,4 +14,2 @@
     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
@@ -24,3 +19,4 @@
         return [
-            //
+            'name' => ['required', 'string', 'max:255'],
+            'billing_email' => ['required', 'email:rfc,dns', 'max:255'],
         ];


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Admin/V1/StorePlatformOrganizationRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/StorePlatformStoreRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;

 class StorePlatformStoreRequest extends FormRequest
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
         return [
-            //
+            'organization_id' => ['required', 'integer', Rule::exists('organizations', 'id')],
+            'name' => ['required', 'string', 'max:255'],
+            'handle' => [
+                'required',
+                'string',
+                'max:63',
+                'regex:/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/',
+                Rule::unique('stores', 'handle'),
+            ],
+            'default_currency' => ['required', 'string', 'size:3'],
+            'default_locale' => ['required', 'string', 'max:12'],
+            'timezone' => ['required', 'timezone:all'],
         ];
     }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Requests/Api/Admin/V1/StorePlatformStoreRequest.php

@@ -5,2 +5,3 @@
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;

@@ -8,8 +9,5 @@
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }
@@ -17,4 +15,2 @@
     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
@@ -24,3 +20,14 @@
         return [
-            //
+            'organization_id' => ['required', 'integer', Rule::exists('organizations', 'id')],
+            'name' => ['required', 'string', 'max:255'],
+            'handle' => [
+                'required',
+                'string',
+                'max:63',
+                'regex:/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/',
+                Rule::unique('stores', 'handle'),
+            ],
+            'default_currency' => ['required', 'string', 'size:3'],
+            'default_locale' => ['required', 'string', 'max:12'],
+            'timezone' => ['required', 'timezone:all'],
         ];


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Admin/V1/StorePlatformStoreRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/StoreInviteRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;

 class StoreInviteRequest extends FormRequest
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
         return [
-            //
+            'email' => ['required', 'email:rfc,dns', 'max:255'],
+            'role' => ['required', Rule::in(['owner', 'admin', 'staff', 'support'])],
         ];
     }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Requests/Api/Admin/V1/StoreInviteRequest.php

@@ -5,2 +5,3 @@
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;

@@ -8,8 +9,5 @@
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }
@@ -17,4 +15,2 @@
     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
@@ -24,3 +20,4 @@
         return [
-            //
+            'email' => ['required', 'email:rfc,dns', 'max:255'],
+            'role' => ['required', Rule::in(['owner', 'admin', 'staff', 'support'])],
         ];


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Admin/V1/StoreInviteRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/StorePlatformOrganizationRequest.php
@@
-            'billing_email' => ['required', 'email:rfc,dns', 'max:255'],
+            'billing_email' => ['required', 'email', 'max:255'],
*** End Patch


@@ -20,3 +20,3 @@
             'name' => ['required', 'string', 'max:255'],
-            'billing_email' => ['required', 'email:rfc,dns', 'max:255'],
+            'billing_email' => ['required', 'email', 'max:255'],
         ];


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Admin/V1/StorePlatformOrganizationRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/StoreInviteRequest.php
@@
-            'email' => ['required', 'email:rfc,dns', 'max:255'],
+            'email' => ['required', 'email', 'max:255'],
*** End Patch


@@ -20,3 +20,3 @@
         return [
-            'email' => ['required', 'email:rfc,dns', 'max:255'],
+            'email' => ['required', 'email', 'max:255'],
             'role' => ['required', Rule::in(['owner', 'admin', 'staff', 'support'])],


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Admin/V1/StoreInviteRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Resources/Admin/V1/OrganizationResource.php
@@
 namespace App\Http\Resources\Admin\V1;

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

 class OrganizationResource extends JsonResource
 {
     /**
      * Transform the resource into an array.
      *
      * @return array<string, mixed>
      */
     public function toArray(Request $request): array
     {
-        return parent::toArray($request);
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+            'billing_email' => $this->billing_email,
+            'created_at' => $this->created_at?->toIso8601String(),
+            'updated_at' => $this->updated_at?->toIso8601String(),
+        ];
     }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Resources/Admin/V1/OrganizationResource.php

@@ -16,3 +16,9 @@
     {
-        return parent::toArray($request);
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+            'billing_email' => $this->billing_email,
+            'created_at' => $this->created_at?->toIso8601String(),
+            'updated_at' => $this->updated_at?->toIso8601String(),
+        ];
     }


Output: {"output":"Success. Updated the following files:\nM app/Http/Resources/Admin/V1/OrganizationResource.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Resources/Admin/V1/StoreResource.php
@@
 namespace App\Http\Resources\Admin\V1;

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

 class StoreResource extends JsonResource
 {
     /**
      * Transform the resource into an array.
      *
      * @return array<string, mixed>
      */
     public function toArray(Request $request): array
     {
-        return parent::toArray($request);
+        return [
+            'id' => $this->id,
+            'organization_id' => $this->organization_id,
+            'name' => $this->name,
+            'handle' => $this->handle,
+            'status' => $this->status?->value,
+            'default_currency' => $this->default_currency,
+            'default_locale' => $this->default_locale,
+            'timezone' => $this->timezone,
+            'created_at' => $this->created_at?->toIso8601String(),
+        ];
     }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Resources/Admin/V1/StoreResource.php

@@ -16,3 +16,13 @@
     {
-        return parent::toArray($request);
+        return [
+            'id' => $this->id,
+            'organization_id' => $this->organization_id,
+            'name' => $this->name,
+            'handle' => $this->handle,
+            'status' => $this->status?->value,
+            'default_currency' => $this->default_currency,
+            'default_locale' => $this->default_locale,
+            'timezone' => $this->timezone,
+            'created_at' => $this->created_at?->toIso8601String(),
+        ];
     }


Output: {"output":"Success. Updated the following files:\nM app/Http/Resources/Admin/V1/StoreResource.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php
@@
 namespace App\Http\Controllers\Api\Admin\V1;

+use App\Http\Controllers\Controller;
+use App\Http\Requests\Api\Admin\V1\StorePlatformOrganizationRequest;
+use App\Http\Resources\Admin\V1\OrganizationResource;
+use App\Models\Organization;
+use Illuminate\Http\JsonResponse;
+
 class PlatformOrganizationController extends Controller
 {
-    //
+    public function store(StorePlatformOrganizationRequest $request): JsonResponse
+    {
+        $organization = Organization::query()->create($request->validated());
+
+        return OrganizationResource::make($organization)
+            ->response()
+            ->setStatusCode(201);
+    }
 }
*** End Patch


Output:
apply_patch verification failed: Failed to find expected lines in /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php:
namespace App\Http\Controllers\Api\Admin\V1;

class PlatformOrganizationController extends Controller
{
    //
}


sed -n '1,80p' app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php


sed -n '1,80p' app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php


sed -n '1,80p' app/Http/Controllers/Api/Admin/V1/StoreInviteController.php


sed -n '1,80p' app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php


sed -n '1,80p' app/Http/Controllers/Api/Admin/V1/StoreInviteController.php

<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class StoreInviteController extends Controller
{
    //
}


sed -n '1,80p' app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php

<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class PlatformOrganizationController extends Controller
{
    //
}


sed -n '1,80p' app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php

<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class PlatformStoreController extends Controller
{
    //
}


Output:
Chunk ID: 310a25
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 47
Output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class PlatformOrganizationController extends Controller
{
    //
}


Output:
Chunk ID: ad93c9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 45
Output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class PlatformStoreController extends Controller
{
    //
}


Output:
Chunk ID: b2ee29
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 45
Output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class StoreInviteController extends Controller
{
    //
}


sed -n '1,80p' app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php

<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class StoreMembershipController extends Controller
{
    //
}


Output:
Chunk ID: 6b9e6d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 46
Output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class StoreMembershipController extends Controller
{
    //
}


Input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php
@@
 use App\Http\Controllers\Controller;
-use Illuminate\Http\Request;
+use App\Http\Requests\Api\Admin\V1\StorePlatformOrganizationRequest;
+use App\Http\Resources\Admin\V1\OrganizationResource;
+use App\Models\Organization;
+use Illuminate\Http\JsonResponse;

 class PlatformOrganizationController extends Controller
 {
-    //
+    public function store(StorePlatformOrganizationRequest $request): JsonResponse
+    {
+        $organization = Organization::query()->create($request->validated());
+
+        return OrganizationResource::make($organization)
+            ->response()
+            ->setStatusCode(201);
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php

@@ -5,3 +5,6 @@
 use App\Http\Controllers\Controller;
-use Illuminate\Http\Request;
+use App\Http\Requests\Api\Admin\V1\StorePlatformOrganizationRequest;
+use App\Http\Resources\Admin\V1\OrganizationResource;
+use App\Models\Organization;
+use Illuminate\Http\JsonResponse;

@@ -9,3 +12,10 @@
 {
-    //
+    public function store(StorePlatformOrganizationRequest $request): JsonResponse
+    {
+        $organization = Organization::query()->create($request->validated());
+
+        return OrganizationResource::make($organization)
+            ->response()
+            ->setStatusCode(201);
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php
@@
 use App\Http\Controllers\Controller;
-use Illuminate\Http\Request;
+use App\Http\Requests\Api\Admin\V1\StorePlatformStoreRequest;
+use App\Http\Resources\Admin\V1\StoreResource;
+use App\Models\Store;
+use App\Models\StoreSettings;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\DB;

 class PlatformStoreController extends Controller
 {
-    //
+    public function store(StorePlatformStoreRequest $request): JsonResponse
+    {
+        $validated = $request->validated();
+
+        $store = DB::transaction(function () use ($request, $validated): Store {
+            $store = Store::query()->create([
+                'organization_id' => $validated['organization_id'],
+                'name' => $validated['name'],
+                'handle' => $validated['handle'],
+                'default_currency' => strtoupper((string) $validated['default_currency']),
+                'default_locale' => $validated['default_locale'],
+                'timezone' => $validated['timezone'],
+            ]);
+
+            StoreSettings::query()->create([
+                'store_id' => $store->getKey(),
+                'settings_json' => [],
+            ]);
+
+            $user = $request->user();
+
+            if ($user instanceof \App\Models\User) {
+                $store->users()->syncWithoutDetaching([
+                    $user->getKey() => ['role' => 'owner', 'created_at' => now()],
+                ]);
+            }
+
+            return $store;
+        });
+
+        return StoreResource::make($store)
+            ->response()
+            ->setStatusCode(201);
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php

@@ -5,3 +5,8 @@
 use App\Http\Controllers\Controller;
-use Illuminate\Http\Request;
+use App\Http\Requests\Api\Admin\V1\StorePlatformStoreRequest;
+use App\Http\Resources\Admin\V1\StoreResource;
+use App\Models\Store;
+use App\Models\StoreSettings;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\DB;

@@ -9,3 +14,36 @@
 {
-    //
+    public function store(StorePlatformStoreRequest $request): JsonResponse
+    {
+        $validated = $request->validated();
+
+        $store = DB::transaction(function () use ($request, $validated): Store {
+            $store = Store::query()->create([
+                'organization_id' => $validated['organization_id'],
+                'name' => $validated['name'],
+                'handle' => $validated['handle'],
+                'default_currency' => strtoupper((string) $validated['default_currency']),
+                'default_locale' => $validated['default_locale'],
+                'timezone' => $validated['timezone'],
+            ]);
+
+            StoreSettings::query()->create([
+                'store_id' => $store->getKey(),
+                'settings_json' => [],
+            ]);
+
+            $user = $request->user();
+
+            if ($user instanceof \App\Models\User) {
+                $store->users()->syncWithoutDetaching([
+                    $user->getKey() => ['role' => 'owner', 'created_at' => now()],
+                ]);
+            }
+
+            return $store;
+        });
+
+        return StoreResource::make($store)
+            ->response()
+            ->setStatusCode(201);
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Admin/V1/StoreInviteController.php
@@
 use App\Http\Controllers\Controller;
+use App\Http\Requests\Api\Admin\V1\StoreInviteRequest;
+use App\Models\Store;
+use App\Models\User;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;

 class StoreInviteController extends Controller
 {
-    //
+    public function store(StoreInviteRequest $request, Store $store): JsonResponse
+    {
+        $this->authorizeInvite($request, $store);
+
+        $validated = $request->validated();
+        $userId = User::query()
+            ->where('email', $validated['email'])
+            ->value('id');
+
+        if ($userId !== null && DB::table('store_users')->where('store_id', $store->getKey())->where('user_id', $userId)->exists()) {
+            return response()->json(['message' => 'User already belongs to this store.'], 409);
+        }
+
+        $invitedAt = now();
+
+        return response()->json([
+            'data' => [
+                'email' => $validated['email'],
+                'role' => $validated['role'],
+                'invited_at' => $invitedAt->toIso8601String(),
+                'expires_at' => $invitedAt->copy()->addDays(7)->toIso8601String(),
+            ],
+        ], 201);
+    }
+
+    private function authorizeInvite(Request $request, Store $store): void
+    {
+        if ($request->attributes->has('admin_api_oauth_token')) {
+            return;
+        }
+
+        $role = $request->user()?->roleForStore($store);
+
+        abort_unless(in_array($role?->value, ['owner', 'admin'], true), 403);
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Controllers/Api/Admin/V1/StoreInviteController.php

@@ -5,3 +5,8 @@
 use App\Http\Controllers\Controller;
+use App\Http\Requests\Api\Admin\V1\StoreInviteRequest;
+use App\Models\Store;
+use App\Models\User;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;

@@ -9,3 +14,37 @@
 {
-    //
+    public function store(StoreInviteRequest $request, Store $store): JsonResponse
+    {
+        $this->authorizeInvite($request, $store);
+
+        $validated = $request->validated();
+        $userId = User::query()
+            ->where('email', $validated['email'])
+            ->value('id');
+
+        if ($userId !== null && DB::table('store_users')->where('store_id', $store->getKey())->where('user_id', $userId)->exists()) {
+            return response()->json(['message' => 'User already belongs to this store.'], 409);
+        }
+
+        $invitedAt = now();
+
+        return response()->json([
+            'data' => [
+                'email' => $validated['email'],
+                'role' => $validated['role'],
+                'invited_at' => $invitedAt->toIso8601String(),
+                'expires_at' => $invitedAt->copy()->addDays(7)->toIso8601String(),
+            ],
+        ], 201);
+    }
+
+    private function authorizeInvite(Request $request, Store $store): void
+    {
+        if ($request->attributes->has('admin_api_oauth_token')) {
+            return;
+        }
+
+        $role = $request->user()?->roleForStore($store);
+
+        abort_unless(in_array($role?->value, ['owner', 'admin'], true), 403);
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Http/Controllers/Api/Admin/V1/StoreInviteController.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php
@@
 use App\Http\Controllers\Controller;
+use App\Models\OauthToken;
+use App\Models\Store;
+use App\Models\User;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;

 class StoreMembershipController extends Controller
 {
-    //
+    public function show(Request $request, Store $store): JsonResponse
+    {
+        $token = $request->attributes->get('admin_api_oauth_token');
+
+        if ($token instanceof OauthToken) {
+            return response()->json([
+                'data' => [
+                    'user_id' => null,
+                    'store_id' => $store->getKey(),
+                    'role' => 'integration',
+                    'email' => null,
+                    'name' => $token->name,
+                    'permissions' => $token->abilities_json ?? [],
+                ],
+            ]);
+        }
+
+        $user = $request->user();
+
+        abort_unless($user instanceof User, 401);
+
+        $role = $user->roleForStore($store);
+
+        abort_unless($role !== null, 403);
+
+        return response()->json([
+            'data' => [
+                'user_id' => $user->getKey(),
+                'store_id' => $store->getKey(),
+                'role' => $role->value,
+                'email' => $user->email,
+                'name' => $user->name,
+                'permissions' => $this->permissionsForRole($role->value),
+            ],
+        ]);
+    }
+
+    /**
+     * @return list<string>
+     */
+    private function permissionsForRole(string $role): array
+    {
+        return match ($role) {
+            'owner', 'admin' => [
+                'manage-platform',
+                'read-products',
+                'write-products',
+                'read-collections',
+                'write-collections',
+                'read-orders',
+                'write-orders',
+                'read-customers',
+                'write-customers',
+                'read-discounts',
+                'write-discounts',
+                'read-content',
+                'write-content',
+                'read-settings',
+                'write-settings',
+                'read-analytics',
+                'write-themes',
+                'manage-apps',
+            ],
+            'staff' => [
+                'read-products',
+                'write-products',
+                'read-collections',
+                'write-collections',
+                'read-orders',
+                'write-orders',
+                'read-customers',
+                'read-discounts',
+                'write-discounts',
+                'read-analytics',
+            ],
+            default => [
+                'read-orders',
+                'read-customers',
+            ],
+        };
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php

@@ -5,2 +5,6 @@
 use App\Http\Controllers\Controller;
+use App\Models\OauthToken;
+use App\Models\Store;
+use App\Models\User;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
@@ -9,3 +13,83 @@
 {
-    //
+    public function show(Request $request, Store $store): JsonResponse
+    {
+        $token = $request->attributes->get('admin_api_oauth_token');
+
+        if ($token instanceof OauthToken) {
+            return response()->json([
+                'data' => [
+                    'user_id' => null,
+                    'store_id' => $store->getKey(),
+                    'role' => 'integration',
+                    'email' => null,
+                    'name' => $token->name,
+                    'permissions' => $token->abilities_json ?? [],
+                ],
+            ]);
+        }
+
+        $user = $request->user();
+
+        abort_unless($user instanceof User, 401);
+
+        $role = $user->roleForStore($store);
+
+        abort_unless($role !== null, 403);
+
+        return response()->json([
+            'data' => [
+                'user_id' => $user->getKey(),
+                'store_id' => $store->getKey(),
+                'role' => $role->value,
+                'email' => $user->email,
+                'name' => $user->name,
+                'permissions' => $this->permissionsForRole($role->value),
+            ],
+        ]);
+    }
+
+    /**
+     * @return list<string>
+     */
+    private function permissionsForRole(string $role): array
+    {
+        return match ($role) {
+            'owner', 'admin' => [
+                'manage-platform',
+                'read-products',
+                'write-products',
+                'read-collections',
+                'write-collections',
+                'read-orders',
+                'write-orders',
+                'read-customers',
+                'write-customers',
+                'read-discounts',
+                'write-discounts',
+                'read-content',
+                'write-content',
+                'read-settings',
+                'write-settings',
+                'read-analytics',
+                'write-themes',
+                'manage-apps',
+            ],
+            'staff' => [
+                'read-products',
+                'write-products',
+                'read-collections',
+                'write-collections',
+                'read-orders',
+                'write-orders',
+                'read-customers',
+                'read-discounts',
+                'write-discounts',
+                'read-analytics',
+            ],
+            default => [
+                'read-orders',
+                'read-customers',
+            ],
+        };
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: routes/api.php
@@
 use App\Http\Controllers\Api\Admin\V1\OrderFulfillmentController as AdminOrderFulfillmentController;
 use App\Http\Controllers\Api\Admin\V1\OrderRefundController as AdminOrderRefundController;
 use App\Http\Controllers\Api\Admin\V1\PageController as AdminPageController;
+use App\Http\Controllers\Api\Admin\V1\PlatformOrganizationController as AdminPlatformOrganizationController;
+use App\Http\Controllers\Api\Admin\V1\PlatformStoreController as AdminPlatformStoreController;
 use App\Http\Controllers\Api\Admin\V1\ProductController as AdminProductController;
 use App\Http\Controllers\Api\Admin\V1\SearchIndexController as AdminSearchIndexController;
 use App\Http\Controllers\Api\Admin\V1\ShippingRateController as AdminShippingRateController;
 use App\Http\Controllers\Api\Admin\V1\ShippingZoneController as AdminShippingZoneController;
+use App\Http\Controllers\Api\Admin\V1\StoreInviteController as AdminStoreInviteController;
+use App\Http\Controllers\Api\Admin\V1\StoreMembershipController as AdminStoreMembershipController;
 use App\Http\Controllers\Api\Admin\V1\StoreSettingsController as AdminStoreSettingsController;
@@
 Route::middleware('throttle:60,1')
+    ->prefix('admin/v1/platform')
+    ->name('api.admin.v1.platform.')
+    ->middleware('platform.api')
+    ->group(function (): void {
+        Route::post('organizations', [AdminPlatformOrganizationController::class, 'store'])->name('organizations.store');
+        Route::post('stores', [AdminPlatformStoreController::class, 'store'])->name('stores.store');
+    });
+
+Route::middleware('throttle:60,1')
     ->prefix('admin/v1/stores/{store}')
     ->name('api.admin.v1.')
     ->group(function (): void {
+        Route::middleware('admin.api')->group(function (): void {
+            Route::get('me', [AdminStoreMembershipController::class, 'show'])->name('stores.me');
+        });
+
+        Route::middleware('admin.api:manage-platform')->group(function (): void {
+            Route::post('invites', [AdminStoreInviteController::class, 'store'])->name('stores.invites.store');
+        });
+
         Route::middleware('admin.api:read-products')->group(function (): void {
             Route::get('products', [AdminProductController::class, 'index'])->name('products.index');
             Route::get('products/{product}', [AdminProductController::class, 'show'])->name('products.show');
         });
*** End Patch


Success. Updated the following files:
M routes/api.php

@@ -11,2 +11,4 @@
 use App\Http\Controllers\Api\Admin\V1\PageController as AdminPageController;
+use App\Http\Controllers\Api\Admin\V1\PlatformOrganizationController as AdminPlatformOrganizationController;
+use App\Http\Controllers\Api\Admin\V1\PlatformStoreController as AdminPlatformStoreController;
 use App\Http\Controllers\Api\Admin\V1\ProductController as AdminProductController;
@@ -15,2 +17,4 @@
 use App\Http\Controllers\Api\Admin\V1\ShippingZoneController as AdminShippingZoneController;
+use App\Http\Controllers\Api\Admin\V1\StoreInviteController as AdminStoreInviteController;
+use App\Http\Controllers\Api\Admin\V1\StoreMembershipController as AdminStoreMembershipController;
 use App\Http\Controllers\Api\Admin\V1\StoreSettingsController as AdminStoreSettingsController;
@@ -63,2 +67,11 @@
 Route::middleware('throttle:60,1')
+    ->prefix('admin/v1/platform')
+    ->name('api.admin.v1.platform.')
+    ->middleware('platform.api')
+    ->group(function (): void {
+        Route::post('organizations', [AdminPlatformOrganizationController::class, 'store'])->name('organizations.store');
+        Route::post('stores', [AdminPlatformStoreController::class, 'store'])->name('stores.store');
+    });
+
+Route::middleware('throttle:60,1')
     ->prefix('admin/v1/stores/{store}')
@@ -66,2 +79,10 @@
     ->group(function (): void {
+        Route::middleware('admin.api')->group(function (): void {
+            Route::get('me', [AdminStoreMembershipController::class, 'show'])->name('stores.me');
+        });
+
+        Route::middleware('admin.api:manage-platform')->group(function (): void {
+            Route::post('invites', [AdminStoreInviteController::class, 'store'])->name('stores.invites.store');
+        });
+
         Route::middleware('admin.api:read-products')->group(function (): void {


Output: {"output":"Success. Updated the following files:\nM routes/api.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,80p' tests/Feature/Api/AdminPlatformApiTest.php


sed -n '1,80p' tests/Feature/Api/AdminPlatformApiTest.php

<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Output:
Chunk ID: 61fdb3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 28
Output:
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Input:
*** Begin Patch
*** Update File: tests/Feature/Api/AdminPlatformApiTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Organization;
+use App\Models\Store;
+use App\Models\StoreSettings;
+use App\Models\User;
+use App\Services\WebhookService;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\DB;

-    $response->assertStatus(200);
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function adminPlatformApiStore(): Store
+{
+    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+}
+
+function adminPlatformApiUser(): User
+{
+    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
+}
+
+/**
+ * @param  list<string>  $abilities
+ * @return array{token: \App\Models\OauthToken, plain_text: string}
+ */
+function adminPlatformApiToken(Store $store, array $abilities): array
+{
+    return app(WebhookService::class)->createApiToken($store, 'Platform integration', $abilities);
+}
+
+test('platform api creates organizations and stores for owner sessions', function (): void {
+    $user = adminPlatformApiUser();
+
+    $organizationResponse = $this->actingAs($user)
+        ->postJson('/api/admin/v1/platform/organizations', [
+            'name' => 'Platform API Org',
+            'billing_email' => 'billing@platform-api.test',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.name', 'Platform API Org')
+        ->assertJsonPath('data.billing_email', 'billing@platform-api.test');
+
+    $organizationId = $organizationResponse->json('data.id');
+
+    $this->actingAs($user)
+        ->postJson('/api/admin/v1/platform/stores', [
+            'organization_id' => $organizationId,
+            'name' => 'Platform API Store',
+            'handle' => 'platform-api-store',
+            'default_currency' => 'EUR',
+            'default_locale' => 'en',
+            'timezone' => 'Europe/Berlin',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.name', 'Platform API Store')
+        ->assertJsonPath('data.handle', 'platform-api-store')
+        ->assertJsonPath('data.status', 'active');
+
+    $store = Store::query()->where('handle', 'platform-api-store')->firstOrFail();
+
+    expect(StoreSettings::query()->whereKey($store->getKey())->exists())->toBeTrue()
+        ->and(DB::table('store_users')
+            ->where('store_id', $store->getKey())
+            ->where('user_id', $user->getKey())
+            ->where('role', 'owner')
+            ->exists())->toBeTrue();
+});
+
+test('platform api rejects non owner sessions and tokens without manage platform ability', function (): void {
+    $store = adminPlatformApiStore();
+    $staff = User::factory()->create([
+        'email' => 'platform-staff@example.test',
+    ]);
+    DB::table('store_users')->insert([
+        'store_id' => $store->getKey(),
+        'user_id' => $staff->getKey(),
+        'role' => 'staff',
+        'created_at' => now(),
+    ]);
+    $readToken = adminPlatformApiToken($store, ['read-products']);
+
+    $this->actingAs($staff)
+        ->postJson('/api/admin/v1/platform/organizations', [
+            'name' => 'Forbidden Org',
+            'billing_email' => 'forbidden@example.test',
+        ])
+        ->assertForbidden();
+
+    $this->withToken($readToken['plain_text'])
+        ->postJson('/api/admin/v1/platform/organizations', [
+            'name' => 'Forbidden Token Org',
+            'billing_email' => 'forbidden-token@example.test',
+        ])
+        ->assertForbidden();
+});
+
+test('platform api accepts manage platform bearer tokens', function (): void {
+    $store = adminPlatformApiStore();
+    $token = adminPlatformApiToken($store, ['manage-platform']);
+
+    $this->withToken($token['plain_text'])
+        ->postJson('/api/admin/v1/platform/organizations', [
+            'name' => 'Token Platform Org',
+            'billing_email' => 'token-platform@example.test',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.name', 'Token Platform Org');
+
+    expect($token['token']->refresh()->last_used_at)->not->toBeNull();
+});
+
+test('store me and invite endpoints expose membership and enforce role or ability', function (): void {
+    $store = adminPlatformApiStore();
+    $user = adminPlatformApiUser();
+    $staff = User::factory()->create([
+        'email' => 'store-staff@example.test',
+    ]);
+    DB::table('store_users')->insert([
+        'store_id' => $store->getKey(),
+        'user_id' => $staff->getKey(),
+        'role' => 'staff',
+        'created_at' => now(),
+    ]);
+    $readToken = adminPlatformApiToken($store, ['read-products']);
+    $manageToken = adminPlatformApiToken($store, ['manage-platform']);
+
+    $this->actingAs($user)
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/me")
+        ->assertOk()
+        ->assertJsonPath('data.email', 'admin@acme.test')
+        ->assertJsonPath('data.role', 'owner')
+        ->assertJsonPath('data.permissions.1', 'read-products');
+
+    $this->actingAs($staff)
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'new-staff@example.test',
+            'role' => 'staff',
+        ])
+        ->assertForbidden();
+
+    $this->withToken($readToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'new-staff@example.test',
+            'role' => 'staff',
+        ])
+        ->assertForbidden();
+
+    $this->withToken($manageToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'new-staff@example.test',
+            'role' => 'staff',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.email', 'new-staff@example.test')
+        ->assertJsonPath('data.role', 'staff');
+
+    $this->actingAs($user)
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'admin@acme.test',
+            'role' => 'admin',
+        ])
+        ->assertStatus(409);
 });
*** End Patch


Success. Updated the following files:
M tests/Feature/Api/AdminPlatformApiTest.php

@@ -2,6 +2,169 @@

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Organization;
+use App\Models\Store;
+use App\Models\StoreSettings;
+use App\Models\User;
+use App\Services\WebhookService;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\DB;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});

-    $response->assertStatus(200);
+function adminPlatformApiStore(): Store
+{
+    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+}
+
+function adminPlatformApiUser(): User
+{
+    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
+}
+
+/**
+ * @param  list<string>  $abilities
+ * @return array{token: \App\Models\OauthToken, plain_text: string}
+ */
+function adminPlatformApiToken(Store $store, array $abilities): array
+{
+    return app(WebhookService::class)->createApiToken($store, 'Platform integration', $abilities);
+}
+
+test('platform api creates organizations and stores for owner sessions', function (): void {
+    $user = adminPlatformApiUser();
+
+    $organizationResponse = $this->actingAs($user)
+        ->postJson('/api/admin/v1/platform/organizations', [
+            'name' => 'Platform API Org',
+            'billing_email' => 'billing@platform-api.test',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.name', 'Platform API Org')
+        ->assertJsonPath('data.billing_email', 'billing@platform-api.test');
+
+    $organizationId = $organizationResponse->json('data.id');
+
+    $this->actingAs($user)
+        ->postJson('/api/admin/v1/platform/stores', [
+            'organization_id' => $organizationId,
+            'name' => 'Platform API Store',
+            'handle' => 'platform-api-store',
+            'default_currency' => 'EUR',
+            'default_locale' => 'en',
+            'timezone' => 'Europe/Berlin',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.name', 'Platform API Store')
+        ->assertJsonPath('data.handle', 'platform-api-store')
+        ->assertJsonPath('data.status', 'active');
+
+    $store = Store::query()->where('handle', 'platform-api-store')->firstOrFail();
+
+    expect(StoreSettings::query()->whereKey($store->getKey())->exists())->toBeTrue()
+        ->and(DB::table('store_users')
+            ->where('store_id', $store->getKey())
+            ->where('user_id', $user->getKey())
+            ->where('role', 'owner')
+            ->exists())->toBeTrue();
+});
+
+test('platform api rejects non owner sessions and tokens without manage platform ability', function (): void {
+    $store = adminPlatformApiStore();
+    $staff = User::factory()->create([
+        'email' => 'platform-staff@example.test',
+    ]);
+    DB::table('store_users')->insert([
+        'store_id' => $store->getKey(),
+        'user_id' => $staff->getKey(),
+        'role' => 'staff',
+        'created_at' => now(),
+    ]);
+    $readToken = adminPlatformApiToken($store, ['read-products']);
+
+    $this->actingAs($staff)
+        ->postJson('/api/admin/v1/platform/organizations', [
+            'name' => 'Forbidden Org',
+            'billing_email' => 'forbidden@example.test',
+        ])
+        ->assertForbidden();
+
+    $this->withToken($readToken['plain_text'])
+        ->postJson('/api/admin/v1/platform/organizations', [
+            'name' => 'Forbidden Token Org',
+            'billing_email' => 'forbidden-token@example.test',
+        ])
+        ->assertForbidden();
+});
+
+test('platform api accepts manage platform bearer tokens', function (): void {
+    $store = adminPlatformApiStore();
+    $token = adminPlatformApiToken($store, ['manage-platform']);
+
+    $this->withToken($token['plain_text'])
+        ->postJson('/api/admin/v1/platform/organizations', [
+            'name' => 'Token Platform Org',
+            'billing_email' => 'token-platform@example.test',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.name', 'Token Platform Org');
+
+    expect($token['token']->refresh()->last_used_at)->not->toBeNull();
+});
+
+test('store me and invite endpoints expose membership and enforce role or ability', function (): void {
+    $store = adminPlatformApiStore();
+    $user = adminPlatformApiUser();
+    $staff = User::factory()->create([
+        'email' => 'store-staff@example.test',
+    ]);
+    DB::table('store_users')->insert([
+        'store_id' => $store->getKey(),
+        'user_id' => $staff->getKey(),
+        'role' => 'staff',
+        'created_at' => now(),
+    ]);
+    $readToken = adminPlatformApiToken($store, ['read-products']);
+    $manageToken = adminPlatformApiToken($store, ['manage-platform']);
+
+    $this->actingAs($user)
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/me")
+        ->assertOk()
+        ->assertJsonPath('data.email', 'admin@acme.test')
+        ->assertJsonPath('data.role', 'owner')
+        ->assertJsonPath('data.permissions.1', 'read-products');
+
+    $this->actingAs($staff)
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'new-staff@example.test',
+            'role' => 'staff',
+        ])
+        ->assertForbidden();
+
+    $this->withToken($readToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'new-staff@example.test',
+            'role' => 'staff',
+        ])
+        ->assertForbidden();
+
+    $this->withToken($manageToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'new-staff@example.test',
+            'role' => 'staff',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.email', 'new-staff@example.test')
+        ->assertJsonPath('data.role', 'staff');
+
+    $this->actingAs($user)
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'admin@acme.test',
+            'role' => 'admin',
+        ])
+        ->assertStatus(409);
 });


Output: {"output":"Success. Updated the following files:\nM tests/Feature/Api/AdminPlatformApiTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


The platform endpoints are wired in now. I’m checking syntax/routes first, then I’ll run the new platform API tests and the API feature directory.


php artisan route:list --path=api/admin/v1 --except-vendor


No syntax errors detected in app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php


No syntax errors detected in app/Http/Controllers/Api/Admin/V1/StoreInviteController.php


No syntax errors detected in app/Http/Middleware/AuthenticatePlatformApi.php


No syntax errors detected in app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php


No syntax errors detected in app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php


Output:
Chunk ID: 5d7559
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 20
Output:
No syntax errors detected in app/Http/Middleware/AuthenticatePlatformApi.php


Output:
Chunk ID: 85efad
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 25
Output:
No syntax errors detected in app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php


Output:
Chunk ID: 75dbf6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 23
Output:
No syntax errors detected in app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php


Output:
Chunk ID: 2a0851
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 23
Output:
No syntax errors detected in app/Http/Controllers/Api/Admin/V1/StoreInviteController.php


Output:
Chunk ID: 1f8553
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 24
Output:
No syntax errors detected in app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php


Output:
Chunk ID: dea469
Wall time: 0.2683 seconds
Process exited with code 0
Original token count: 937
Output:

  POST      api/admin/v1/platform/organizations api.admin.v1.platform.organiz…
  POST      api/admin/v1/platform/stores api.admin.v1.platform.stores.store  …
  GET|HEAD  api/admin/v1/stores/{store}/analytics/summary api.admin.v1.analyt…
  GET|HEAD  api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  POST      api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  PUT       api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  DELETE    api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  GET|HEAD  api/admin/v1/stores/{store}/customers api.admin.v1.customers.inde…
  GET|HEAD  api/admin/v1/stores/{store}/customers/{customer} api.admin.v1.cus…
  GET|HEAD  api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.inde…
  POST      api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.stor…
  PUT       api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  DELETE    api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  POST      api/admin/v1/stores/{store}/exports/orders api.admin.v1.exports.o…
  GET|HEAD  api/admin/v1/stores/{store}/exports/{dataExport} api.admin.v1.exp…
  POST      api/admin/v1/stores/{store}/invites api.admin.v1.stores.invites.s…
  GET|HEAD  api/admin/v1/stores/{store}/me api.admin.v1.stores.me › Api\Admin…
  GET|HEAD  api/admin/v1/stores/{store}/orders api.admin.v1.orders.index › Ap…
  GET|HEAD  api/admin/v1/stores/{store}/orders/{order} api.admin.v1.orders.sh…
  POST      api/admin/v1/stores/{store}/orders/{order}/fulfillments api.admin…
  POST      api/admin/v1/stores/{store}/orders/{order}/refunds api.admin.v1.o…
  GET|HEAD  api/admin/v1/stores/{store}/pages api.admin.v1.pages.index › Api\…
  POST      api/admin/v1/stores/{store}/pages api.admin.v1.pages.store › Api\…
  PUT       api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.updat…
  DELETE    api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.destr…
  GET|HEAD  api/admin/v1/stores/{store}/products api.admin.v1.products.index …
  POST      api/admin/v1/stores/{store}/products api.admin.v1.products.store …
  GET|HEAD  api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  PUT       api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  DELETE    api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  POST      api/admin/v1/stores/{store}/products/{product}/media/presign-upload api.admin.v1.products.media.presign-upload › Api\Admin\V1\ProductController@presignUpl…
  POST      api/admin/v1/stores/{store}/search/reindex api.admin.v1.search.re…
  GET|HEAD  api/admin/v1/stores/{store}/search/status api.admin.v1.search.sta…
  GET|HEAD  api/admin/v1/stores/{store}/settings api.admin.v1.settings.show  …
  PUT       api/admin/v1/stores/{store}/settings api.admin.v1.settings.update…
  GET|HEAD  api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  POST      api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  PUT       api/admin/v1/stores/{store}/shipping/zones/{shippingZone} api.adm…
  POST      api/admin/v1/stores/{store}/shipping/zones/{shippingZone}/rates a…
  GET|HEAD  api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  PUT       api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  POST      api/admin/v1/stores/{store}/themes api.admin.v1.themes.store › Ap…
  POST      api/admin/v1/stores/{store}/themes/{theme}/publish api.admin.v1.t…
  PUT       api/admin/v1/stores/{store}/themes/{theme}/settings api.admin.v1.…

                                                           Showing [44] routes


php artisan route:list --path=api/admin/v1 --except-vendor

  POST      api/admin/v1/platform/organizations api.admin.v1.platform.organiz…
  POST      api/admin/v1/platform/stores api.admin.v1.platform.stores.store  …
  GET|HEAD  api/admin/v1/stores/{store}/analytics/summary api.admin.v1.analyt…
  GET|HEAD  api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  POST      api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  PUT       api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  DELETE    api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  GET|HEAD  api/admin/v1/stores/{store}/customers api.admin.v1.customers.inde…
  GET|HEAD  api/admin/v1/stores/{store}/customers/{customer} api.admin.v1.cus…
  GET|HEAD  api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.inde…
  POST      api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.stor…
  PUT       api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  DELETE    api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  POST      api/admin/v1/stores/{store}/exports/orders api.admin.v1.exports.o…
  GET|HEAD  api/admin/v1/stores/{store}/exports/{dataExport} api.admin.v1.exp…
  POST      api/admin/v1/stores/{store}/invites api.admin.v1.stores.invites.s…
  GET|HEAD  api/admin/v1/stores/{store}/me api.admin.v1.stores.me › Api\Admin…
  GET|HEAD  api/admin/v1/stores/{store}/orders api.admin.v1.orders.index › Ap…
  GET|HEAD  api/admin/v1/stores/{store}/orders/{order} api.admin.v1.orders.sh…
  POST      api/admin/v1/stores/{store}/orders/{order}/fulfillments api.admin…
  POST      api/admin/v1/stores/{store}/orders/{order}/refunds api.admin.v1.o…
  GET|HEAD  api/admin/v1/stores/{store}/pages api.admin.v1.pages.index › Api\…
  POST      api/admin/v1/stores/{store}/pages api.admin.v1.pages.store › Api\…
  PUT       api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.updat…
  DELETE    api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.destr…
  GET|HEAD  api/admin/v1/stores/{store}/products api.admin.v1.products.index …
  POST      api/admin/v1/stores/{store}/products api.admin.v1.products.store …
  GET|HEAD  api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  PUT       api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  DELETE    api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  POST      api/admin/v1/stores/{store}/products/{product}/media/presign-upload api.admin.v1.products.media.presign-upload › Api\Admin\V1\ProductController@presignUpl…
  POST      api/admin/v1/stores/{store}/search/reindex api.admin.v1.search.re…
  GET|HEAD  api/admin/v1/stores/{store}/search/status api.admin.v1.search.sta…
  GET|HEAD  api/admin/v1/stores/{store}/settings api.admin.v1.settings.show  …
  PUT       api/admin/v1/stores/{store}/settings api.admin.v1.settings.update…
  GET|HEAD  api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  POST      api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  PUT       api/admin/v1/stores/{store}/shipping/zones/{shippingZone} api.adm…
  POST      api/admin/v1/stores/{store}/shipping/zones/{shippingZone}/rates a…
  GET|HEAD  api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  PUT       api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  POST      api/admin/v1/stores/{store}/themes api.admin.v1.themes.store › Ap…
  POST      api/admin/v1/stores/{store}/themes/{theme}/publish api.admin.v1.t…
  PUT       api/admin/v1/stores/{store}/themes/{theme}/settings api.admin.v1.…

                                                           Showing [44] routes


php artisan test --compact tests/Feature/Api/AdminPlatformApiTest.php


Output:
Chunk ID: a05239
Wall time: 2.5016 seconds
Process exited with code 1
Original token count: 472
Output:

  ⨯..⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Api\AdminPlatformApiTest > platform api creates org…
  Failed asserting that null is identical to 'active'.

  at tests/Feature/Api/AdminPlatformApiTest.php:64
     60▕         ])
     61▕         ->assertCreated()
     62▕         ->assertJsonPath('data.name', 'Platform API Store')
     63▕         ->assertJsonPath('data.handle', 'platform-api-store')
  ➜  64▕         ->assertJsonPath('data.status', 'active');
     65▕
     66▕     $store = Store::query()->where('handle', 'platform-api-store')->firstOrFail();
     67▕
     68▕     expect(StoreSettings::query()->whereKey($store->getKey())->exists())->toBeTrue()

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Api\AdminPlatformApiTest > store me and invite endp…
  Expected response status code [201] but received 403.
Failed asserting that 403 is identical to 201.

  at tests/Feature/Api/AdminPlatformApiTest.php:160
    156▕         ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
    157▕             'email' => 'new-staff@example.test',
    158▕             'role' => 'staff',
    159▕         ])
  ➜ 160▕         ->assertCreated()
    161▕         ->assertJsonPath('data.email', 'new-staff@example.test')
    162▕         ->assertJsonPath('data.role', 'staff');
    163▕
    164▕     $this->actingAs($user)

  Tests:    2 failed, 2 passed (19 assertions)
  Duration: 2.24s


php artisan test --compact tests/Feature/Api/AdminPlatformApiTest.php

  ⨯..⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Api\AdminPlatformApiTest > platform api creates org…
  Failed asserting that null is identical to 'active'.

  at tests/Feature/Api/AdminPlatformApiTest.php:64
     60▕         ])
     61▕         ->assertCreated()
     62▕         ->assertJsonPath('data.name', 'Platform API Store')
     63▕         ->assertJsonPath('data.handle', 'platform-api-store')
  ➜  64▕         ->assertJsonPath('data.status', 'active');
     65▕
     66▕     $store = Store::query()->where('handle', 'platform-api-store')->firstOrFail();
     67▕
     68▕     expect(StoreSettings::query()->whereKey($store->getKey())->exists())->toBeTrue()

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Api\AdminPlatformApiTest > store me and invite endp…
  Expected response status code [201] but received 403.
Failed asserting that 403 is identical to 201.

  at tests/Feature/Api/AdminPlatformApiTest.php:160
    156▕         ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
    157▕             'email' => 'new-staff@example.test',
    158▕             'role' => 'staff',
    159▕         ])
  ➜ 160▕         ->assertCreated()
    161▕         ->assertJsonPath('data.email', 'new-staff@example.test')
    162▕         ->assertJsonPath('data.role', 'staff');
    163▕
    164▕     $this->actingAs($user)

  Tests:    2 failed, 2 passed (19 assertions)
  Duration: 2.24s


Input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php
@@
                 'organization_id' => $validated['organization_id'],
                 'name' => $validated['name'],
                 'handle' => $validated['handle'],
+                'status' => 'active',
                 'default_currency' => strtoupper((string) $validated['default_currency']),
*** End Patch


@@ -23,2 +23,3 @@
                 'handle' => $validated['handle'],
+                'status' => 'active',
                 'default_currency' => strtoupper((string) $validated['default_currency']),


Output: {"output":"Success. Updated the following files:\nM app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: tests/Feature/Api/AdminPlatformApiTest.php
@@
     $staff = User::factory()->create([
         'email' => 'store-staff@example.test',
     ]);
@@
     ]);
     $readToken = adminPlatformApiToken($store, ['read-products']);
     $manageToken = adminPlatformApiToken($store, ['manage-platform']);

+    $this->withToken($readToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'new-staff@example.test',
+            'role' => 'staff',
+        ])
+        ->assertForbidden();
+
+    $this->withToken($manageToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
+            'email' => 'new-staff@example.test',
+            'role' => 'staff',
+        ])
+        ->assertCreated()
+        ->assertJsonPath('data.email', 'new-staff@example.test')
+        ->assertJsonPath('data.role', 'staff');
+
     $this->actingAs($user)
         ->getJson("/api/admin/v1/stores/{$store->getKey()}/me")
         ->assertOk()
@@
         ])
         ->assertForbidden();
-
-    $this->withToken($readToken['plain_text'])
-        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
-            'email' => 'new-staff@example.test',
-            'role' => 'staff',
-        ])
-        ->assertForbidden();
-
-    $this->withToken($manageToken['plain_text'])
-        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
-            'email' => 'new-staff@example.test',
-            'role' => 'staff',
-        ])
-        ->assertCreated()
-        ->assertJsonPath('data.email', 'new-staff@example.test')
-        ->assertJsonPath('data.role', 'staff');

     $this->actingAs($user)
*** End Patch


@@ -133,10 +133,3 @@

-    $this->actingAs($user)
-        ->getJson("/api/admin/v1/stores/{$store->getKey()}/me")
-        ->assertOk()
-        ->assertJsonPath('data.email', 'admin@acme.test')
-        ->assertJsonPath('data.role', 'owner')
-        ->assertJsonPath('data.permissions.1', 'read-products');
-
-    $this->actingAs($staff)
+    $this->withToken($readToken['plain_text'])
         ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
@@ -147,3 +140,3 @@

-    $this->withToken($readToken['plain_text'])
+    $this->withToken($manageToken['plain_text'])
         ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
@@ -152,5 +145,14 @@
         ])
-        ->assertForbidden();
+        ->assertCreated()
+        ->assertJsonPath('data.email', 'new-staff@example.test')
+        ->assertJsonPath('data.role', 'staff');
+
+    $this->actingAs($user)
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/me")
+        ->assertOk()
+        ->assertJsonPath('data.email', 'admin@acme.test')
+        ->assertJsonPath('data.role', 'owner')
+        ->assertJsonPath('data.permissions.1', 'read-products');

-    $this->withToken($manageToken['plain_text'])
+    $this->actingAs($staff)
         ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
@@ -159,5 +161,3 @@
         ])
-        ->assertCreated()
-        ->assertJsonPath('data.email', 'new-staff@example.test')
-        ->assertJsonPath('data.role', 'staff');
+        ->assertForbidden();


Output: {"output":"Success. Updated the following files:\nM tests/Feature/Api/AdminPlatformApiTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Output:
Chunk ID: af4756
Wall time: 2.0979 seconds
Process exited with code 0
Original token count: 17
Output:

  ....

  Tests:    4 passed (24 assertions)
  Duration: 1.86s


  ....

  Tests:    4 passed (24 assertions)
  Duration: 1.86s


The new platform API test is green after tightening the store status payload and avoiding session-auth leakage in token assertions. I’m formatting the slice and running the full API feature set next.


Output:
Chunk ID: f66fc9
Wall time: 0.4306 seconds
Process exited with code 0
Original token count: 29
Output:
{"result":"fixed","files":[{"path":"tests\/Feature\/Api\/AdminPlatformApiTest.php","fixers":["no_unused_imports"]}]}


/bin/zsh
-lc
vendor/bin/pint --dirty --format agent


php artisan test --compact tests/Feature/Api


Output:
Chunk ID: e48318
Wall time: 1.8507 seconds
Process exited with code 0
Original token count: 17
Output:

  ....

  Tests:    4 passed (24 assertions)
  Duration: 1.74s


  ....

  Tests:    4 passed (24 assertions)
  Duration: 1.74s


Output:
Chunk ID: 11e1d2
Wall time: 23.1169 seconds
Process exited with code 0
Original token count: 32
Output:

  .............................................................

  Tests:    61 passed (504 assertions)
  Duration: 23.00s


The full API feature directory passes now: 61 tests and 504 assertions. I’m updating progress with the platform-route audit fix and then I’ll commit this slice separately.


Input:
*** Begin Patch
*** Update File: specs/progress.md
@@
-| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product list/detail/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
+| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes platform organization/store creation, store invite and membership endpoints, store-scoped product list/detail/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
@@
-| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final completion audit found and closed the missing admin product write API surface; the audit continues. |
+| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final completion audit found and closed the missing admin product write API plus platform/membership API surfaces; the audit continues. |
*** End Patch


Success. Updated the following files:
M specs/progress.md

@@ -29,3 +29,3 @@
 | Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables including `navigation_items.parent_id` nesting, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics/apps/webhooks/export tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries, and data_exports. |
-| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product list/detail/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
+| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes platform organization/store creation, store invite and membership endpoints, store-scoped product list/detail/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
 | Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, nested navigation menu editor with drag sorting and parent selection, theme grid, theme settings editor with iframe preview, theme file list/editor, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. |
@@ -36,3 +36,3 @@
 | Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | complete | Manual Playwright MCP smoke coverage has verified storefront catalog browsing on `shop.test` and `acme-fashion.test`, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search API JSON, analytics event ingestion API, DB-backed content pages, seeded nested navigation dropdowns, admin dashboard on desktop/mobile, admin analytics, admin app list/detail, admin developers token/webhook flows, admin product/collection/inventory/order/customer/discount/content/navigation/settings/theme/search pages, admin checkout/notification settings pages, admin product media section rendering, admin analytics CSV export, admin customer address creation, admin discount creation/editing/filtering, admin page creation, admin nested navigation rendering/item creation/save, admin theme editor preview, admin theme file list/editor, admin search reindexing, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated Pest browser coverage now checks all 18 Spec 08 browser suites with 143 tests: Suite 1 smoke pages/critical-page batch, Suite 2 admin authentication login/validation/redirect/logout/sidebar navigation interactions, Suite 3 admin product listing/create/edit/archive/search/status-filter/draft visibility interactions, Suite 4 admin order listing/filtering/detail/timeline/fulfillment/refund/bank-payment/guard/shipment interactions, Suite 5 admin discount seeded listing/percent/fixed/free-shipping creation/edit/status interactions, Suite 6 admin general/domain/shipping/tax settings interactions, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account registration/login/orders/addresses/logout, Suite 11 inventory policy enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile flows, Suite 14 accessibility checks, Suite 15 admin collection listing/create/edit interactions, Suite 16 admin customer listing/detail/order-history/address interactions, Suite 17 admin page listing/create/edit interactions, and Suite 18 admin analytics dashboard/KPI/funnel interactions. |
-| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final completion audit found and closed the missing admin product write API surface; the audit continues. |
+| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final completion audit found and closed the missing admin product write API plus platform/membership API surfaces; the audit continues. |


Output: {"output":"Success. Updated the following files:\nM specs/progress.md\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: specs/progress.md
@@
 - 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin product API write-route changes.
 - 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminCatalogApiTest.php` passed after adding product create/update/archive/media-presign/token-ability coverage: 7 tests, 63 assertions.
 - 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin product API write-route changes: 57 tests, 480 assertions.
+- 2026-05-04: Final audit found the remaining Spec 02 platform/membership API route gap; `mcp__laravel_boost__.search_docs` consulted Laravel 12 route middleware, FormRequest, transaction, and HTTP API testing docs before closing it.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1 --except-vendor` confirmed `POST /api/admin/v1/platform/organizations`, `POST /api/admin/v1/platform/stores`, `GET /api/admin/v1/stores/{store}/me`, and `POST /api/admin/v1/stores/{store}/invites`.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the platform/membership API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminPlatformApiTest.php` passed for platform organization/store creation, membership, invite, role, and token ability coverage: 4 tests, 24 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the platform/membership API changes: 61 tests, 504 assertions.
@@
-- Admin REST API routes use the `admin.api` middleware with session-authenticated store users or hashed `oauth_tokens` bearer tokens; the current implemented surfaces are product read/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, search status/reindex, analytics summary, shipping zone/rate settings, tax settings read/update, and order fulfillment/refund mutations.
+- Admin REST API routes use the `admin.api` middleware with session-authenticated store users or hashed `oauth_tokens` bearer tokens; the current implemented surfaces are platform organization/store creation, store membership/invite endpoints, product read/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, search status/reindex, analytics summary, shipping zone/rate settings, tax settings read/update, and order fulfillment/refund mutations.
@@
-Not complete until the final completion audit is closed. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, deterministic Spec 07 customer/address/order/payment/fulfillment/refund seed data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, admin product create/update/archive/media-presign API surfaces, deferred OAuth/app route stubs, all 18 Spec 08 automated browser suites, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented.
+Not complete until the final completion audit is closed. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, deterministic Spec 07 customer/address/order/payment/fulfillment/refund seed data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, platform/store-membership APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, admin product create/update/archive/media-presign API surfaces, deferred OAuth/app route stubs, all 18 Spec 08 automated browser suites, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented.
*** End Patch


@@ -577,2 +577,7 @@
 - 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin product API write-route changes: 57 tests, 480 assertions.
+- 2026-05-04: Final audit found the remaining Spec 02 platform/membership API route gap; `mcp__laravel_boost__.search_docs` consulted Laravel 12 route middleware, FormRequest, transaction, and HTTP API testing docs before closing it.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1 --except-vendor` confirmed `POST /api/admin/v1/platform/organizations`, `POST /api/admin/v1/platform/stores`, `GET /api/admin/v1/stores/{store}/me`, and `POST /api/admin/v1/stores/{store}/invites`.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the platform/membership API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminPlatformApiTest.php` passed for platform organization/store creation, membership, invite, role, and token ability coverage: 4 tests, 24 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the platform/membership API changes: 61 tests, 504 assertions.

@@ -610,3 +615,3 @@
 - Storefront order lookup uses a deterministic HMAC access token derived from store/order identity so confirmation links can be token-gated without adding a new runtime column.
-- Admin REST API routes use the `admin.api` middleware with session-authenticated store users or hashed `oauth_tokens` bearer tokens; the current implemented surfaces are product read/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, search status/reindex, analytics summary, shipping zone/rate settings, tax settings read/update, and order fulfillment/refund mutations.
+- Admin REST API routes use the `admin.api` middleware with session-authenticated store users or hashed `oauth_tokens` bearer tokens; the current implemented surfaces are platform organization/store creation, store membership/invite endpoints, product read/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, search status/reindex, analytics summary, shipping zone/rate settings, tax settings read/update, and order fulfillment/refund mutations.
 - Admin dashboard sales and top-product metrics are derived from paid or partially refunded orders; the dedicated analytics page now reads analytics event and daily aggregate tables.
@@ -673,2 +678,2 @@

-Not complete until the final completion audit is closed. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, deterministic Spec 07 customer/address/order/payment/fulfillment/refund seed data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, admin product create/update/archive/media-presign API surfaces, deferred OAuth/app route stubs, all 18 Spec 08 automated browser suites, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented.
+Not complete until the final completion audit is closed. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, deterministic Spec 07 customer/address/order/payment/fulfillment/refund seed data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, platform/store-membership APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, admin product create/update/archive/media-presign API surfaces, deferred OAuth/app route stubs, all 18 Spec 08 automated browser suites, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented.


Output: {"output":"Success. Updated the following files:\nM specs/progress.md\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


 bootstrap/app.php |  2 ++
 routes/api.php    | 21 +++++++++++++++++++++
 specs/progress.md | 13 +++++++++----
 3 files changed, 32 insertions(+), 4 deletions(-)


 M bootstrap/app.php
 M routes/api.php
 M specs/progress.md
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
?? .playwright-mcp/console-2026-05-04T13-33-19-511Z.log
?? app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php
?? app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php
?? app/Http/Controllers/Api/Admin/V1/StoreInviteController.php
?? app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php
?? app/Http/Middleware/AuthenticatePlatformApi.php
?? app/Http/Requests/Api/Admin/V1/StoreInviteRequest.php
?? app/Http/Requests/Api/Admin/V1/StorePlatformOrganizationRequest.php
?? app/Http/Requests/Api/Admin/V1/StorePlatformStoreRequest.php
?? app/Http/Resources/Admin/V1/OrganizationResource.php
?? app/Http/Resources/Admin/V1/StoreResource.php
?? tests/Feature/Api/AdminPlatformApiTest.php


Output:
Chunk ID: 3ca0af
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


Output:
Chunk ID: 0dbe31
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 41
Output:
 bootstrap/app.php |  2 ++
 routes/api.php    | 21 +++++++++++++++++++++
 specs/progress.md | 13 +++++++++----
 3 files changed, 32 insertions(+), 4 deletions(-)


Output:
Chunk ID: 651763
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 223
Output:
 M bootstrap/app.php
 M routes/api.php
 M specs/progress.md
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
?? .playwright-mcp/console-2026-05-04T13-33-19-511Z.log
?? app/Http/Controllers/Api/Admin/V1/PlatformOrganizationController.php
?? app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php
?? app/Http/Controllers/Api/Admin/V1/StoreInviteController.php
?? app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php
?? app/Http/Middleware/AuthenticatePlatformApi.php
?? app/Http/Requests/Api/Admin/V1/StoreInviteRequest.php
?? app/Http/Requests/Api/Admin/V1/StorePlatformOrganizationRequest.php
?? app/Http/Requests/Api/Admin/V1/StorePlatformStoreRequest.php
?? app/Http/Resources/Admin/V1/OrganizationResource.php
?? app/Http/Resources/Admin/V1/StoreResource.php
?? tests/Feature/Api/AdminPlatformApiTest.php


sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php


sed -n '1,220p' app/Http/Middleware/AuthenticatePlatformApi.php


sed -n '1,260p' tests/Feature/Api/AdminPlatformApiTest.php


sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/PlatformStoreController.php

<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Admin\V1\StorePlatformStoreRequest;
use App\Http\Resources\Admin\V1\StoreResource;
use App\Models\Store;
use App\Models\StoreSettings;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;

class PlatformStoreController extends Controller
{
    public function store(StorePlatformStoreRequest $request): JsonResponse
    {
        $validated = $request->validated();

        $store = DB::transaction(function () use ($request, $validated): Store {
            $store = Store::query()->create([
                'organization_id' => $validated['organization_id'],
                'name' => $validated['name'],
                'handle' => $validated['handle'],
                'status' => 'active',
                'default_currency' => strtoupper((string) $validated['default_currency']),
                'default_locale' => $validated['default_locale'],
                'timezone' => $validated['timezone'],
            ]);

            StoreSettings::query()->create([
                'store_id' => $store->getKey(),
                'settings_json' => [],
            ]);

            $user = $request->user();

            if ($user instanceof \App\Models\User) {
                $store->users()->syncWithoutDetaching([
                    $user->getKey() => ['role' => 'owner', 'created_at' => now()],
                ]);
            }

            return $store;
        });

        return StoreResource::make($store)
            ->response()
            ->setStatusCode(201);
    }
}


sed -n '1,220p' app/Http/Middleware/AuthenticatePlatformApi.php

<?php

namespace App\Http\Middleware;

use App\Enums\AppInstallationStatus;
use App\Enums\StoreUserRole;
use App\Models\OauthToken;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class AuthenticatePlatformApi
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        app()->forgetInstance('admin_api_oauth_token');

        $user = $request->user();

        if ($user instanceof User) {
            if ($this->userCanManagePlatform($user)) {
                return $next($request);
            }

            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $plainTextToken = $request->bearerToken();

        if (! is_string($plainTextToken) || $plainTextToken === '') {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $token = OauthToken::query()
            ->with('installation')
            ->where('access_token_hash', hash('sha256', $plainTextToken))
            ->first();

        if (! $token instanceof OauthToken || $token->isExpired()) {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $installation = $token->installation;

        if ($installation === null || $installation->status !== AppInstallationStatus::Active) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        if (! $this->hasManagePlatformAbility($token)) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $token->forceFill(['last_used_at' => now()])->save();

        $request->attributes->set('admin_api_oauth_token', $token);
        app()->instance('admin_api_oauth_token', $token);

        return $next($request);
    }

    private function userCanManagePlatform(User $user): bool
    {
        return $user->stores()
            ->wherePivot('role', StoreUserRole::Owner->value)
            ->exists();
    }

    private function hasManagePlatformAbility(OauthToken $token): bool
    {
        $abilities = $token->abilities_json ?? [];

        return in_array('*', $abilities, true) || in_array('manage-platform', $abilities, true);
    }
}