server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## Responses Streamed Responses Streamed Downloads
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

### Streamed Downloads

Sometimes you may wish to turn the string response of a given operation into a downloadable response without having to write the contents of the operation to disk. You may use the `streamDownload` method in this scenario. This method accepts a callback, filename, and an optional array of headers as its arguments:

```php
use App\Services\GitHub;

return response()->streamDownload(function () {
    echo GitHub::api('repo')
        ->contents()
        ->readme('laravel', 'laravel')['contents'];
}, 'laravel-readme.md');
```

---

## Upgrade Upgrading To 12.0 From 11.x Storage Local Filesystem Disk Default Root Path
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Local Filesystem Disk Default Root Path

**Likelihood Of Impact: Low**

If your application does not explicitly define a `local` disk in your filesystems configuration, Laravel will now default the local disk's root to `storage/app/private`. In previous releases, this defaulted to `storage/app`. As a result, calls to `Storage::disk('local')` will read from and write to `storage/app/private` unless otherwise configured. To restore the previous behavior, you may define the `local` disk manually and set the desired root path.

---

## Eloquent Generating Model Classes
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

## Generating Model Classes

To get started, let's create an Eloquent model. Models typically live in the `app\Models` directory and extend the `Illuminate\Database\Eloquent\Model` class. You may use the `make:model` [Artisan command](/docs/{{version}}/artisan) to generate a new model:

```shell
php artisan make:model Flight
```

If you would like to generate a [database migration](/docs/{{version}}/migrations) when you generate the model, you may use the `--migration` or `-m` option:

```shell
php artisan make:model Flight --migration
```

You may generate various other types of classes when generating a model, such as factories, seeders, policies, controllers, and form requests. In addition, these options may be combined to create multiple classes at once:

```shell
# Generate a model and a FlightFactory class...
php artisan make:model Flight --factory
php artisan make:model Flight -f

# Generate a model and a FlightSeeder class...
php artisan make:model Flight --seed
php artisan make:model Flight -s

# Generate a model and a FlightController class...
php artisan make:model Flight --controller
php artisan make:model Flight -c

# Generate a model, FlightController resource class, and form request classes...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR

# Generate a model and a FlightPolicy class...
php artisan make:model Flight --policy

# Generate a model and a migration, factory, seeder, and controller...
php artisan make:model Flight -mfsc

# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...
php artisan make:model Flight --all
php artisan make:model Flight -a

# Generate a pivot model...
php artisan make:model Member --pivot
php artisan make:model Member -p
```

---

## Structure The Root Directory The Storage Directory
Structure | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/structure.md

### The Storage Directory

The `storage` directory contains your logs, compiled Blade templates, file based sessions, file caches, and other files generated by the framework. This directory is segregated into `app`, `framework`, and `logs` directories. The `app` directory may be used to store any files generated by your application. The `framework` directory is used to store framework generated files and caches. Finally, the `logs` directory contains your application's log files.

The `storage/app/public` directory may be used to store user-generated files, such as profile avatars, that should be publicly accessible. You should create a symbolic link at `public/storage` which points to this directory. You may create the link using the `php artisan storage:link` Artisan command.

---

## Sail File Storage
Sail | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sail.md

## File Storage

