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

namespace Database\Factories;

use App\Enums\FulfillmentShipmentStatus;
use App\Models\Order;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Fulfillment>
 */
class FulfillmentFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'order_id' => Order::factory(),
            'status' => FulfillmentShipmentStatus::Pending,
            'tracking_company' => null,
            'tracking_number' => null,
            'tracking_url' => null,
            'shipped_at' => null,
            'delivered_at' => null,
        ];
    }

    public function withTracking(): static
    {
        return $this->state(fn (array $attributes): array => [
            'tracking_company' => fake()->randomElement(['DHL', 'UPS', 'DPD']),
            'tracking_number' => fake()->bothify('??##########'),
            'tracking_url' => fake()->url(),
        ]);
    }

    public function shipped(): static
    {
        return $this->withTracking()->state(fn (array $attributes): array => [
            'status' => FulfillmentShipmentStatus::Shipped,
            'shipped_at' => now(),
        ]);
    }

    public function delivered(): static
    {
        return $this->withTracking()->state(fn (array $attributes): array => [
            'status' => FulfillmentShipmentStatus::Delivered,
            'shipped_at' => now()->subDay(),
            'delivered_at' => now(),
        ]);
    }
}


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

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

-    $response->assertStatus(200);
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function adminOrderExportApiStore(): Store
+{
+    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+}
+
+function adminOrderExportApiUser(): User
+{
+    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
+}
+
+/**
+ * @param  list<string>  $abilities
+ * @return array{token: \App\Models\OauthToken, plain_text: string}
+ */
+function adminOrderExportApiToken(Store $store, array $abilities): array
+{
+    return app(WebhookService::class)->createApiToken($store, 'Order export integration', $abilities);
+}
+
+function adminOrderExportApiOrder(Store $store, array $attributes = []): Order
+{
+    return Order::factory()->paid()->create([
+        'store_id' => $store->getKey(),
+        'email' => 'export@example.test',
+        'subtotal_amount' => 2500,
+        'discount_amount' => 100,
+        'shipping_amount' => 500,
+        'tax_amount' => 0,
+        'total_amount' => 2900,
+        'currency' => $store->default_currency,
+        ...$attributes,
+    ]);
+}
+
+test('admin order export api creates completed csv exports', function (): void {
+    Storage::fake('local');
+
+    $store = adminOrderExportApiStore();
+    $matching = adminOrderExportApiOrder($store, [
+        'order_number' => '#EX1001',
+        'created_at' => now()->subDays(2),
+    ]);
+    Fulfillment::factory()->shipped()->create([
+        'order_id' => $matching->getKey(),
+        'tracking_number' => 'TRACK-1001',
+    ]);
+    adminOrderExportApiOrder($store, [
+        'order_number' => '#EX2001',
+        'status' => 'pending',
+        'financial_status' => 'pending',
+        'created_at' => now()->subDays(2),
+    ]);
+    adminOrderExportApiOrder(Store::factory()->create(), [
+        'order_number' => '#EX3001',
+        'created_at' => now()->subDays(2),
+    ]);
+
+    $createResponse = $this->actingAs(adminOrderExportApiUser())
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/exports/orders", [
+            'format' => 'csv',
+            'filters' => [
+                'status' => 'paid',
+                'created_after' => now()->subDays(3)->toIso8601String(),
+                'created_before' => now()->subDay()->toIso8601String(),
+            ],
+        ])
+        ->assertAccepted()
+        ->assertJsonPath('status', 'completed');
+
+    $export = DataExport::query()->findOrFail($createResponse->json('export_id'));
+
+    Storage::disk('local')->assertExists($export->storage_key);
+
+    $showResponse = $this->actingAs(adminOrderExportApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/exports/{$export->getKey()}")
+        ->assertOk()
+        ->assertJsonPath('data.status', 'completed')
+        ->assertJsonPath('data.format', 'csv')
+        ->assertJsonPath('data.row_count', 1)
+        ->assertJsonPath('data.download_expires_at', $export->download_expires_at?->toIso8601String());
+
+    $csv = rawurldecode(Str::after((string) $showResponse->json('data.download_url'), ','));
+
+    expect($csv)->toContain('order_number,created_at,status')
+        ->and($csv)->toContain('#EX1001')
+        ->and($csv)->toContain('TRACK-1001')
+        ->and($csv)->not->toContain('#EX2001')
+        ->and($csv)->not->toContain('#EX3001');
+});
+
+test('admin order export api enforces token abilities and store scope', function (): void {
+    Storage::fake('local');
+
+    $store = adminOrderExportApiStore();
+    adminOrderExportApiOrder($store, ['order_number' => '#TOKEN-EXPORT']);
+    $readToken = adminOrderExportApiToken($store, ['read-orders']);
+    $wrongAbilityToken = adminOrderExportApiToken($store, ['read-products']);
+    $otherStoreToken = adminOrderExportApiToken(Store::factory()->create(), ['read-orders']);
+
+    $this->withToken($wrongAbilityToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/exports/orders", [
+            'format' => 'csv',
+        ])
+        ->assertForbidden();
+
+    $createResponse = $this->withToken($readToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/exports/orders", [
+            'format' => 'csv',
+        ])
+        ->assertAccepted();
+
+    $export = DataExport::query()->findOrFail($createResponse->json('export_id'));
+
+    $this->withToken($readToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/exports/{$export->getKey()}")
+        ->assertOk();
+
+    $this->withToken($otherStoreToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/exports/{$export->getKey()}")
+        ->assertForbidden();
+
+    $otherStoreExport = DataExport::factory()->create([
+        'store_id' => Store::factory()->create()->getKey(),
+    ]);
+
+    $this->actingAs(adminOrderExportApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/exports/{$otherStoreExport->getKey()}")
+        ->assertNotFound();
+});
+
+test('admin order export api validates format and filters', function (): void {
+    $store = adminOrderExportApiStore();
+
+    $this->actingAs(adminOrderExportApiUser())
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/exports/orders", [
+            'format' => 'xlsx',
+            'filters' => [
+                'status' => 'lost',
+                'created_after' => now()->toIso8601String(),
+                'created_before' => now()->subDay()->toIso8601String(),
+            ],
+        ])
+        ->assertUnprocessable()
+        ->assertJsonValidationErrors(['format', 'filters.status', 'filters.created_before']);
 });
*** End Patch


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