If you plan to use Amazon S3 to store files while running your application in its production environment, you may wish to install the [RustFS](https://rustfs.com) service when installing Sail. RustFS provides an S3 compatible API that you may use to develop locally using Laravel's `s3` file storage driver without creating "test" storage buckets in your production S3 environment. If you choose to install RustFS while installing Sail, a RustFS configuration section will be added to your application's `compose.yaml` file.

By default, your application's `filesystems` configuration file already contains a disk configuration for the `s3` disk. In addition to using this disk to interact with Amazon S3, you may use it to interact with any S3 compatible file storage service such as RustFS by simply modifying the associated environment variables that control its configuration. For example, when using RustFS, your filesystem environment variable configuration should be defined as follows:

```ini
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=sail
AWS_SECRET_ACCESS_KEY=password
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=local
AWS_ENDPOINT=http://rustfs:9000
AWS_USE_PATH_STYLE_ENDPOINT=true
```

---

## Eloquent Factories Defining Model Factories Generating Factories
Eloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md

### Generating Factories

To create a factory, execute the `make:factory` [Artisan command](/docs/{{version}}/artisan):

```shell
php artisan make:factory PostFactory
```

The new factory class will be placed in your `database/factories` directory.

---

## Responses Streamed Responses
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

## Streamed Responses

By streaming data to the client as it is generated, you can significantly reduce memory usage and improve performance, especially for very large responses. Streamed responses allow the client to begin processing data before the server has finished sending it:

```php
Route::get('/stream', function () {
    return response()->stream(function (): void {
        foreach (['developer', 'admin'] as $string) {
            echo $string;
            ob_flush();
            flush();
            sleep(2); // Simulate delay between chunks...
        }
    }, 200, ['X-Accel-Buffering' => 'no']);
});
```

For convenience, if the closure you provide to the `stream` method returns a [Generator](https://www.php.net/manual/en/language.generators.overview.php), Laravel will automatically flush the output buffer between strings returned by the generator, as well as disable Nginx output buffering:

```php
Route::post('/chat', function () {
    return response()->stream(function (): Generator {
        $stream = OpenAI::client()->chat()->createStreamed(...);

        foreach ($stream as $response) {
            yield $response->choices[0];
        }
    });
});
```

---

## Responses Streamed Responses Event Streams (SSE) Consuming Event Streams
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

#### Consuming Event Streams

Event streams may be consumed using Laravel's `stream` npm package, which provides a convenient API for interacting with Laravel event streams. To get started, install the `@laravel/stream-react`, `@laravel/stream-vue`, or `@laravel/stream-svelte` package:

```shell tab=React
npm install @laravel/stream-react
```

```shell tab=Vue
npm install @laravel/stream-vue
```

```shell tab=Svelte
npm install @laravel/stream-svelte
```

Then, `useEventStream` may be used to consume the event stream. After providing your stream URL, the hook will automatically update the `message` with the concatenated response as messages are returned from your Laravel application:

```jsx tab=React
import { useEventStream } from "@laravel/stream-react";

function App() {
  const { message } = useEventStream("/chat");

  return <div>{message}</div>;
}
```

```vue tab=Vue
<script setup lang="ts">
import { useEventStream } from "@laravel/stream-vue";

const { message } = useEventStream("/chat");
</script>

<template>
  <div>{{ message }}</div>
</template>
```

```svelte tab=Svelte
<script>
import { useEventStream } from "@laravel/stream-svelte";

const eventStream = useEventStream("/chat");
</script>

<div>{$eventStream.message}</div>
```

The second argument given to `useEventStream` is an options object that you may use to customize the stream consumption behavior. The default values for this object are shown below:

```jsx tab=React
import { useEventStream } from "@laravel/stream-react";

function App() {
  const { message } = useEventStream("/stream", {
    eventName: "update",
    onMessage: (message) => {
      //
    },
    onError: (error) => {
      //
    },
    onComplete: () => {
      //
    },
    endSignal: "</stream>",
    glue: " ",
  });

  return <div>{message}</div>;
}
```

```vue tab=Vue
<script setup lang="ts">
import { useEventStream } from "@laravel/stream-vue";

const { message } = useEventStream("/chat", {
  eventName: "update",
  onMessage: (message) => {
    // ...
  },
  onError: (error) => {
    // ...
  },
  onComplete: () => {
    // ...
  },
  endSignal: "</stream>",
  glue: " ",
});
</script>
```

```svelte tab=Svelte
<script>
import { useEventStream } from "@laravel/stream-svelte";

const eventStream = useEventStream("/chat", {
    eventName: "update",
    onMessage: (event) => {
        //
    },
    onError: (error) => {
        //
    },
    onComplete: () => {
        //
    },
    endSignal: "</stream>",
    glue: " ",
    replace: false,
});
</script>
```

Event streams may also be manually consumed via an [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) object by your application's frontend. The `eventStream` method will automatically send a `</stream>` update to the event stream when the stream is complete:

```js
const source = new EventSource('/chat');

source.addEventListener('update', (event) => {
    if (event.data === '</stream>') {
        source.close();

        return;
    }

    console.log(event.data);
});
```

To customize the final event that is sent to the event stream, you may provide a `StreamedEvent` instance to the `eventStream` method's `endStreamWith` argument:

```php
return response()->eventStream(function () {
    // ...
}, endStreamWith: new StreamedEvent(event: 'update', data: '</stream>'));
```

---

## Seeding Writing Seeders Using Model Factories
Seeding | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/seeding.md

### Using Model Factories

Of course, manually specifying the attributes for each model seed is cumbersome. Instead, you can use [model factories](/docs/{{version}}/eloquent-factories) to conveniently generate large amounts of database records. First, review the [model factory documentation](/docs/{{version}}/eloquent-factories) to learn how to define your factories.

For example, let's create 50 users that each has one related post:

```php
use App\Models\User;

/**
 * Run the database seeders.
 */
public function run(): void
{
    User::factory()
        ->count(50)
        ->hasPosts(1)
        ->create();
}
```

_[Truncated]_


output:
Wall time: 3.4407 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Responses Streamed Responses Streamed Downloads\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n### Streamed Downloads\n\nSometimes you may wish to turn the string response of a given operation into a downloadable response without having to write the contents of the operation to disk. You may use the `streamDownload` method in this scenario. This method accepts a callback, filename, and an optional array of headers as its arguments:\n\n```php\nuse App\\Services\\GitHub;\n\nreturn response()->streamDownload(function () {\n    echo GitHub::api('repo')\n        ->contents()\n        ->readme('laravel', 'laravel')['contents'];\n}, 'laravel-readme.md');\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Storage Local Filesystem Disk Default Root Path\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Local Filesystem Disk Default Root Path\n\n**Likelihood Of Impact: Low**\n\nIf your application does not explicitly define a `local` disk in your filesystems configuration, Laravel will now default the local disk's root to `storage/app/private`. In previous releases, this defaulted to `storage/app`. As a result, calls to `Storage::disk('local')` will read from and write to `storage/app/private` unless otherwise configured. To restore the previous behavior, you may define the `local` disk manually and set the desired root path.\n\n---\n\n## Eloquent Generating Model Classes\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n## Generating Model Classes\n\nTo get started, let's create an Eloquent model. Models typically live in the `app\\Models` directory and extend the `Illuminate\\Database\\Eloquent\\Model` class. You may use the `make:model` [Artisan command](/docs/{{version}}/artisan) to generate a new model:\n\n```shell\nphp artisan make:model Flight\n```\n\nIf you would like to generate a [database migration](/docs/{{version}}/migrations) when you generate the model, you may use the `--migration` or `-m` option:\n\n```shell\nphp artisan make:model Flight --migration\n```\n\nYou may generate various other types of classes when generating a model, such as factories, seeders, policies, controllers, and form requests. In addition, these options may be combined to create multiple classes at once:\n\n```shell\n# Generate a model and a FlightFactory class...\nphp artisan make:model Flight --factory\nphp artisan make:model Flight -f\n\n# Generate a model and a FlightSeeder class...\nphp artisan make:model Flight --seed\nphp artisan make:model Flight -s\n\n# Generate a model and a FlightController class...\nphp artisan make:model Flight --controller\nphp artisan make:model Flight -c\n\n# Generate a model, FlightController resource class, and form request classes...\nphp artisan make:model Flight --controller --resource --requests\nphp artisan make:model Flight -crR\n\n# Generate a model and a FlightPolicy class...\nphp artisan make:model Flight --policy\n\n# Generate a model and a migration, factory, seeder, and controller...\nphp artisan make:model Flight -mfsc\n\n# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...\nphp artisan make:model Flight --all\nphp artisan make:model Flight -a\n\n# Generate a pivot model...\nphp artisan make:model Member --pivot\nphp artisan make:model Member -p\n```\n\n---\n\n## Structure The Root Directory The Storage Directory\nStructure | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/structure.md\n\n\n### The Storage Directory\n\nThe `storage` directory contains your logs, compiled Blade templates, file based sessions, file caches, and other files generated by the framework. This directory is segregated into `app`, `framework`, and `logs` directories. The `app` directory may be used to store any files generated by your application. The `framework` directory is used to store framework generated files and caches. Finally, the `logs` directory contains your application's log files.\n\nThe `storage/app/public` directory may be used to store user-generated files, such as profile avatars, that should be publicly accessible. You should create a symbolic link at `public/storage` which points to this directory. You may create the link using the `php artisan storage:link` Artisan command.\n\n---\n\n## Sail File Storage\nSail | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sail.md\n\n\n## File Storage\n\nIf you plan to use Amazon S3 to store files while running your application in its production environment, you may wish to install the [RustFS](https://rustfs.com) service when installing Sail. RustFS provides an S3 compatible API that you may use to develop locally using Laravel's `s3` file storage driver without creating \"test\" storage buckets in your production S3 environment. If you choose to install RustFS while installing Sail, a RustFS configuration section will be added to your application's `compose.yaml` file.\n\nBy default, your application's `filesystems` configuration file already contains a disk configuration for the `s3` disk. In addition to using this disk to interact with Amazon S3, you may use it to interact with any S3 compatible file storage service such as RustFS by simply modifying the associated environment variables that control its configuration. For example, when using RustFS, your filesystem environment variable configuration should be defined as follows:\n\n```ini\nFILESYSTEM_DISK=s3\nAWS_ACCESS_KEY_ID=sail\nAWS_SECRET_ACCESS_KEY=password\nAWS_DEFAULT_REGION=us-east-1\nAWS_BUCKET=local\nAWS_ENDPOINT=http://rustfs:9000\nAWS_USE_PATH_STYLE_ENDPOINT=true\n```\n\n---\n\n## Eloquent Factories Defining Model Factories Generating Factories\nEloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md\n\n\n### Generating Factories\n\nTo create a factory, execute the `make:factory` [Artisan command](/docs/{{version}}/artisan):\n\n```shell\nphp artisan make:factory PostFactory\n```\n\nThe new factory class will be placed in your `database/factories` directory.\n\n---\n\n## Responses Streamed Responses\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n## Streamed Responses\n\nBy streaming data to the client as it is generated, you can significantly reduce memory usage and improve performance, especially for very large responses. Streamed responses allow the client to begin processing data before the server has finished sending it:\n\n```php\nRoute::get('/stream', function () {\n    return response()->stream(function (): void {\n        foreach (['developer', 'admin'] as $string) {\n            echo $string;\n            ob_flush();\n            flush();\n            sleep(2); // Simulate delay between chunks...\n        }\n    }, 200, ['X-Accel-Buffering' => 'no']);\n});\n```\n\nFor convenience, if the closure you provide to the `stream` method returns a [Generator](https://www.php.net/manual/en/language.generators.overview.php), Laravel will automatically flush the output buffer between strings returned by the generator, as well as disable Nginx output buffering:\n\n```php\nRoute::post('/chat', function () {\n    return response()->stream(function (): Generator {\n        $stream = OpenAI::client()->chat()->createStreamed(...);\n\n        foreach ($stream as $response) {\n            yield $response->choices[0];\n        }\n    });\n});\n```\n\n---\n\n## Responses Streamed Responses Event Streams (SSE) Consuming Event Streams\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n#### Consuming Event Streams\n\nEvent streams may be consumed using Laravel's `stream` npm package, which provides a convenient API for interacting with Laravel event streams. To get started, install the `@laravel/stream-react`, `@laravel/stream-vue`, or `@laravel/stream-svelte` package:\n\n```shell tab=React\nnpm install @laravel/stream-react\n```\n\n```shell tab=Vue\nnpm install @laravel/stream-vue\n```\n\n```shell tab=Svelte\nnpm install @laravel/stream-svelte\n```\n\nThen, `useEventStream` may be used to consume the event stream. After providing your stream URL, the hook will automatically update the `message` with the concatenated response as messages are returned from your Laravel application:\n\n```jsx tab=React\nimport { useEventStream } from \"@laravel/stream-react\";\n\nfunction App() {\n  const { message } = useEventStream(\"/chat\");\n\n  return <div>{message}</div>;\n}\n```\n\n```vue tab=Vue\n<script setup lang=\"ts\">\nimport { useEventStream } from \"@laravel/stream-vue\";\n\nconst { message } = useEventStream(\"/chat\");\n</script>\n\n<template>\n  <div>{{ message }}</div>\n</template>\n```\n\n```svelte tab=Svelte\n<script>\nimport { useEventStream } from \"@laravel/stream-svelte\";\n\nconst eventStream = useEventStream(\"/chat\");\n</script>\n\n<div>{$eventStream.message}</div>\n```\n\nThe second argument given to `useEventStream` is an options object that you may use to customize the stream consumption behavior. The default values for this object are shown below:\n\n```jsx tab=React\nimport { useEventStream } from \"@laravel/stream-react\";\n\nfunction App() {\n  const { message } = useEventStream(\"/stream\", {\n    eventName: \"update\",\n    onMessage: (message) => {\n      //\n    },\n    onError: (error) => {\n      //\n    },\n    onComplete: () => {\n      //\n    },\n    endSignal: \"</stream>\",\n    glue: \" \",\n  });\n\n  return <div>{message}</div>;\n}\n```\n\n```vue tab=Vue\n<script setup lang=\"ts\">\nimport { useEventStream } from \"@laravel/stream-vue\";\n\nconst { message } = useEventStream(\"/chat\", {\n  eventName: \"update\",\n  onMessage: (message) => {\n    // ...\n  },\n  onError: (error) => {\n    // ...\n  },\n  onComplete: () => {\n    // ...\n  },\n  endSignal: \"</stream>\",\n  glue: \" \",\n});\n</script>\n```\n\n```svelte tab=Svelte\n<script>\nimport { useEventStream } from \"@laravel/stream-svelte\";\n\nconst eventStream = useEventStream(\"/chat\", {\n    eventName: \"update\",\n    onMessage: (event) => {\n        //\n    },\n    onError: (error) => {\n        //\n    },\n    onComplete: () => {\n        //\n    },\n    endSignal: \"</stream>\",\n    glue: \" \",\n    replace: false,\n});\n</script>\n```\n\nEvent streams may also be manually consumed via an [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) object by your application's frontend. The `eventStream` method will automatically send a `</stream>` update to the event stream when the stream is complete:\n\n```js\nconst source = new EventSource('/chat');\n\nsource.addEventListener('update', (event) => {\n    if (event.data === '</stream>') {\n        source.close();\n\n        return;\n    }\n\n    console.log(event.data);\n});\n```\n\nTo customize the final event that is sent to the event stream, you may provide a `StreamedEvent` instance to the `eventStream` method's `endStreamWith` argument:\n\n```php\nreturn response()->eventStream(function () {\n    // ...\n}, endStreamWith: new StreamedEvent(event: 'update', data: '</stream>'));\n```\n\n---\n\n## Seeding Writing Seeders Using Model Factories\nSeeding | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/seeding.md\n\n\n### Using Model Factories\n\nOf course, manually specifying the attributes for each model seed is cumbersome. Instead, you can use [model factories](/docs/{{version}}/eloquent-factories) to conveniently generate large amounts of database records. First, review the [model factory documentation](/docs/{{version}}/eloquent-factories) to learn how to define your factories.\n\nFor example, let's create 50 users that each has one related post:\n\n```php\nuse App\\Models\\User;\n\n/**\n * Run the database seeders.\n */\npublic function run(): void\n{\n    User::factory()\n        ->count(50)\n        ->hasPosts(1)\n        ->create();\n}\n```\n\n_[Truncated]_"}]


cmd: sed -n '2760,2898p' specs/02-API-ROUTES.md


cmd: sed -n '1270,1292p' specs/05-BUSINESS-LOGIC.md


cmd: rg -n "exportCsv|CSV|export" app tests resources specs/progress.md


cmd: find app/Models -maxdepth 1 -type f | sort | rg 'Export|Order|Payment|Fulfillment'


cmd: sed -n '1,240p' app/Http/Controllers/Api/Admin/V1/OrderController.php


cmd: sed -n '1,240p' app/Http/Resources/Admin/V1/OrderResource.php


command: sed -n '2760,2898p' specs/02-API-ROUTES.md

type: read

aggregated_output:
```

---

### 3.11 Analytics and Exports

#### Admin Analytics Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/analytics/summary` | `read-analytics` | Get analytics summary for date range | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/exports/orders` | `read-orders` | Queue an order export job (async) | `api.admin` (60/min) |
| GET | `/api/admin/v1/stores/{storeId}/exports/{exportId}` | `read-orders` | Check export status and get download URL | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/analytics/summary

Get analytics summary for a date range.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from` | string (YYYY-MM-DD) | Yes | Start date. |
| `to` | string (YYYY-MM-DD) | Yes | End date. Max range: 365 days. |
| `granularity` | string | No | One of: `day`, `week`, `month`. Default: `day`. |

**Success response:** `200 OK`

```json
{
  "data": {
    "period": {
      "from": "2026-02-01",
      "to": "2026-02-11"
    },
    "summary": {
      "orders_count": 156,
      "revenue_amount": 1250000,
      "aov_amount": 8012,
      "visits_count": 4520,
      "add_to_cart_count": 890,
      "checkout_started_count": 420,
      "conversion_rate": 0.0345,
      "currency": "EUR"
    },
    "daily": [
      {
        "date": "2026-02-01",
        "orders_count": 12,
        "revenue_amount": 95000,
        "aov_amount": 7917,
        "visits_count": 380,
        "add_to_cart_count": 72,
        "checkout_started_count": 35
      }
    ],
    "top_products": [
      {
        "product_id": 10,
        "title": "Classic T-Shirt",
        "units_sold": 45,
        "revenue_amount": 112500
      }
    ]
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `422` - Invalid date range

---

#### POST /api/admin/v1/stores/{storeId}/exports/orders

Queue an order export job. The export runs asynchronously and produces a downloadable CSV.

**Request body:**

```json
{
  "format": "csv",
  "filters": {
    "status": "paid",
    "created_after": "2026-01-01T00:00:00Z",
    "created_before": "2026-02-11T23:59:59Z"
  }
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `format` | string | No | One of: `csv`. Default: `csv`. |
| `filters` | object | No | Same filter options as GET /orders. |

**Success response:** `202 Accepted`

```json
{
  "export_id": 1,
  "status": "queued",
  "created_at": "2026-02-11T10:00:00Z"
}
```

---

#### GET /api/admin/v1/stores/{storeId}/exports/{exportId}

Check export status and get download URL.

**Success response:** `200 OK`

```json
{
  "data": {
    "id": 1,
    "status": "completed",
    "format": "csv",
    "row_count": 156,
    "download_url": "https://s3.example.com/...presigned...",
    "download_expires_at": "2026-02-11T11:00:00Z",
    "created_at": "2026-02-11T10:00:00Z",
    "completed_at": "2026-02-11T10:01:30Z"
  }
}
```

**Export status values:** `queued`, `processing`, `completed`, `failed`.

**Error responses:**
- `403` - Insufficient permissions
- `404` - Export not found

---

## 4. Webhook Receiver Endpoints

status: completed


command: sed -n '1270,1292p' specs/05-BUSINESS-LOGIC.md

aggregated_output:
   - `status = 'delivered'` (skip pending and shipped)
   - `tracking_company = null`, `tracking_number = null`, `tracking_url = null`
   - `shipped_at = now`
   - Set `order.fulfillment_status = 'fulfilled'`
   - Set `order.status = 'fulfilled'`
3. If the order contains a mix of physical and digital products: do **not** auto-fulfill. The admin must create fulfillments manually (they can fulfill digital items immediately and physical items when shipped).

This ensures digital product orders are immediately marked as complete after payment.

### 11.6 Order Export

`GET /api/admin/v1/orders/export` returns a CSV file of orders matching the current filter criteria.

**CSV columns:** `order_number`, `created_at`, `status`, `financial_status`, `fulfillment_status`, `customer_email`, `customer_name`, `subtotal_amount`, `discount_amount`, `shipping_amount`, `tax_amount`, `total_amount`, `currency`, `shipping_method`, `tracking_number`

All monetary values are exported in minor units (cents). Dates use ISO-8601 format. The response uses `Content-Type: text/csv` with `Content-Disposition: attachment; filename="orders-{date}.csv"`.

---

## 12. Customer Accounts

### 12.1 Registration


output:
Chunk ID: 73880f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 822
Output:
```

---

### 3.11 Analytics and Exports

#### Admin Analytics Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/analytics/summary` | `read-analytics` | Get analytics summary for date range | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/exports/orders` | `read-orders` | Queue an order export job (async) | `api.admin` (60/min) |
| GET | `/api/admin/v1/stores/{storeId}/exports/{exportId}` | `read-orders` | Check export status and get download URL | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/analytics/summary

Get analytics summary for a date range.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from` | string (YYYY-MM-DD) | Yes | Start date. |
| `to` | string (YYYY-MM-DD) | Yes | End date. Max range: 365 days. |
| `granularity` | string | No | One of: `day`, `week`, `month`. Default: `day`. |

**Success response:** `200 OK`

```json
{
  "data": {
    "period": {
      "from": "2026-02-01",
      "to": "2026-02-11"
    },
    "summary": {
      "orders_count": 156,
      "revenue_amount": 1250000,
      "aov_amount": 8012,
      "visits_count": 4520,
      "add_to_cart_count": 890,
      "checkout_started_count": 420,
      "conversion_rate": 0.0345,
      "currency": "EUR"
    },
    "daily": [
      {
        "date": "2026-02-01",
        "orders_count": 12,
        "revenue_amount": 95000,
        "aov_amount": 7917,
        "visits_count": 380,
        "add_to_cart_count": 72,
        "checkout_started_count": 35
      }
    ],
    "top_products": [
      {
        "product_id": 10,
        "title": "Classic T-Shirt",
        "units_sold": 45,
        "revenue_amount": 112500
      }
    ]
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `422` - Invalid date range

---

#### POST /api/admin/v1/stores/{storeId}/exports/orders

Queue an order export job. The export runs asynchronously and produces a downloadable CSV.

**Request body:**

```json
{
  "format": "csv",
  "filters": {
    "status": "paid",
    "created_after": "2026-01-01T00:00:00Z",
    "created_before": "2026-02-11T23:59:59Z"
  }
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `format` | string | No | One of: `csv`. Default: `csv`. |
| `filters` | object | No | Same filter options as GET /orders. |

**Success response:** `202 Accepted`

```json
{
  "export_id": 1,
  "status": "queued",
  "created_at": "2026-02-11T10:00:00Z"
}
```

---

#### GET /api/admin/v1/stores/{storeId}/exports/{exportId}

Check export status and get download URL.

**Success response:** `200 OK`

```json
{
  "data": {
    "id": 1,
    "status": "completed",
    "format": "csv",
    "row_count": 156,
    "download_url": "https://s3.example.com/...presigned...",
    "download_expires_at": "2026-02-11T11:00:00Z",
    "created_at": "2026-02-11T10:00:00Z",
    "completed_at": "2026-02-11T10:01:30Z"
  }
}
```

**Export status values:** `queued`, `processing`, `completed`, `failed`.

**Error responses:**
- `403` - Insufficient permissions
- `404` - Export not found

---

## 4. Webhook Receiver Endpoints


output:
Chunk ID: 71cb3d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 299
Output:
   - `status = 'delivered'` (skip pending and shipped)
   - `tracking_company = null`, `tracking_number = null`, `tracking_url = null`
   - `shipped_at = now`
   - Set `order.fulfillment_status = 'fulfilled'`
   - Set `order.status = 'fulfilled'`
3. If the order contains a mix of physical and digital products: do **not** auto-fulfill. The admin must create fulfillments manually (they can fulfill digital items immediately and physical items when shipped).

This ensures digital product orders are immediately marked as complete after payment.

### 11.6 Order Export

`GET /api/admin/v1/orders/export` returns a CSV file of orders matching the current filter criteria.

**CSV columns:** `order_number`, `created_at`, `status`, `financial_status`, `fulfillment_status`, `customer_email`, `customer_name`, `subtotal_amount`, `discount_amount`, `shipping_amount`, `tax_amount`, `total_amount`, `currency`, `shipping_method`, `tracking_number`

All monetary values are exported in minor units (cents). Dates use ISO-8601 format. The response uses `Content-Type: text/csv` with `Content-Disposition: attachment; filename="orders-{date}.csv"`.

---

## 12. Customer Accounts

### 12.1 Registration


command: sed -n '1,240p' app/Http/Controllers/Api/Admin/V1/OrderController.php

aggregated_output:
<?php

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

use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\OrderResource;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Validation\Rule;

class OrderController extends Controller
{
    public function index(Request $request, Store $store): AnonymousResourceCollection
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'status' => ['nullable', Rule::in(['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])],
            'financial_status' => ['nullable', Rule::in(['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])],
            'fulfillment_status' => ['nullable', Rule::in(['unfulfilled', 'partial', 'fulfilled'])],
            'query' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ]);

        $orders = Order::withoutGlobalScopes()
            ->with('customer')
            ->withCount('lines')
            ->where('store_id', $store->getKey())
            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $query->where('status', $status))
            ->when(data_get($validated, 'financial_status'), fn (Builder $query, string $status) => $query->where('financial_status', $status))
            ->when(data_get($validated, 'fulfillment_status'), fn (Builder $query, string $status) => $query->where('fulfillment_status', $status))
            ->when(data_get($validated, 'query'), function (Builder $query, string $search): void {
                $like = '%'.$search.'%';

                $query->where(function (Builder $query) use ($like): void {
                    $query
                        ->where('order_number', 'like', $like)
                        ->orWhere('email', 'like', $like)
                        ->orWhereHas('customer', fn (Builder $query) => $query->where('name', 'like', $like));
                });
            })
            ->latest('placed_at')
            ->latest('id')
            ->paginate((int) data_get($validated, 'per_page', 25));

        return OrderResource::collection($orders);
    }

    public function show(Request $request, Store $store, Order $order): OrderResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessOrderBelongsToStore($order, $store);

        return OrderResource::make($this->loadOrder($order));
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        if (! $request->attributes->has('admin_api_oauth_token')) {
            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
        }

        app()->instance('current_store', $store);
    }

    private function abortUnlessOrderBelongsToStore(Order $order, Store $store): void
    {
        abort_unless((int) $order->store_id === $store->getKey(), 404);
    }

    private function loadOrder(Order $order): Order
    {
        return $order->load([
            'customer',
            'lines',
            'payments',
            'refunds',
            'fulfillments.lines',
        ])->loadCount('lines');
    }
}


command: sed -n '1,240p' app/Http/Resources/Admin/V1/OrderResource.php

aggregated_output:
<?php

namespace App\Http\Resources\Admin\V1;

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

class OrderResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'order_number' => $this->order_number,
            'email' => $this->email,
            'customer' => $this->whenLoaded('customer', fn () => $this->customer ? [
                'id' => $this->customer->id,
                'name' => $this->customer->name,
                'email' => $this->customer->email,
            ] : null),
            'status' => $this->status?->value,
            'financial_status' => $this->financial_status?->value,
            'fulfillment_status' => $this->fulfillment_status?->value,
            'payment_method' => $this->payment_method?->value,
            'currency' => $this->currency,
            'subtotal_amount' => $this->subtotal_amount,
            'discount_amount' => $this->discount_amount,
            'shipping_amount' => $this->shipping_amount,
            'tax_amount' => $this->tax_amount,
            'total_amount' => $this->total_amount,
            'lines_count' => $this->whenCounted('lines'),
            'lines' => $this->whenLoaded('lines', fn () => $this->lines->map(fn ($line): array => [
                'id' => $line->id,
                'product_id' => $line->product_id,
                'variant_id' => $line->variant_id,
                'title' => $line->title_snapshot,
                'sku' => $line->sku_snapshot,
                'quantity' => $line->quantity,
                'unit_price_amount' => $line->unit_price_amount,
                'total_amount' => $line->total_amount,
            ])->values()),
            'payments' => $this->whenLoaded('payments', fn () => $this->payments->map(fn ($payment): array => [
                'id' => $payment->id,
                'provider' => $payment->provider,
                'method' => $payment->method?->value,
                'provider_payment_id' => $payment->provider_payment_id,
                'status' => $payment->status?->value,
                'amount' => $payment->amount,
                'currency' => $payment->currency,
            ])->values()),
            'refunds' => RefundResource::collection($this->whenLoaded('refunds')),
            'fulfillments' => FulfillmentResource::collection($this->whenLoaded('fulfillments')),
            'shipping_address' => $this->shipping_address_json,
            'billing_address' => $this->billing_address_json,
            'placed_at' => $this->placed_at?->toIso8601String(),
            'created_at' => $this->created_at?->toIso8601String(),
        ];
    }
}


command: find app/Models -maxdepth 1 -type f | sort | rg 'Export|Order|Payment|Fulfillment'

cmd: find app/Models -maxdepth 1 -type f

path: Models

type: search

cmd: rg 'Export|Order|Payment|Fulfillment'

query: Export|Order|Payment|Fulfillment

aggregated_output:
app/Models/Fulfillment.php
app/Models/FulfillmentLine.php
app/Models/Order.php
app/Models/OrderLine.php
app/Models/Payment.php


command: rg -n "exportCsv|CSV|export" app tests resources specs/progress.md

type: search

cmd: rg -n 'exportCsv|CSV|export' app tests resources specs/progress.md

query: exportCsv|CSV|export

path: app

aggregated_output:
specs/progress.md:31:| 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. |
specs/progress.md:36:| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | 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 smoke coverage now checks storefront core pages, authenticated admin core pages, and mobile storefront rendering for JavaScript errors. Full Spec 08 browser suites are still incomplete. |
specs/progress.md:218:- 2026-05-04: Playwright MCP verified `http://shop.test/admin/analytics` renders metrics, charts, top referrers, and CSV export; browser fetch to `/api/storefront/v1/analytics/events` returned 202 with 1 accepted event and current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
specs/progress.md:453:- The admin analytics CSV export is generated as a data URL for the self-contained local app rather than creating persistent export files/jobs.
specs/progress.md:468:- Admin REST export surfaces are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
resources/views/livewire/admin/analytics/index.blade.php:35:        <flux:button wire:click="exportCsv" icon="arrow-down-tray" variant="ghost" wire:loading.attr="disabled">
resources/views/livewire/admin/analytics/index.blade.php:36:            Export CSV
resources/views/livewire/admin/analytics/index.blade.php:37:            Exporting...
resources/views/livewire/admin/analytics/index.blade.php:40:        @if ($exportUrl)
resources/views/livewire/admin/analytics/index.blade.php:41:            <flux:button :href="$exportUrl" download="analytics.csv" icon="document-arrow-down" variant="filled">
app/Livewire/Admin/Analytics/Index.php:69:    public ?string $exportUrl = null;
app/Livewire/Admin/Analytics/Index.php:148:    public function exportCsv(AnalyticsService $analytics): void
app/Livewire/Admin/Analytics/Index.php:159:        $this->exportUrl = 'data:text/csv;charset=utf-8,'.rawurlencode(implode("\n", $lines));
tests/Feature/Admin/AnalyticsDashboardTest.php:56:test('admin analytics component filters and exports csv data', function (): void {
tests/Feature/Admin/AnalyticsDashboardTest.php:66:        ->call('exportCsv')
tests/Feature/Admin/AnalyticsDashboardTest.php:68:        ->assertSet('exportUrl', fn (?string $url): bool => str_starts_with((string) $url, 'data:text/csv'));


output:
Chunk ID: e3be36
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1083
Output:
specs/progress.md:31:| 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. |
specs/progress.md:36:| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | 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 smoke coverage now checks storefront core pages, authenticated admin core pages, and mobile storefront rendering for JavaScript errors. Full Spec 08 browser suites are still incomplete. |
specs/progress.md:218:- 2026-05-04: Playwright MCP verified `http://shop.test/admin/analytics` renders metrics, charts, top referrers, and CSV export; browser fetch to `/api/storefront/v1/analytics/events` returned 202 with 1 accepted event and current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
specs/progress.md:453:- The admin analytics CSV export is generated as a data URL for the self-contained local app rather than creating persistent export files/jobs.
specs/progress.md:468:- Admin REST export surfaces are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
resources/views/livewire/admin/analytics/index.blade.php:35:        <flux:button wire:click="exportCsv" icon="arrow-down-tray" variant="ghost" wire:loading.attr="disabled">
resources/views/livewire/admin/analytics/index.blade.php:36:            Export CSV
resources/views/livewire/admin/analytics/index.blade.php:37:            Exporting...
resources/views/livewire/admin/analytics/index.blade.php:40:        @if ($exportUrl)
resources/views/livewire/admin/analytics/index.blade.php:41:            <flux:button :href="$exportUrl" download="analytics.csv" icon="document-arrow-down" variant="filled">
app/Livewire/Admin/Analytics/Index.php:69:    public ?string $exportUrl = null;
app/Livewire/Admin/Analytics/Index.php:148:    public function exportCsv(AnalyticsService $analytics): void
app/Livewire/Admin/Analytics/Index.php:159:        $this->exportUrl = 'data:text/csv;charset=utf-8,'.rawurlencode(implode("\n", $lines));
tests/Feature/Admin/AnalyticsDashboardTest.php:56:test('admin analytics component filters and exports csv data', function (): void {
tests/Feature/Admin/AnalyticsDashboardTest.php:66:        ->call('exportCsv')
tests/Feature/Admin/AnalyticsDashboardTest.php:68:        ->assertSet('exportUrl', fn (?string $url): bool => str_starts_with((string) $url, 'data:text/csv'));


output:
Chunk ID: bcab0e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 32
Output:
app/Models/Fulfillment.php
app/Models/FulfillmentLine.php
app/Models/Order.php
app/Models/OrderLine.php
app/Models/Payment.php


output:
Chunk ID: faf8b6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 839
Output:
<?php

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

use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\OrderResource;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Validation\Rule;

class OrderController extends Controller
{
    public function index(Request $request, Store $store): AnonymousResourceCollection
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'status' => ['nullable', Rule::in(['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])],
            'financial_status' => ['nullable', Rule::in(['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])],
            'fulfillment_status' => ['nullable', Rule::in(['unfulfilled', 'partial', 'fulfilled'])],
            'query' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ]);

        $orders = Order::withoutGlobalScopes()
            ->with('customer')
            ->withCount('lines')
            ->where('store_id', $store->getKey())
            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $query->where('status', $status))
            ->when(data_get($validated, 'financial_status'), fn (Builder $query, string $status) => $query->where('financial_status', $status))
            ->when(data_get($validated, 'fulfillment_status'), fn (Builder $query, string $status) => $query->where('fulfillment_status', $status))
            ->when(data_get($validated, 'query'), function (Builder $query, string $search): void {
                $like = '%'.$search.'%';

                $query->where(function (Builder $query) use ($like): void {
                    $query
                        ->where('order_number', 'like', $like)
                        ->orWhere('email', 'like', $like)
                        ->orWhereHas('customer', fn (Builder $query) => $query->where('name', 'like', $like));
                });
            })
            ->latest('placed_at')
            ->latest('id')
            ->paginate((int) data_get($validated, 'per_page', 25));

        return OrderResource::collection($orders);
    }

    public function show(Request $request, Store $store, Order $order): OrderResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessOrderBelongsToStore($order, $store);

        return OrderResource::make($this->loadOrder($order));
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        if (! $request->attributes->has('admin_api_oauth_token')) {
            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
        }

        app()->instance('current_store', $store);
    }

    private function abortUnlessOrderBelongsToStore(Order $order, Store $store): void
    {
        abort_unless((int) $order->store_id === $store->getKey(), 404);
    }

    private function loadOrder(Order $order): Order
    {
        return $order->load([
            'customer',
            'lines',
            'payments',
            'refunds',
            'fulfillments.lines',
        ])->loadCount('lines');
    }
}


output:
Chunk ID: 17b14f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 718
Output:
<?php

namespace App\Http\Resources\Admin\V1;

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

class OrderResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'order_number' => $this->order_number,
            'email' => $this->email,
            'customer' => $this->whenLoaded('customer', fn () => $this->customer ? [
                'id' => $this->customer->id,
                'name' => $this->customer->name,
                'email' => $this->customer->email,
            ] : null),
            'status' => $this->status?->value,
            'financial_status' => $this->financial_status?->value,
            'fulfillment_status' => $this->fulfillment_status?->value,
            'payment_method' => $this->payment_method?->value,
            'currency' => $this->currency,
            'subtotal_amount' => $this->subtotal_amount,
            'discount_amount' => $this->discount_amount,
            'shipping_amount' => $this->shipping_amount,
            'tax_amount' => $this->tax_amount,
            'total_amount' => $this->total_amount,
            'lines_count' => $this->whenCounted('lines'),
            'lines' => $this->whenLoaded('lines', fn () => $this->lines->map(fn ($line): array => [
                'id' => $line->id,
                'product_id' => $line->product_id,
                'variant_id' => $line->variant_id,
                'title' => $line->title_snapshot,
                'sku' => $line->sku_snapshot,
                'quantity' => $line->quantity,
                'unit_price_amount' => $line->unit_price_amount,
                'total_amount' => $line->total_amount,
            ])->values()),
            'payments' => $this->whenLoaded('payments', fn () => $this->payments->map(fn ($payment): array => [
                'id' => $payment->id,
                'provider' => $payment->provider,
                'method' => $payment->method?->value,
                'provider_payment_id' => $payment->provider_payment_id,
                'status' => $payment->status?->value,
                'amount' => $payment->amount,
                'currency' => $payment->currency,
            ])->values()),
            'refunds' => RefundResource::collection($this->whenLoaded('refunds')),
            'fulfillments' => FulfillmentResource::collection($this->whenLoaded('fulfillments')),
            'shipping_address' => $this->shipping_address_json,
            'billing_address' => $this->billing_address_json,
            'placed_at' => $this->placed_at?->toIso8601String(),
            'created_at' => $this->created_at?->toIso8601String(),
        ];
    }
}


cmd: sed -n '1,260p' database/factories/OrderFactory.php


cmd: sed -n '1,220p' app/Models/Fulfillment.php


cmd: sed -n '1,220p' database/migrations/*create_orders* 2>/dev/null


cmd: sed -n '1,260p' tests/Feature/Api/AdminOrderApiTest.php


cmd: rg -n "enum .*Status|case Completed|ExportStatus" app/Enums app/Models database/factories


command:
/bin/zsh
-lc
sed -n '1,260p' app/Models/Order.php

Order.php

cmd: sed -n '1,260p' app/Models/Order.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Order extends Model
{
    /** @use HasFactory<\Database\Factories\OrderFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'checkout_id',
        'customer_id',
        'order_number',
        'payment_method',
        'status',
        'financial_status',
        'fulfillment_status',
        'currency',
        'subtotal_amount',
        'discount_amount',
        'shipping_amount',
        'tax_amount',
        'total_amount',
        'email',
        'billing_address_json',
        'shipping_address_json',
        'placed_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'pending',
        'financial_status' => 'pending',
        'fulfillment_status' => 'unfulfilled',
        'currency' => 'USD',
        'subtotal_amount' => 0,
        'discount_amount' => 0,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => 0,
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return BelongsTo<Checkout, $this>
     */
    public function checkout(): BelongsTo
    {
        return $this->belongsTo(Checkout::class);
    }

    /**
     * @return BelongsTo<Customer, $this>
     */
    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }

    /**
     * @return HasMany<OrderLine, $this>
     */
    public function lines(): HasMany
    {
        return $this->hasMany(OrderLine::class);
    }

    /**
     * @return HasMany<Payment, $this>
     */
    public function payments(): HasMany
    {
        return $this->hasMany(Payment::class);
    }

    /**
     * @return HasMany<Refund, $this>
     */
    public function refunds(): HasMany
    {
        return $this->hasMany(Refund::class);
    }

    /**
     * @return HasMany<Fulfillment, $this>
     */
    public function fulfillments(): HasMany
    {
        return $this->hasMany(Fulfillment::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'payment_method' => PaymentMethod::class,
            'status' => OrderStatus::class,
            'financial_status' => FinancialStatus::class,
            'fulfillment_status' => FulfillmentStatus::class,
            'subtotal_amount' => 'integer',
            'discount_amount' => 'integer',
            'shipping_amount' => 'integer',
            'tax_amount' => 'integer',
            'total_amount' => 'integer',
            'billing_address_json' => 'array',
            'shipping_address_json' => 'array',
            'placed_at' => 'datetime',
        ];
    }
}


command: sed -n '1,260p' database/factories/OrderFactory.php

aggregated_output:
<?php

namespace Database\Factories;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Order>
 */
class OrderFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $subtotal = fake()->numberBetween(2500, 25000);
        $shipping = fake()->randomElement([0, 499, 799]);
        $tax = 0;

        return [
            'store_id' => Store::factory(),
            'checkout_id' => null,
            'customer_id' => null,
            'order_number' => '#'.fake()->unique()->numberBetween(1001, 9999),
            'payment_method' => PaymentMethod::CreditCard,
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Unfulfilled,
            'currency' => 'EUR',
            'subtotal_amount' => $subtotal,
            'discount_amount' => 0,
            'shipping_amount' => $shipping,
            'tax_amount' => $tax,
            'total_amount' => $subtotal + $shipping + $tax,
            'email' => fake()->safeEmail(),
            'billing_address_json' => $this->address(),
            'shipping_address_json' => $this->address(),
            'placed_at' => now(),
        ];
    }

    public function forCheckout(?Checkout $checkout = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'checkout_id' => $checkout?->getKey() ?? Checkout::factory(),
        ]);
    }

    public function forCustomer(?Customer $customer = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'customer_id' => $customer?->getKey() ?? Customer::factory(),
        ]);
    }

    public function paid(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
        ]);
    }

    public function pending(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function creditCard(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::CreditCard,
        ]);
    }

    public function paypal(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::Paypal,
        ]);
    }

    public function bankTransfer(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::BankTransfer,
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function cancelled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Cancelled,
            'financial_status' => FinancialStatus::Voided,
        ]);
    }

    public function partiallyFulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'fulfillment_status' => FulfillmentStatus::Partial,
        ]);
    }

    public function fulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Fulfilled,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Fulfilled,
        ]);
    }

    public function refunded(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Refunded,
            'financial_status' => FinancialStatus::Refunded,
        ]);
    }

    /**
     * @return array<string, string|null>
     */
    private function address(): array
    {
        return [
            'first_name' => fake()->firstName(),
            'last_name' => fake()->lastName(),
            'address1' => fake()->streetAddress(),
            'address2' => null,
            'city' => fake()->city(),
            'province_code' => null,
            'country' => 'DE',
            'postal_code' => fake()->postcode(),
        ];
    }
}


command: sed -n '1,220p' app/Models/Fulfillment.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\FulfillmentShipmentStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Fulfillment extends Model
{
    /** @use HasFactory<\Database\Factories\FulfillmentFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'order_id',
        'status',
        'tracking_company',
        'tracking_number',
        'tracking_url',
        'shipped_at',
        'delivered_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'pending',
    ];

    /**
     * @return BelongsTo<Order, $this>
     */
    public function order(): BelongsTo
    {
        return $this->belongsTo(Order::class);
    }

    /**
     * @return HasMany<FulfillmentLine, $this>
     */
    public function lines(): HasMany
    {
        return $this->hasMany(FulfillmentLine::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => FulfillmentShipmentStatus::class,
            'shipped_at' => 'datetime',
            'delivered_at' => 'datetime',
        ];
    }
}


output:
Chunk ID: 75289c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 808
Output:
<?php

namespace App\Models;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Order extends Model
{
    /** @use HasFactory<\Database\Factories\OrderFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'checkout_id',
        'customer_id',
        'order_number',
        'payment_method',
        'status',
        'financial_status',
        'fulfillment_status',
        'currency',
        'subtotal_amount',
        'discount_amount',
        'shipping_amount',
        'tax_amount',
        'total_amount',
        'email',
        'billing_address_json',
        'shipping_address_json',
        'placed_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'pending',
        'financial_status' => 'pending',
        'fulfillment_status' => 'unfulfilled',
        'currency' => 'USD',
        'subtotal_amount' => 0,
        'discount_amount' => 0,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => 0,
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return BelongsTo<Checkout, $this>
     */
    public function checkout(): BelongsTo
    {
        return $this->belongsTo(Checkout::class);
    }