@@ -2,6 +2,164 @@

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\DataExport;
+use App\Models\Fulfillment;
+use App\Models\Order;
+use App\Models\Store;
+use App\Models\User;
+use App\Services\WebhookService;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function adminOrderExportApiStore(): Store
+{
+    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+}
+
+function adminOrderExportApiUser(): User
+{
+    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
+}
+
+/**
+ * @param  list<string>  $abilities
+ * @return array{token: \App\Models\OauthToken, plain_text: string}
+ */
+function adminOrderExportApiToken(Store $store, array $abilities): array
+{
+    return app(WebhookService::class)->createApiToken($store, 'Order export integration', $abilities);
+}
+
+function adminOrderExportApiOrder(Store $store, array $attributes = []): Order
+{
+    return Order::factory()->paid()->create([
+        'store_id' => $store->getKey(),
+        'email' => 'export@example.test',
+        'subtotal_amount' => 2500,
+        'discount_amount' => 100,
+        'shipping_amount' => 500,
+        'tax_amount' => 0,
+        'total_amount' => 2900,
+        'currency' => $store->default_currency,
+        ...$attributes,
+    ]);
+}
+
+test('admin order export api creates completed csv exports', function (): void {
+    Storage::fake('local');
+
+    $store = adminOrderExportApiStore();
+    $matching = adminOrderExportApiOrder($store, [
+        'order_number' => '#EX1001',
+        'created_at' => now()->subDays(2),
+    ]);
+    Fulfillment::factory()->shipped()->create([
+        'order_id' => $matching->getKey(),
+        'tracking_number' => 'TRACK-1001',
+    ]);
+    adminOrderExportApiOrder($store, [
+        'order_number' => '#EX2001',
+        'status' => 'pending',
+        'financial_status' => 'pending',
+        'created_at' => now()->subDays(2),
+    ]);
+    adminOrderExportApiOrder(Store::factory()->create(), [
+        'order_number' => '#EX3001',
+        'created_at' => now()->subDays(2),
+    ]);
+
+    $createResponse = $this->actingAs(adminOrderExportApiUser())
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/exports/orders", [
+            'format' => 'csv',
+            'filters' => [
+                'status' => 'paid',
+                'created_after' => now()->subDays(3)->toIso8601String(),
+                'created_before' => now()->subDay()->toIso8601String(),
+            ],
+        ])
+        ->assertAccepted()
+        ->assertJsonPath('status', 'completed');
+
+    $export = DataExport::query()->findOrFail($createResponse->json('export_id'));
+
+    Storage::disk('local')->assertExists($export->storage_key);
+
+    $showResponse = $this->actingAs(adminOrderExportApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/exports/{$export->getKey()}")
+        ->assertOk()
+        ->assertJsonPath('data.status', 'completed')
+        ->assertJsonPath('data.format', 'csv')
+        ->assertJsonPath('data.row_count', 1)
+        ->assertJsonPath('data.download_expires_at', $export->download_expires_at?->toIso8601String());
+
+    $csv = rawurldecode(Str::after((string) $showResponse->json('data.download_url'), ','));
+
+    expect($csv)->toContain('order_number,created_at,status')
+        ->and($csv)->toContain('#EX1001')
+        ->and($csv)->toContain('TRACK-1001')
+        ->and($csv)->not->toContain('#EX2001')
+        ->and($csv)->not->toContain('#EX3001');
+});
+
+test('admin order export api enforces token abilities and store scope', function (): void {
+    Storage::fake('local');
+
+    $store = adminOrderExportApiStore();
+    adminOrderExportApiOrder($store, ['order_number' => '#TOKEN-EXPORT']);
+    $readToken = adminOrderExportApiToken($store, ['read-orders']);
+    $wrongAbilityToken = adminOrderExportApiToken($store, ['read-products']);
+    $otherStoreToken = adminOrderExportApiToken(Store::factory()->create(), ['read-orders']);
+
+    $this->withToken($wrongAbilityToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/exports/orders", [
+            'format' => 'csv',
+        ])
+        ->assertForbidden();
+
+    $createResponse = $this->withToken($readToken['plain_text'])
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/exports/orders", [
+            'format' => 'csv',
+        ])
+        ->assertAccepted();