    /**
     * @return BelongsTo<Customer, $this>
     */
    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }

    /**
     * @return HasMany<OrderLine, $this>
     */
    public function lines(): HasMany
    {
        return $this->hasMany(OrderLine::class);
    }

    /**
     * @return HasMany<Payment, $this>
     */
    public function payments(): HasMany
    {
        return $this->hasMany(Payment::class);
    }

    /**
     * @return HasMany<Refund, $this>
     */
    public function refunds(): HasMany
    {
        return $this->hasMany(Refund::class);
    }

    /**
     * @return HasMany<Fulfillment, $this>
     */
    public function fulfillments(): HasMany
    {
        return $this->hasMany(Fulfillment::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'payment_method' => PaymentMethod::class,
            'status' => OrderStatus::class,
            'financial_status' => FinancialStatus::class,
            'fulfillment_status' => FulfillmentStatus::class,
            'subtotal_amount' => 'integer',
            'discount_amount' => 'integer',
            'shipping_amount' => 'integer',
            'tax_amount' => 'integer',
            'total_amount' => 'integer',
            'billing_address_json' => 'array',
            'shipping_address_json' => 'array',
            'placed_at' => 'datetime',
        ];
    }
}


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

namespace Database\Factories;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Order>
 */
class OrderFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $subtotal = fake()->numberBetween(2500, 25000);
        $shipping = fake()->randomElement([0, 499, 799]);
        $tax = 0;

        return [
            'store_id' => Store::factory(),
            'checkout_id' => null,
            'customer_id' => null,
            'order_number' => '#'.fake()->unique()->numberBetween(1001, 9999),
            'payment_method' => PaymentMethod::CreditCard,
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Unfulfilled,
            'currency' => 'EUR',
            'subtotal_amount' => $subtotal,
            'discount_amount' => 0,
            'shipping_amount' => $shipping,
            'tax_amount' => $tax,
            'total_amount' => $subtotal + $shipping + $tax,
            'email' => fake()->safeEmail(),
            'billing_address_json' => $this->address(),
            'shipping_address_json' => $this->address(),
            'placed_at' => now(),
        ];
    }

    public function forCheckout(?Checkout $checkout = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'checkout_id' => $checkout?->getKey() ?? Checkout::factory(),
        ]);
    }

    public function forCustomer(?Customer $customer = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'customer_id' => $customer?->getKey() ?? Customer::factory(),
        ]);
    }

    public function paid(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
        ]);
    }

    public function pending(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function creditCard(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::CreditCard,
        ]);
    }

    public function paypal(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::Paypal,
        ]);
    }

    public function bankTransfer(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::BankTransfer,
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function cancelled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Cancelled,
            'financial_status' => FinancialStatus::Voided,
        ]);
    }

    public function partiallyFulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'fulfillment_status' => FulfillmentStatus::Partial,
        ]);
    }

    public function fulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Fulfilled,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Fulfilled,
        ]);
    }

    public function refunded(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Refunded,
            'financial_status' => FinancialStatus::Refunded,
        ]);
    }

    /**
     * @return array<string, string|null>
     */
    private function address(): array
    {
        return [
            'first_name' => fake()->firstName(),
            'last_name' => fake()->lastName(),
            'address1' => fake()->streetAddress(),
            'address2' => null,
            'city' => fake()->city(),
            'province_code' => null,
            'country' => 'DE',
            'postal_code' => fake()->postcode(),
        ];
    }
}


output:
Chunk ID: 9fed0a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 337
Output:
<?php

namespace App\Models;

use App\Enums\FulfillmentShipmentStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Fulfillment extends Model
{
    /** @use HasFactory<\Database\Factories\FulfillmentFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'order_id',
        'status',
        'tracking_company',
        'tracking_number',
        'tracking_url',
        'shipped_at',
        'delivered_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'pending',
    ];

    /**
     * @return BelongsTo<Order, $this>
     */
    public function order(): BelongsTo
    {
        return $this->belongsTo(Order::class);
    }

    /**
     * @return HasMany<FulfillmentLine, $this>
     */
    public function lines(): HasMany
    {
        return $this->hasMany(FulfillmentLine::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => FulfillmentShipmentStatus::class,
            'shipped_at' => 'datetime',
            'delivered_at' => 'datetime',
        ];
    }
}


command: sed -n '1,220p' database/migrations/*create_orders* 2>/dev/null

type: unknown

aggregated_output:
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('checkout_id')->nullable()->constrained()->nullOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('order_number');
            $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer']);
            $table->enum('status', ['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])->default('pending');
            $table->enum('financial_status', ['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])->default('pending');
            $table->enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled'])->default('unfulfilled');
            $table->string('currency', 3)->default('USD');
            $table->integer('subtotal_amount')->default(0);
            $table->integer('discount_amount')->default(0);
            $table->integer('shipping_amount')->default(0);
            $table->integer('tax_amount')->default(0);
            $table->integer('total_amount')->default(0);
            $table->string('email')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->timestamp('placed_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'order_number']);
            $table->unique('checkout_id');
            $table->index('store_id');
            $table->index('customer_id');
            $table->index(['store_id', 'status']);
            $table->index(['store_id', 'financial_status']);
            $table->index(['store_id', 'fulfillment_status']);
            $table->index(['store_id', 'placed_at']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('orders');
    }
};


output:
Chunk ID: f0dd50
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 571
Output:
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('checkout_id')->nullable()->constrained()->nullOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('order_number');
            $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer']);
            $table->enum('status', ['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])->default('pending');
            $table->enum('financial_status', ['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])->default('pending');
            $table->enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled'])->default('unfulfilled');
            $table->string('currency', 3)->default('USD');
            $table->integer('subtotal_amount')->default(0);
            $table->integer('discount_amount')->default(0);
            $table->integer('shipping_amount')->default(0);
            $table->integer('tax_amount')->default(0);
            $table->integer('total_amount')->default(0);
            $table->string('email')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->timestamp('placed_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'order_number']);
            $table->unique('checkout_id');
            $table->index('store_id');
            $table->index('customer_id');
            $table->index(['store_id', 'status']);
            $table->index(['store_id', 'financial_status']);
            $table->index(['store_id', 'fulfillment_status']);
            $table->index(['store_id', 'placed_at']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('orders');
    }
};


command: sed -n '1,260p' tests/Feature/Api/AdminOrderApiTest.php

aggregated_output:
<?php

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\PaymentStatus;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    $this->seed(DatabaseSeeder::class);
});

function adminOrderApiStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function adminOrderApiUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminOrderApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Order integration', $abilities);
}

/**
 * @return array{0: Order, 1: OrderLine, 2: ProductVariant}
 */