-    $response->assertStatus(200);
+    $export = DataExport::query()->findOrFail($createResponse->json('export_id'));
+
+    $this->withToken($readToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/exports/{$export->getKey()}")
+        ->assertOk();
+
+    $this->withToken($otherStoreToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/exports/{$export->getKey()}")
+        ->assertForbidden();
+
+    $otherStoreExport = DataExport::factory()->create([
+        'store_id' => Store::factory()->create()->getKey(),
+    ]);
+
+    $this->actingAs(adminOrderExportApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/exports/{$otherStoreExport->getKey()}")
+        ->assertNotFound();
+});
+
+test('admin order export api validates format and filters', function (): void {
+    $store = adminOrderExportApiStore();
+
+    $this->actingAs(adminOrderExportApiUser())
+        ->postJson("/api/admin/v1/stores/{$store->getKey()}/exports/orders", [
+            'format' => 'xlsx',
+            'filters' => [
+                'status' => 'lost',
+                'created_after' => now()->toIso8601String(),
+                'created_before' => now()->subDay()->toIso8601String(),
+            ],
+        ])
+        ->assertUnprocessable()
+        ->assertJsonValidationErrors(['format', 'filters.status', 'filters.created_before']);
 });


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


The export model/service/resource/tests are in place. I’m running the new export test file first; this will also exercise the new migration against a fresh SQLite test database.


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


Output:
Chunk ID: 9406fd
Wall time: 1.6434 seconds
Process exited with code 0
Original token count: 16
Output:

  ...

  Tests:    3 passed (23 assertions)
  Duration: 1.35s


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

  ...

  Tests:    3 passed (23 assertions)
  Duration: 1.35s


The export tests pass on the first run. I’m formatting the PHP files now, then I’ll run the export/order/API suites and full test suite.


Output:
Chunk ID: 5fb5c0
Wall time: 0.3956 seconds
Process exited with code 0
Original token count: 5
Output:
{"result":"pass"}


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


php artisan test --compact tests/Feature/Api/AdminOrderExportApiTest.php tests/Feature/Api/AdminOrderApiTest.php


php artisan test --compact tests/Feature/Api


php artisan route:list --path=api/admin/v1/stores/{store}/exports --except-vendor


php artisan migrate:fresh --seed --no-interaction


php artisan route:list --path=api/admin/v1/stores/{store}/exports --except-vendor

  POST       api/admin/v1/stores/{store}/exports/orders api.admin.v1.exports.…
  GET|HEAD   api/admin/v1/stores/{store}/exports/{dataExport} api.admin.v1.ex…

                                                            Showing [2] routes


Output:
Chunk ID: 77e0a5
Wall time: 3.3444 seconds
Process exited with code 0
Original token count: 18
Output:

  .........

  Tests:    9 passed (48 assertions)
  Duration: 3.26s


php artisan test --compact tests/Feature/Api/AdminOrderExportApiTest.php tests/Feature/Api/AdminOrderApiTest.php

  .........

  Tests:    9 passed (48 assertions)
  Duration: 3.26s


Output:
Chunk ID: a7379e
Wall time: 18.3649 seconds
Process exited with code 0
Original token count: 30
Output:

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

  Tests:    55 passed (451 assertions)
  Duration: 18.28s


Output:
Chunk ID: e4cf42
Wall time: 0.0491 seconds
Process exited with code 0
Original token count: 61
Output:

  POST       api/admin/v1/stores/{store}/exports/orders api.admin.v1.exports.…
  GET|HEAD   api/admin/v1/stores/{store}/exports/{dataExport} api.admin.v1.ex…

                                                            Showing [2] routes


Output:
Chunk ID: 092d89
Wall time: 1.0022 seconds
Process exited with code 0
Original token count: 2008
Output:

  Dropping all tables ............................................ 3.86ms DONE

   INFO  Preparing database.

  Creating migration table ....................................... 1.64ms DONE

   INFO  Running migrations.

  0001_01_01_000000_create_users_table ........................... 1.03ms DONE
  0001_01_01_000001_create_cache_table ........................... 0.50ms DONE
  0001_01_01_000002_create_jobs_table ............................ 0.69ms DONE
  2025_08_14_170933_add_two_factor_columns_to_users_table ........ 0.44ms DONE
  2026_05_03_211409_create_organizations_table ................... 0.22ms DONE
  2026_05_03_211413_create_stores_table .......................... 2.22ms DONE
  2026_05_03_211416_create_store_domains_table ................... 0.64ms DONE
  2026_05_03_211419_create_store_settings_table .................. 0.25ms DONE
  2026_05_03_211425_create_store_users_table ..................... 0.42ms DONE
  2026_05_03_211556_create_customers_table ....................... 0.34ms DONE
  2026_05_03_212158_create_customer_password_reset_tokens_table .. 0.32ms DONE
  2026_05_03_213856_create_products_table ........................ 0.62ms DONE
  2026_05_03_213857_create_product_options_table ................. 0.52ms DONE
  2026_05_03_213858_create_product_option_values_table ........... 0.37ms DONE
  2026_05_03_213859_create_product_variants_table ................ 0.81ms DONE
  2026_05_03_213900_create_variant_option_values_table ........... 0.43ms DONE
  2026_05_03_213901_create_inventory_items_table ................. 0.36ms DONE
  2026_05_03_213902_create_collections_table ..................... 0.42ms DONE
  2026_05_03_213903_create_collection_products_table ............. 0.34ms DONE
  2026_05_03_213904_create_product_media_table ................... 0.46ms DONE
  2026_05_03_223824_create_themes_table .......................... 0.33ms DONE
  2026_05_03_223830_create_theme_files_table ..................... 0.35ms DONE
  2026_05_03_223837_create_theme_settings_table .................. 0.16ms DONE
  2026_05_03_223841_create_pages_table ........................... 0.40ms DONE
  2026_05_03_223844_create_navigation_menus_table ................ 0.31ms DONE
  2026_05_03_223848_create_navigation_items_table ................ 0.32ms DONE
  2026_05_03_225400_create_carts_table ........................... 0.41ms DONE
  2026_05_03_225405_create_cart_lines_table ...................... 0.37ms DONE
  2026_05_03_225409_create_checkouts_table ....................... 0.61ms DONE
  2026_05_03_225413_create_shipping_zones_table .................. 0.50ms DONE
  2026_05_03_225418_create_shipping_rates_table .................. 0.42ms DONE
  2026_05_03_225422_create_tax_settings_table .................... 0.21ms DONE
  2026_05_03_225427_create_discounts_table ....................... 0.55ms DONE
  2026_05_04_000408_create_customer_addresses_table .............. 0.32ms DONE
  2026_05_04_000408_create_orders_table .......................... 0.87ms DONE
  2026_05_04_000410_create_order_lines_table ..................... 0.54ms DONE
  2026_05_04_000411_create_payments_table ........................ 0.56ms DONE
  2026_05_04_000412_create_refunds_table ......................... 0.40ms DONE
  2026_05_04_000413_create_fulfillments_table .................... 0.41ms DONE
  2026_05_04_000414_create_fulfillment_lines_table ............... 0.61ms DONE
  2026_05_04_030847_create_search_settings_table ................. 0.29ms DONE
  2026_05_04_030848_create_products_fts_table .................... 0.35ms DONE
  2026_05_04_030848_create_search_queries_table .................. 0.40ms DONE
  2026_05_04_032640_create_analytics_events_table ................ 0.61ms DONE
  2026_05_04_032641_create_analytics_dailies_table ............... 0.34ms DONE
  2026_05_04_034556_create_apps_table ............................ 0.22ms DONE
  2026_05_04_034601_create_app_installations_table ............... 0.54ms DONE
  2026_05_04_034606_create_oauth_clients_table ................... 0.40ms DONE
  2026_05_04_034611_create_oauth_tokens_table .................... 0.45ms DONE
  2026_05_04_034616_create_webhook_subscriptions_table ........... 0.44ms DONE
  2026_05_04_034621_create_webhook_deliveries_table .............. 0.53ms DONE
  2026_05_04_055004_add_parent_id_to_navigation_items_table ...... 3.43ms DONE
  2026_05_04_074802_create_data_exports_table .................... 0.46ms DONE

   INFO  Seeding database.

  Database\Seeders\OrganizationSeeder ................................ RUNNING
  Database\Seeders\OrganizationSeeder .............................. 3 ms DONE

  Database\Seeders\StoreSeeder ....................................... RUNNING
  Database\Seeders\StoreSeeder ..................................... 1 ms DONE

  Database\Seeders\StoreDomainSeeder ................................. RUNNING
  Database\Seeders\StoreDomainSeeder ............................... 1 ms DONE

  Database\Seeders\AppSeeder ......................................... RUNNING
  Database\Seeders\AppSeeder ....................................... 8 ms DONE

  Database\Seeders\UserSeeder ........................................ RUNNING
  Database\Seeders\UserSeeder .................................... 277 ms DONE

  Database\Seeders\StoreUserSeeder ................................... RUNNING
  Database\Seeders\StoreUserSeeder ................................. 1 ms DONE

  Database\Seeders\StoreSettingsSeeder ............................... RUNNING
  Database\Seeders\StoreSettingsSeeder ............................. 1 ms DONE

  Database\Seeders\SearchSettingsSeeder .............................. RUNNING
  Database\Seeders\SearchSettingsSeeder ............................ 1 ms DONE

  Database\Seeders\CollectionSeeder .................................. RUNNING
  Database\Seeders\CollectionSeeder ................................ 2 ms DONE

  Database\Seeders\ProductSeeder ..................................... RUNNING
  Database\Seeders\ProductSeeder ................................. 214 ms DONE

  Database\Seeders\ThemeSeeder ....................................... RUNNING
  Database\Seeders\ThemeSeeder ..................................... 1 ms DONE

  Database\Seeders\ThemeFileSeeder ................................... RUNNING
  Database\Seeders\ThemeFileSeeder ................................. 7 ms DONE

  Database\Seeders\ThemeSettingsSeeder ............................... RUNNING
  Database\Seeders\ThemeSettingsSeeder ............................. 6 ms DONE

  Database\Seeders\PageSeeder ........................................ RUNNING
  Database\Seeders\PageSeeder ...................................... 2 ms DONE

  Database\Seeders\NavigationMenuSeeder .............................. RUNNING
  Database\Seeders\NavigationMenuSeeder ............................ 1 ms DONE

  Database\Seeders\NavigationItemSeeder .............................. RUNNING
  Database\Seeders\NavigationItemSeeder ............................ 5 ms DONE

  Database\Seeders\TaxSettingsSeeder ................................. RUNNING
  Database\Seeders\TaxSettingsSeeder ............................... 1 ms DONE

  Database\Seeders\ShippingZoneSeeder ................................ RUNNING
  Database\Seeders\ShippingZoneSeeder .............................. 1 ms DONE

  Database\Seeders\ShippingRateSeeder ................................ RUNNING
  Database\Seeders\ShippingRateSeeder .............................. 2 ms DONE

  Database\Seeders\DiscountSeeder .................................... RUNNING
  Database\Seeders\DiscountSeeder .................................. 4 ms DONE

  Database\Seeders\CustomerSeeder .................................... RUNNING
  Database\Seeders\CustomerSeeder ................................ 275 ms DONE

  Database\Seeders\AnalyticsSeeder ................................... RUNNING
  Database\Seeders\AnalyticsSeeder ................................ 69 ms DONE


The export/order focused tests, full API suite, export route list, and `migrate:fresh --seed` all pass. I’m running the full suite once more with the new migration included.


Output:
Chunk ID: bde525
Wall time: 30.0009 seconds
Process running with session ID 6315
Original token count: 21
Output:

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


Output:
Chunk ID: 01cc1e
Wall time: 24.6315 seconds
Process exited with code 0
Original token count: 59
Output:
..........................................................................
  ............................................................................
  ..............

  Tests:    242 passed (1408 assertions)
  Duration: 61.17s


 M routes/api.php
?? .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-04T05-58-46-727Z.log
?? app/Enums/ExportStatus.php
?? app/Http/Controllers/Api/Admin/V1/OrderExportController.php
?? app/Http/Resources/Admin/V1/DataExportResource.php
?? app/Models/DataExport.php
?? app/Services/OrderExportService.php
?? database/factories/DataExportFactory.php
?? database/migrations/2026_05_04_074802_create_data_exports_table.php
?? tests/Feature/Api/AdminOrderExportApiTest.php


Output:
Chunk ID: cad570
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 142
Output:
 M routes/api.php
?? .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-04T05-58-46-727Z.log
?? app/Enums/ExportStatus.php
?? app/Http/Controllers/Api/Admin/V1/OrderExportController.php
?? app/Http/Resources/Admin/V1/DataExportResource.php
?? app/Models/DataExport.php
?? app/Services/OrderExportService.php
?? database/factories/DataExportFactory.php
?? database/migrations/2026_05_04_074802_create_data_exports_table.php
?? tests/Feature/Api/AdminOrderExportApiTest.php


The full suite is green with the export migration: 242 tests, 1408 assertions. I’m updating progress now to reflect the new `data_exports` table, export API routes, and the synchronous self-contained export decision.


Input:
*** Begin Patch
*** Update File: specs/progress.md
@@
-| 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 tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, and webhook_deliveries. |
-| 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`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/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/customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, 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. Write endpoints for remaining admin REST surfaces are still missing. |
+| 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`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/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/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. |
@@
-| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
+| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `OrderExportService`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
@@
 - 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin store settings API changes: 52 tests, 428 assertions.
 - 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/settings --except-vendor` confirmed 2 general settings API routes: read and update.
 - 2026-05-04: `php artisan test --compact` passed after the admin store settings API changes: 239 tests, 1385 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 streamed/download responses, local storage behavior, model/migration/factory generation, and Pest 4 docs before the admin order export API changes.
+- 2026-05-04: `php artisan make:enum ExportStatus --no-interaction`, `php artisan make:model DataExport --migration --factory --no-interaction`, `php artisan make:class Services/OrderExportService --no-interaction`, `php artisan make:controller Api/Admin/V1/OrderExportController --no-interaction`, `php artisan make:resource Admin/V1/DataExportResource --no-interaction`, and `php artisan make:test Api/AdminOrderExportApiTest --pest --no-interaction` created the export status enum, durable export model/table, export service, API controller/resource, and tests.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin order export API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminOrderExportApiTest.php` passed after the admin order export API changes: 3 tests, 23 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminOrderExportApiTest.php tests/Feature/Api/AdminOrderApiTest.php` passed after the admin order export API changes: 9 tests, 48 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin order export API changes: 55 tests, 451 assertions.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/exports --except-vendor` confirmed 2 order export API routes: create and status.
+- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the admin order export API changes and confirmed the `data_exports` migration runs with the seed suite.
+- 2026-05-04: `php artisan test --compact` passed after the admin order export API changes: 242 tests, 1408 assertions.
@@
 - The store settings API exposes the general settings UI surface as `/api/admin/v1/stores/{store}/settings`: store name/currency/locale/timezone fields update the `stores` row, while `settings_json` patches are recursively merged into `store_settings` so checkout and notification defaults are preserved unless explicitly changed.
+- The order export API adds a small `data_exports` table to support the spec's export status endpoint; this local app generates CSV synchronously into the local disk, marks the export `completed` inside the create request, and returns a data URL from the status response instead of a cloud presigned URL.
 - Checkout and notification settings persist in `store_settings.settings_json`; `bank_transfer_cancel_days` remains a root-level key because the existing cancellation job already consumes it from that location.
@@
-- Admin REST export surfaces are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
 - Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
 - SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.
@@
-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation 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 order 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 API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation 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 order 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, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, deferred OAuth/app ecosystem routes, full browser-suite, and database CHECK constraint gaps tracked above.
*** End Patch


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

@@ -28,7 +28,7 @@
 | --- | --- | --- | --- |
-| 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 tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, and webhook_deliveries. |
-| 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`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/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/customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, 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. Write endpoints for remaining admin REST surfaces are still missing. |
+| 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`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/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/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. |
 | Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout, home, collections index/detail, product detail, SQLite-backed search with filters/sort/pagination, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus with one-level dropdown children, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, customer password reset forms, and customer order list/detail views render seeded and runtime data. |
-| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
+| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `OrderExportService`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
 | Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, customer password reset with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, concrete resource policies, encrypted app client/webhook secrets, one-time display developer token generation, and a compatible hashed bearer-token admin API middleware with store/ability enforcement are implemented. |
@@ -397,2 +397,11 @@
 - 2026-05-04: `php artisan test --compact` passed after the admin store settings API changes: 239 tests, 1385 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 streamed/download responses, local storage behavior, model/migration/factory generation, and Pest 4 docs before the admin order export API changes.
+- 2026-05-04: `php artisan make:enum ExportStatus --no-interaction`, `php artisan make:model DataExport --migration --factory --no-interaction`, `php artisan make:class Services/OrderExportService --no-interaction`, `php artisan make:controller Api/Admin/V1/OrderExportController --no-interaction`, `php artisan make:resource Admin/V1/DataExportResource --no-interaction`, and `php artisan make:test Api/AdminOrderExportApiTest --pest --no-interaction` created the export status enum, durable export model/table, export service, API controller/resource, and tests.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin order export API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminOrderExportApiTest.php` passed after the admin order export API changes: 3 tests, 23 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminOrderExportApiTest.php tests/Feature/Api/AdminOrderApiTest.php` passed after the admin order export API changes: 9 tests, 48 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin order export API changes: 55 tests, 451 assertions.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/exports --except-vendor` confirmed 2 order export API routes: create and status.
+- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the admin order export API changes and confirmed the `data_exports` migration runs with the seed suite.
+- 2026-05-04: `php artisan test --compact` passed after the admin order export API changes: 242 tests, 1408 assertions.

@@ -441,2 +450,3 @@
 - The store settings API exposes the general settings UI surface as `/api/admin/v1/stores/{store}/settings`: store name/currency/locale/timezone fields update the `stores` row, while `settings_json` patches are recursively merged into `store_settings` so checkout and notification defaults are preserved unless explicitly changed.
+- The order export API adds a small `data_exports` table to support the spec's export status endpoint; this local app generates CSV synchronously into the local disk, marks the export `completed` inside the create request, and returns a data URL from the status response instead of a cloud presigned URL.
 - Checkout and notification settings persist in `store_settings.settings_json`; `bank_transfer_cancel_days` remains a root-level key because the existing cancellation job already consumes it from that location.
@@ -467,3 +477,2 @@
 - `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
-- Admin REST export surfaces are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
 - Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
@@ -473,2 +482,2 @@

-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation 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 order 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 API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation 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 order 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, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, deferred OAuth/app ecosystem routes, full browser-suite, and database CHECK constraint gaps tracked above.


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


rm -f .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-04T05-58-46-727Z.log && git diff --check && git status --short


rm -f .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-04T05-58-46-727Z.log && git diff --check && git status --short

 M routes/api.php
 M specs/progress.md
?? app/Enums/ExportStatus.php
?? app/Http/Controllers/Api/Admin/V1/OrderExportController.php
?? app/Http/Resources/Admin/V1/DataExportResource.php
?? app/Models/DataExport.php
?? app/Services/OrderExportService.php
?? database/factories/DataExportFactory.php
?? database/migrations/2026_05_04_074802_create_data_exports_table.php
?? tests/Feature/Api/AdminOrderExportApiTest.php


Output:
Chunk ID: 9803e1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 105
Output:
 M routes/api.php
 M specs/progress.md
?? app/Enums/ExportStatus.php
?? app/Http/Controllers/Api/Admin/V1/OrderExportController.php
?? app/Http/Resources/Admin/V1/DataExportResource.php
?? app/Models/DataExport.php
?? app/Services/OrderExportService.php
?? database/factories/DataExportFactory.php
?? database/migrations/2026_05_04_074802_create_data_exports_table.php
?? tests/Feature/Api/AdminOrderExportApiTest.php


git add app/Enums/ExportStatus.php app/Http/Controllers/Api/Admin/V1/OrderExportController.php app/Http/Resources/Admin/V1/DataExportResource.php app/Models/DataExport.php app/Services/OrderExportService.php database/factories/DataExportFactory.php database/migrations/2026_05_04_074802_create_data_exports_table.php routes/api.php specs/progress.md tests/Feature/Api/AdminOrderExportApiTest.php && git diff --cached --stat


git add app/Enums/ExportStatus.php app/Http/Controllers/Api/Admin/V1/OrderExportController.php app/Http/Resources/Admin/V1/DataExportResource.php app/Models/DataExport.php app/Services/OrderExportService.php database/factories/DataExportFactory.php database/migrations/2026_05_04_074802_create_data_exports_table.php routes/api.php specs/progress.md tests/Feature/Api/AdminOrderExportApiTest.php && git diff --cached --stat

 app/Enums/ExportStatus.php                         |  11 ++
 .../Api/Admin/V1/OrderExportController.php         |  61 ++++++++
 app/Http/Resources/Admin/V1/DataExportResource.php |  48 ++++++
 app/Models/DataExport.php                          |  65 ++++++++
 app/Services/OrderExportService.php                | 143 ++++++++++++++++++
 database/factories/DataExportFactory.php           |  33 +++++
 ...2026_05_04_074802_create_data_exports_table.php |  41 +++++
 routes/api.php                                     |   3 +
 specs/progress.md                                  |  19 ++-
 tests/Feature/Api/AdminOrderExportApiTest.php      | 165 +++++++++++++++++++++
 10 files changed, 584 insertions(+), 5 deletions(-)


Output:
Chunk ID: 87f0df
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 180
Output:
 app/Enums/ExportStatus.php                         |  11 ++
 .../Api/Admin/V1/OrderExportController.php         |  61 ++++++++
 app/Http/Resources/Admin/V1/DataExportResource.php |  48 ++++++
 app/Models/DataExport.php                          |  65 ++++++++
 app/Services/OrderExportService.php                | 143 ++++++++++++++++++
 database/factories/DataExportFactory.php           |  33 +++++
 ...2026_05_04_074802_create_data_exports_table.php |  41 +++++
 routes/api.php                                     |   3 +
 specs/progress.md                                  |  19 ++-
 tests/Feature/Api/AdminOrderExportApiTest.php      | 165 +++++++++++++++++++++
 10 files changed, 584 insertions(+), 5 deletions(-)


A  app/Enums/ExportStatus.php
A  app/Http/Controllers/Api/Admin/V1/OrderExportController.php
A  app/Http/Resources/Admin/V1/DataExportResource.php
A  app/Models/DataExport.php
A  app/Services/OrderExportService.php
A  database/factories/DataExportFactory.php
A  database/migrations/2026_05_04_074802_create_data_exports_table.php
M  routes/api.php
M  specs/progress.md
A  tests/Feature/Api/AdminOrderExportApiTest.php


Output:
Chunk ID: e20715
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 105
Output:
A  app/Enums/ExportStatus.php
A  app/Http/Controllers/Api/Admin/V1/OrderExportController.php
A  app/Http/Resources/Admin/V1/DataExportResource.php
A  app/Models/DataExport.php
A  app/Services/OrderExportService.php
A  database/factories/DataExportFactory.php
A  database/migrations/2026_05_04_074802_create_data_exports_table.php
M  routes/api.php
M  specs/progress.md
A  tests/Feature/Api/AdminOrderExportApiTest.php


The export slice is staged: enum/model/migration/factory, service, controller/resource, routes, tests, and progress. I’m committing this as the final admin REST gap slice.


git commit -m "Add admin order export API"


git commit -m "Add admin order export API"

[2026-05-04-codex-5-5-goal cf3288fe] Add admin order export API
 10 files changed, 584 insertions(+), 5 deletions(-)
 create mode 100644 app/Enums/ExportStatus.php
 create mode 100644 app/Http/Controllers/Api/Admin/V1/OrderExportController.php
 create mode 100644 app/Http/Resources/Admin/V1/DataExportResource.php
 create mode 100644 app/Models/DataExport.php
 create mode 100644 app/Services/OrderExportService.php
 create mode 100644 database/factories/DataExportFactory.php
 create mode 100644 database/migrations/2026_05_04_074802_create_data_exports_table.php
 create mode 100644 tests/Feature/Api/AdminOrderExportApiTest.php


Output:
Chunk ID: 572593
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 158
Output:
[2026-05-04-codex-5-5-goal cf3288fe] Add admin order export API
 10 files changed, 584 insertions(+), 5 deletions(-)
 create mode 100644 app/Enums/ExportStatus.php
 create mode 100644 app/Http/Controllers/Api/Admin/V1/OrderExportController.php
 create mode 100644 app/Http/Resources/Admin/V1/DataExportResource.php
 create mode 100644 app/Models/DataExport.php
 create mode 100644 app/Services/OrderExportService.php
 create mode 100644 database/factories/DataExportFactory.php
 create mode 100644 database/migrations/2026_05_04_074802_create_data_exports_table.php
 create mode 100644 tests/Feature/Api/AdminOrderExportApiTest.php


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


Committed `cf3288fe Add admin order export API`; the worktree is clean. With the listed admin REST gap closed, I’m doing a remaining-gap audit against `specs/progress.md` and route coverage to decide whether there is another implementable slice or whether only deferred/verification items remain.


rg -n "Not Implemented|501|deferred|TODO|missing|Open Issues|root paths|CHECK|OAuth|Passport|browser suite|full automated" specs app routes tests


/bin/zsh
-lc
tail -n 80 specs/progress.md

- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/exports --except-vendor` confirmed 2 order export API routes: create and status.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the admin order export API changes and confirmed the `data_exports` migration runs with the seed suite.
- 2026-05-04: `php artisan test --compact` passed after the admin order export API changes: 242 tests, 1408 assertions.

## Decisions

- Follow the roadmap order strictly. Phase 1 is required before catalog, checkout, admin, storefront, and QA flows can be meaningfully implemented.
- Preserve the starter Fortify conventions where they do not conflict with the shop specs.
- Use integer minor units for all money columns and JSON text columns cast through Eloquent as specified.
- Keep `password_hash` as the database column for admin users and customers while exposing `password`/`getAuthPassword()` at the model layer for Fortify/Laravel auth compatibility.
- Seed both `shop.test` and `acme-fashion.test` as storefront domains for the first store so the goal URL and E2E spec URL can both resolve.
- Herd needs a site link for non-project-folder aliases; `acme-fashion.test` is linked to the same local project path as `shop.test` so the seeded domain can be verified in browser E2E flows.
- Customer Livewire auth components persist `storeId` from the initial storefront request because Livewire update requests do not run through the original storefront route middleware.
- The seed specification contains an arithmetic conflict: Acme Fashion's per-product variant table sums to 117 variants, while the prose says 107. The implementation follows the concrete per-product table, resulting in 117 Fashion variants and 10 Electronics variants, 127 total.
- Child catalog models without a `store_id` column are tenant-scoped through their parent product relationship. Inventory keeps its denormalized `store_id` and enforces that it matches the variant product store at save time.
- Storefront content pages now resolve from the `pages` table and only published pages are rendered.
- The catalog admin form intentionally blocks active products without a priced variant and duplicate in-store SKUs during UI edits, mirroring service-layer invariants where the current CRUD surface touches product variants directly.
- The product admin form generates variant rows from the option cartesian product, preserves matching variant edits by option combination, and syncs product option values plus variant pivots on save; stale variants are deleted or archived if order-line history exists.
- Navigation items use a nullable `parent_id` for one-level storefront dropdowns; `NavigationService::buildTree()` groups items recursively by parent while the admin editor enforces top-level parents for child items.
- Cart, checkout, and pricing runtime records are not seeded; only deterministic tax, shipping, and discount configuration is seeded. Runtime carts/checkouts are created by services and tests.
- The cart drawer reads the current session/customer cart without creating an empty cart on every storefront render; carts are created on first add-to-cart or checkout/cart service mutation.
- The checkout UI reserves inventory by selecting a payment method, then submits payment through `CheckoutService::completeCheckout()` so failed card payments can release reservations and return customers to payment selection.
- Order confirmation access is allowed for the authenticated owning customer or for the session that just placed the order via `last_order_id`.
- Customer account order list/detail routes use the `customer` guard and reload orders through explicit store/customer constraints.
- The cart REST API exposes `cart_version` as the public optimistic concurrency field while the service layer keeps its `expectedVersion` argument; `expected_version` remains accepted as a compatibility alias in API requests.
- Cart page discount codes are validated with `DiscountService`, saved in session, and applied to the checkout when the customer proceeds.
- Admin discount forms persist minimum purchase, product eligibility, collection eligibility, and `one_per_customer` in the existing `discounts.rules_json` keys consumed by `DiscountService`; code discounts now check customer order-line allocation history before allowing another use.
- `PricingEngine` applies an explicit checkout discount code first, then active automatic discounts returned by `DiscountService`.
- Order creation replays the same code-then-automatic discount sequence in memory so `order_lines.discount_allocations_json` keeps separate `{discount_id, code, amount}` entries for every line-item discount while preserving aggregate cart line totals.
- `orders.checkout_id` is intentionally added beyond the original schema table to enforce idempotent checkout completion without duplicate orders.
- Failed card payments release reserved inventory and move the checkout back to `shipping_sel…5193 chars truncated…uery time: per-store stop words are removed, synonym groups expand into sanitized SQLite FTS5 `OR` terms, and terms are joined with explicit `AND`; the admin page can rebuild the per-store index synchronously for this self-contained app.
- Search API rate limiting is registered as `search` (30/minute per IP), analytics ingestion uses the `analytics` limiter (60/minute per IP), and a `webhooks` limiter is registered for future inbound app endpoints.
- Analytics ingestion stores raw events with client-event deduplication and keeps aggregation separate in `analytics_daily`; admin analytics reads pre-aggregated daily metrics for KPIs and raw events for referrers.
- Seeded analytics metrics are deterministic demo data and are not tied to seeded orders because order/payment seed data remains intentionally absent.
- The admin analytics CSV export is generated as a data URL for the self-contained local app rather than creating persistent export files/jobs.
- OAuth/Passport app authorization remains deferred per the roadmap; `/oauth/*` and `/api/apps/v1/*` now return explicit `501 Not Implemented` responses, while the developer token UI uses the existing app installation and `oauth_tokens` schema as a self-contained local token store.
- Admin order API routes use the `admin.api` middleware, which accepts either the existing session-authenticated admin user or a hashed `oauth_tokens` bearer token scoped to the route store; token requests require `read-orders` for GET routes and `write-orders` for refund/fulfillment mutations and update `last_used_at` on successful use.
- Outbound webhook delivery jobs are forced onto the database queue and `webhooks` queue name so domain events enqueue delivery work instead of making external HTTP requests inline when the default queue connection is `sync`.
- Webhook signatures follow the security spec message shape of `{timestamp}.{json_body}` with HMAC-SHA256, while the delivery payload is wrapped with `id`, `api_version`, `event_type`, `store_id`, `occurred_at`, and `data`.
- Webhook circuit breaker state is derived from the latest five delivery rows for a subscription because the schema does not include a dedicated consecutive-failure counter column.
- Customer password resets use a custom store-scoped service instead of Laravel's stock password broker token repository because the `customer_password_reset_tokens` table includes `store_id` in the primary key and the stock repository does not write tenant columns.
- Customer password reset pages are routed under `/account/forgot-password` and `/account/reset-password/{token}` so the existing Fortify starter/admin root routes (`/forgot-password`, `/reset-password/{token}`) remain intact.
- Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.
- Resource policies now type concrete model parameters for Product, Collection, Customer, Discount, Order, Fulfillment, Refund, Page, and Theme actions; `ChecksStoreRole::storeIdForModel()` stays generic because it is the shared store-id extraction helper.

## Open Issues

- Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
- `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
- Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
- SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.

## Completion Summary

Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation 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 order 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, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, deferred OAuth/app ecosystem routes, full browser-suite, and database CHECK constraint gaps tracked above.


Output:
Chunk ID: f39af4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3799
Output:
- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores/{store}/exports --except-vendor` confirmed 2 order export API routes: create and status.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the admin order export API changes and confirmed the `data_exports` migration runs with the seed suite.
- 2026-05-04: `php artisan test --compact` passed after the admin order export API changes: 242 tests, 1408 assertions.

## Decisions

- Follow the roadmap order strictly. Phase 1 is required before catalog, checkout, admin, storefront, and QA flows can be meaningfully implemented.
- Preserve the starter Fortify conventions where they do not conflict with the shop specs.
- Use integer minor units for all money columns and JSON text columns cast through Eloquent as specified.
- Keep `password_hash` as the database column for admin users and customers while exposing `password`/`getAuthPassword()` at the model layer for Fortify/Laravel auth compatibility.
- Seed both `shop.test` and `acme-fashion.test` as storefront domains for the first store so the goal URL and E2E spec URL can both resolve.
- Herd needs a site link for non-project-folder aliases; `acme-fashion.test` is linked to the same local project path as `shop.test` so the seeded domain can be verified in browser E2E flows.
- Customer Livewire auth components persist `storeId` from the initial storefront request because Livewire update requests do not run through the original storefront route middleware.
- The seed specification contains an arithmetic conflict: Acme Fashion's per-product variant table sums to 117 variants, while the prose says 107. The implementation follows the concrete per-product table, resulting in 117 Fashion variants and 10 Electronics variants, 127 total.
- Child catalog models without a `store_id` column are tenant-scoped through their parent product relationship. Inventory keeps its denormalized `store_id` and enforces that it matches the variant product store at save time.
- Storefront content pages now resolve from the `pages` table and only published pages are rendered.
- The catalog admin form intentionally blocks active products without a priced variant and duplicate in-store SKUs during UI edits, mirroring service-layer invariants where the current CRUD surface touches product variants directly.
- The product admin form generates variant rows from the option cartesian product, preserves matching variant edits by option combination, and syncs product option values plus variant pivots on save; stale variants are deleted or archived if order-line history exists.
- Navigation items use a nullable `parent_id` for one-level storefront dropdowns; `NavigationService::buildTree()` groups items recursively by parent while the admin editor enforces top-level parents for child items.
- Cart, checkout, and pricing runtime records are not seeded; only deterministic tax, shipping, and discount configuration is seeded. Runtime carts/checkouts are created by services and tests.
- The cart drawer reads the current session/customer cart without creating an empty cart on every storefront render; carts are created on first add-to-cart or checkout/cart service mutation.
- The checkout UI reserves inventory by selecting a payment method, then submits payment through `CheckoutService::completeCheckout()` so failed card payments can release reservations and return customers to payment selection.
- Order confirmation access is allowed for the authenticated owning customer or for the session that just placed the order via `last_order_id`.
- Customer account order list/detail routes use the `customer` guard and reload orders through explicit store/customer constraints.
- The cart REST API exposes `cart_version` as the public optimistic concurrency field while the service layer keeps its `expectedVersion` argument; `expected_version` remains accepted as a compatibility alias in API requests.
- Cart page discount codes are validated with `DiscountService`, saved in session, and applied to the checkout when the customer proceeds.
- Admin discount forms persist minimum purchase, product eligibility, collection eligibility, and `one_per_customer` in the existing `discounts.rules_json` keys consumed by `DiscountService`; code discounts now check customer order-line allocation history before allowing another use.
- `PricingEngine` applies an explicit checkout discount code first, then active automatic discounts returned by `DiscountService`.
- Order creation replays the same code-then-automatic discount sequence in memory so `order_lines.discount_allocations_json` keeps separate `{discount_id, code, amount}` entries for every line-item discount while preserving aggregate cart line totals.
- `orders.checkout_id` is intentionally added beyond the original schema table to enforce idempotent checkout completion without duplicate orders.
- Failed card payments release reserved inventory and move the checkout back to `shipping_selected` so customers can retry payment selection.
- Bank transfer order completion creates a pending order and payment while keeping inventory reserved until the later admin payment-confirmation flow.
- Inventory commit/release/restock locks by primary key without the current-store global scope because scheduled jobs can operate across stores in one run.
- Admin order Livewire components persist locked `storeId`/`orderId` state and reload orders without global scopes but with explicit store constraints before each service-backed action.
- Admin order actions deliberately delegate to `OrderService`, `RefundService`, and `FulfillmentService` so UI validation cannot bypass payment, refund, fulfillment, or inventory guards.
- 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 read-only products/customers/orders, 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.
- Customer addresses are scoped through the locked customer id because `customer_addresses` intentionally belongs to customers and has no direct `store_id` column.
- Admin/store Livewire route middleware is registered with Livewire persistent middleware so component POST requests keep `current_store` binding and store-role checks after the initial full-page route load.
- Admin routes use a custom `EnsureUserEmailIsVerified` middleware so verified-user gating works for full-page Livewire routes without relying on the stock redirect middleware path that fails during Livewire component redirects in this app.
- Admin settings are split across `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, and `/admin/settings/notifications`; domains are managed on the general settings page because the current route surface does not need a separate domains route.
- The tax settings API accepts the spec-facing `default_tax_rate`/`tax_rates` payload shape and normalizes it to the existing internal `default_rate_bps`/`rates` config consumed by the calculator and admin UI.
- The shipping settings API accepts spec-facing `price_amount`/`tiers` rate config keys and normalizes them to the existing `amount`/`ranges` config consumed by `ShippingCalculator`.
- The theme upload API imports ZIP archives with `theme.json` or `manifest.json`, requires the same core template paths seeded for default themes, stores each imported file on the local disk under the new theme id, and seeds theme settings from the manifest or the store defaults.
- The theme publish API validates required files before switching the store to exactly one published theme and flushing cached theme settings.
- The store settings API exposes the general settings UI surface as `/api/admin/v1/stores/{store}/settings`: store name/currency/locale/timezone fields update the `stores` row, while `settings_json` patches are recursively merged into `store_settings` so checkout and notification defaults are preserved unless explicitly changed.
- The order export API adds a small `data_exports` table to support the spec's export status endpoint; this local app generates CSV synchronously into the local disk, marks the export `completed` inside the create request, and returns a data URL from the status response instead of a cloud presigned URL.
- Checkout and notification settings persist in `store_settings.settings_json`; `bank_transfer_cancel_days` remains a root-level key because the existing cancellation job already consumes it from that location.
- Admin navigation persists nested ordered menu items with Livewire `wire:sort`, up/down sibling controls, and a parent selector; children are saved under their selected top-level parent with per-parent positions.
- The theme editor uses the `ThemeSettingsService` default settings shape as the editable schema and writes back to `theme_settings.settings_json`.
- Theme files are seeded and edited through local disk storage at `theme_files.storage_key`; saving a file updates the stored contents plus `sha256` and `byte_size` metadata.
- Theme duplication copies each file's local disk contents to a new storage key for the copied theme instead of sharing the original file storage path.
- Search uses a local SQLite FTS5 virtual table (`products_fts`) with `SearchService` as the only query/sync boundary; product observer events keep the index synchronized for create/update/delete.
- Admin search reindex API runs the existing local `SearchService::reindex()` synchronously and returns `202 Accepted` with `completed` status because this self-contained app has no separate durable search job state table.
- Search settings apply at query time: per-store stop words are removed, synonym groups expand into sanitized SQLite FTS5 `OR` terms, and terms are joined with explicit `AND`; the admin page can rebuild the per-store index synchronously for this self-contained app.
- Search API rate limiting is registered as `search` (30/minute per IP), analytics ingestion uses the `analytics` limiter (60/minute per IP), and a `webhooks` limiter is registered for future inbound app endpoints.
- Analytics ingestion stores raw events with client-event deduplication and keeps aggregation separate in `analytics_daily`; admin analytics reads pre-aggregated daily metrics for KPIs and raw events for referrers.
- Seeded analytics metrics are deterministic demo data and are not tied to seeded orders because order/payment seed data remains intentionally absent.
- The admin analytics CSV export is generated as a data URL for the self-contained local app rather than creating persistent export files/jobs.
- OAuth/Passport app authorization remains deferred per the roadmap; `/oauth/*` and `/api/apps/v1/*` now return explicit `501 Not Implemented` responses, while the developer token UI uses the existing app installation and `oauth_tokens` schema as a self-contained local token store.
- Admin order API routes use the `admin.api` middleware, which accepts either the existing session-authenticated admin user or a hashed `oauth_tokens` bearer token scoped to the route store; token requests require `read-orders` for GET routes and `write-orders` for refund/fulfillment mutations and update `last_used_at` on successful use.
- Outbound webhook delivery jobs are forced onto the database queue and `webhooks` queue name so domain events enqueue delivery work instead of making external HTTP requests inline when the default queue connection is `sync`.
- Webhook signatures follow the security spec message shape of `{timestamp}.{json_body}` with HMAC-SHA256, while the delivery payload is wrapped with `id`, `api_version`, `event_type`, `store_id`, `occurred_at`, and `data`.
- Webhook circuit breaker state is derived from the latest five delivery rows for a subscription because the schema does not include a dedicated consecutive-failure counter column.
- Customer password resets use a custom store-scoped service instead of Laravel's stock password broker token repository because the `customer_password_reset_tokens` table includes `store_id` in the primary key and the stock repository does not write tenant columns.
- Customer password reset pages are routed under `/account/forgot-password` and `/account/reset-password/{token}` so the existing Fortify starter/admin root routes (`/forgot-password`, `/reset-password/{token}`) remain intact.
- Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.
- Resource policies now type concrete model parameters for Product, Collection, Customer, Discount, Order, Fulfillment, Refund, Page, and Theme actions; `ChecksStoreRole::storeIdForModel()` stays generic because it is the shared store-id extraction helper.

## Open Issues

- Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
- `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
- Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
- SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.

## Completion Summary

Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation 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 order 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, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, deferred OAuth/app ecosystem routes, full browser-suite, and database CHECK constraint gaps tracked above.


cf3288fe Add admin order export API
b02a92c2 Add admin store settings API
b61e6322 Add admin theme API endpoints
95ad3071 Add admin shipping settings API
a8f0799e Add admin tax settings API
a5135d16 Add admin analytics summary API
99670769 Add admin search index API endpoints
d8fd4ebe Add admin page API endpoints