function adminOrderApiOrder(Store $store, array $orderAttributes = [], int $quantity = 2, int $unitPrice = 2500): array
{
    $product = Product::factory()
        ->withDefaultVariant($unitPrice)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 10,
            'quantity_reserved' => 0,
        ]);

    $total = $quantity * $unitPrice;
    $order = Order::factory()->paid()->create(array_merge([
        'store_id' => $store->getKey(),
        'subtotal_amount' => $total,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $total,
        'email' => 'buyer@example.test',
    ], $orderAttributes));
    $line = OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Admin API Product',
        'sku_snapshot' => 'ADMIN-API-001',
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

    Payment::factory()->create([
        'order_id' => $order->getKey(),
        'status' => $order->financial_status === FinancialStatus::Pending ? PaymentStatus::Pending : PaymentStatus::Captured,
        'amount' => $total,
        'currency' => $order->currency,
    ]);

    return [$order, $line, $variant];
}

test('admin order api lists and shows store scoped orders', function (): void {
    $store = adminOrderApiStore();
    [$order] = adminOrderApiOrder($store, [
        'order_number' => '#8001',
        'email' => 'alpha@example.test',
    ]);
    $otherStore = Store::factory()->create();
    adminOrderApiOrder($otherStore, [
        'order_number' => '#9001',
        'email' => 'other@example.test',
    ]);

    $this->getJson("/api/admin/v1/stores/{$store->getKey()}/orders")
        ->assertUnauthorized();

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders?query=alpha")
        ->assertOk()
        ->assertJsonPath('data.0.order_number', '#8001')
        ->assertJsonMissing(['order_number' => '#9001']);

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}")
        ->assertOk()
        ->assertJsonPath('data.order_number', '#8001')
        ->assertJsonPath('data.lines.0.title', 'Admin API Product');
});

test('admin order api creates refunds and fulfillments', function (): void {
    $store = adminOrderApiStore();
    [$order, $line] = adminOrderApiOrder($store, quantity: 2, unitPrice: 2500);
    $user = adminOrderApiUser();

    $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/refunds", [
            'amount' => 1000,
            'reason' => 'Customer return',
        ])
        ->assertCreated()
        ->assertJsonPath('data.amount', 1000)
        ->assertJsonPath('data.status', 'processed');

    expect($order->refresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded);

    $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/fulfillments", [
            'line_items' => [
                ['order_line_id' => $line->getKey(), 'quantity' => 1],
            ],
            'tracking_company' => 'DHL',
            'tracking_number' => 'DHL123',
        ])
        ->assertCreated()
        ->assertJsonPath('data.status', 'pending')
        ->assertJsonPath('data.lines.0.quantity', 1);

    $fulfillment = Fulfillment::query()->where('order_id', $order->getKey())->firstOrFail();

    expect($order->refresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial)
        ->and($fulfillment->tracking_number)->toBe('DHL123');
});

test('admin order api rejects orders outside the requested store', function (): void {
    $store = adminOrderApiStore();
    $otherStore = Store::factory()->create();
    [$otherOrder] = adminOrderApiOrder($otherStore, [
        'order_number' => '#9001',
    ]);

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$otherOrder->getKey()}")
        ->assertNotFound();
});

test('admin order api accepts scoped bearer tokens', function (): void {
    $store = adminOrderApiStore();
    adminOrderApiOrder($store, [
        'order_number' => '#8101',
        'email' => 'token@example.test',
    ]);
    $result = adminOrderApiToken($store, ['read-orders']);

    $this->withToken($result['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders?query=token")
        ->assertOk()
        ->assertJsonPath('data.0.order_number', '#8101');

    expect($result['token']->refresh()->last_used_at)->not->toBeNull();
});

test('admin order api enforces token store scope and abilities', function (): void {
    $store = adminOrderApiStore();
    [$order] = adminOrderApiOrder($store);
    $readOnly = adminOrderApiToken($store, ['read-orders']);
    $writeToken = adminOrderApiToken($store, ['write-orders']);
    $otherStore = Store::factory()->create();
    $otherStoreToken = adminOrderApiToken($otherStore, ['read-orders']);

    $this->withToken($readOnly['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/refunds", [
            'amount' => 500,
            'reason' => 'Read-only token',
        ])
        ->assertForbidden();

    $this->withToken($otherStoreToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders")
        ->assertForbidden();

    $this->withToken($writeToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/refunds", [
            'amount' => 500,
            'reason' => 'Write token',
        ])
        ->assertCreated()
        ->assertJsonPath('data.amount', 500);
});

test('admin order api rejects expired bearer tokens', function (): void {
    $store = adminOrderApiStore();
    adminOrderApiOrder($store);
    $result = adminOrderApiToken($store, ['read-orders']);
    $result['token']->forceFill(['expires_at' => now()->subMinute()])->save();

    $this->withToken($result['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders")
        ->assertUnauthorized();
});


output:
Chunk ID: 129d8a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1936
Output:
<?php

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\PaymentStatus;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    $this->seed(DatabaseSeeder::class);
});

function adminOrderApiStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function adminOrderApiUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminOrderApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Order integration', $abilities);
}

/**
 * @return array{0: Order, 1: OrderLine, 2: ProductVariant}
 */
function adminOrderApiOrder(Store $store, array $orderAttributes = [], int $quantity = 2, int $unitPrice = 2500): array
{
    $product = Product::factory()
        ->withDefaultVariant($unitPrice)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 10,
            'quantity_reserved' => 0,
        ]);

    $total = $quantity * $unitPrice;
    $order = Order::factory()->paid()->create(array_merge([
        'store_id' => $store->getKey(),
        'subtotal_amount' => $total,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $total,
        'email' => 'buyer@example.test',
    ], $orderAttributes));
    $line = OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Admin API Product',
        'sku_snapshot' => 'ADMIN-API-001',
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

    Payment::factory()->create([
        'order_id' => $order->getKey(),
        'status' => $order->financial_status === FinancialStatus::Pending ? PaymentStatus::Pending : PaymentStatus::Captured,
        'amount' => $total,
        'currency' => $order->currency,
    ]);

    return [$order, $line, $variant];
}

test('admin order api lists and shows store scoped orders', function (): void {
    $store = adminOrderApiStore();
    [$order] = adminOrderApiOrder($store, [
        'order_number' => '#8001',
        'email' => 'alpha@example.test',
    ]);
    $otherStore = Store::factory()->create();
    adminOrderApiOrder($otherStore, [
        'order_number' => '#9001',
        'email' => 'other@example.test',
    ]);

    $this->getJson("/api/admin/v1/stores/{$store->getKey()}/orders")
        ->assertUnauthorized();

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders?query=alpha")
        ->assertOk()
        ->assertJsonPath('data.0.order_number', '#8001')
        ->assertJsonMissing(['order_number' => '#9001']);

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}")
        ->assertOk()
        ->assertJsonPath('data.order_number', '#8001')
        ->assertJsonPath('data.lines.0.title', 'Admin API Product');
});

test('admin order api creates refunds and fulfillments', function (): void {
    $store = adminOrderApiStore();
    [$order, $line] = adminOrderApiOrder($store, quantity: 2, unitPrice: 2500);
    $user = adminOrderApiUser();

    $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/refunds", [
            'amount' => 1000,
            'reason' => 'Customer return',
        ])
        ->assertCreated()
        ->assertJsonPath('data.amount', 1000)
        ->assertJsonPath('data.status', 'processed');

    expect($order->refresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded);

    $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/fulfillments", [
            'line_items' => [
                ['order_line_id' => $line->getKey(), 'quantity' => 1],
            ],
            'tracking_company' => 'DHL',
            'tracking_number' => 'DHL123',
        ])
        ->assertCreated()
        ->assertJsonPath('data.status', 'pending')
        ->assertJsonPath('data.lines.0.quantity', 1);

    $fulfillment = Fulfillment::query()->where('order_id', $order->getKey())->firstOrFail();

    expect($order->refresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial)
        ->and($fulfillment->tracking_number)->toBe('DHL123');
});

test('admin order api rejects orders outside the requested store', function (): void {
    $store = adminOrderApiStore();
    $otherStore = Store::factory()->create();
    [$otherOrder] = adminOrderApiOrder($otherStore, [
        'order_number' => '#9001',
    ]);

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$otherOrder->getKey()}")
        ->assertNotFound();
});

test('admin order api accepts scoped bearer tokens', function (): void {
    $store = adminOrderApiStore();
    adminOrderApiOrder($store, [
        'order_number' => '#8101',
        'email' => 'token@example.test',
    ]);
    $result = adminOrderApiToken($store, ['read-orders']);

    $this->withToken($result['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders?query=token")
        ->assertOk()
        ->assertJsonPath('data.0.order_number', '#8101');

    expect($result['token']->refresh()->last_used_at)->not->toBeNull();
});

test('admin order api enforces token store scope and abilities', function (): void {
    $store = adminOrderApiStore();
    [$order] = adminOrderApiOrder($store);
    $readOnly = adminOrderApiToken($store, ['read-orders']);
    $writeToken = adminOrderApiToken($store, ['write-orders']);
    $otherStore = Store::factory()->create();
    $otherStoreToken = adminOrderApiToken($otherStore, ['read-orders']);

    $this->withToken($readOnly['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/refunds", [
            'amount' => 500,
            'reason' => 'Read-only token',
        ])
        ->assertForbidden();

    $this->withToken($otherStoreToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders")
        ->assertForbidden();

    $this->withToken($writeToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/refunds", [
            'amount' => 500,
            'reason' => 'Write token',
        ])
        ->assertCreated()
        ->assertJsonPath('data.amount', 500);
});

test('admin order api rejects expired bearer tokens', function (): void {
    $store = adminOrderApiStore();
    adminOrderApiOrder($store);
    $result = adminOrderApiToken($store, ['read-orders']);
    $result['token']->forceFill(['expires_at' => now()->subMinute()])->save();

    $this->withToken($result['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders")
        ->assertUnauthorized();
});


command: rg -n "enum .*Status|case Completed|ExportStatus" app/Enums app/Models database/factories

type: search

cmd: rg -n 'enum .*Status|case Completed|ExportStatus' app/Enums app/Models database/factories

query: enum .*Status|case Completed|ExportStatus

path: Enums

aggregated_output:
app/Enums/PaymentStatus.php:5:enum PaymentStatus: string
app/Enums/ThemeStatus.php:5:enum ThemeStatus: string
app/Enums/WebhookDeliveryStatus.php:5:enum WebhookDeliveryStatus: string
app/Enums/StoreStatus.php:5:enum StoreStatus: string
app/Enums/FulfillmentStatus.php:5:enum FulfillmentStatus: string
app/Enums/AppInstallationStatus.php:5:enum AppInstallationStatus: string
app/Enums/ProductStatus.php:5:enum ProductStatus: string
app/Enums/CheckoutStatus.php:5:enum CheckoutStatus: string
app/Enums/CheckoutStatus.php:11:    case Completed = 'completed';
app/Enums/FinancialStatus.php:5:enum FinancialStatus: string
app/Enums/AppStatus.php:5:enum AppStatus: string
app/Enums/MediaStatus.php:5:enum MediaStatus: string
app/Enums/CollectionStatus.php:5:enum CollectionStatus: string
app/Enums/PageStatus.php:5:enum PageStatus: string
app/Enums/VariantStatus.php:5:enum VariantStatus: string
app/Enums/DiscountStatus.php:5:enum DiscountStatus: string
app/Enums/OrderStatus.php:5:enum OrderStatus: string
app/Enums/RefundStatus.php:5:enum RefundStatus: string
app/Enums/CartStatus.php:5:enum CartStatus: string
app/Enums/FulfillmentShipmentStatus.php:5:enum FulfillmentShipmentStatus: string
app/Enums/WebhookSubscriptionStatus.php:5:enum WebhookSubscriptionStatus: string


output:
Chunk ID: e26493
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 318
Output:
app/Enums/PaymentStatus.php:5:enum PaymentStatus: string
app/Enums/ThemeStatus.php:5:enum ThemeStatus: string
app/Enums/WebhookDeliveryStatus.php:5:enum WebhookDeliveryStatus: string
app/Enums/StoreStatus.php:5:enum StoreStatus: string
app/Enums/FulfillmentStatus.php:5:enum FulfillmentStatus: string
app/Enums/AppInstallationStatus.php:5:enum AppInstallationStatus: string
app/Enums/ProductStatus.php:5:enum ProductStatus: string
app/Enums/CheckoutStatus.php:5:enum CheckoutStatus: string
app/Enums/CheckoutStatus.php:11:    case Completed = 'completed';
app/Enums/FinancialStatus.php:5:enum FinancialStatus: string
app/Enums/AppStatus.php:5:enum AppStatus: string
app/Enums/MediaStatus.php:5:enum MediaStatus: string
app/Enums/CollectionStatus.php:5:enum CollectionStatus: string
app/Enums/PageStatus.php:5:enum PageStatus: string
app/Enums/VariantStatus.php:5:enum VariantStatus: string
app/Enums/DiscountStatus.php:5:enum DiscountStatus: string
app/Enums/OrderStatus.php:5:enum OrderStatus: string
app/Enums/RefundStatus.php:5:enum RefundStatus: string
app/Enums/CartStatus.php:5:enum CartStatus: string
app/Enums/FulfillmentShipmentStatus.php:5:enum FulfillmentShipmentStatus: string
app/Enums/WebhookSubscriptionStatus.php:5:enum WebhookSubscriptionStatus: string


message: The spec’s export endpoint expects async status tracking, but the schema has no export table yet. I’m going to add a small durable `data_exports` table/model and generate CSV synchronously into local storage for this self-contained app, returning `202` with a completed export row and a data URL from the status endpoint.

phase: commentary


type: message

role: assistant

text: The spec’s export endpoint expects async status tracking, but the schema has no export table yet. I’m going to add a small durable `data_exports` table/model and generate CSV synchronously into local storage for this self-contained app, returning `202` with a completed export row and a data URL from the status endpoint.


cmd: php artisan make:enum ExportStatus --no-interaction


cmd: php artisan make:model DataExport --migration --factory --no-interaction


cmd: php artisan make:class Services/OrderExportService --no-interaction


cmd: php artisan make:controller Api/Admin/V1/OrderExportController --no-interaction


cmd: php artisan make:resource Admin/V1/DataExportResource --no-interaction