diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 101f3c3e..cbd22839 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,17 +1,17 @@ { + "permissions": { + "allow": [ + "Bash(git add:*)", + "Bash(git commit:*)" + ] + }, "enableAllProjectMcpServers": true, "enabledMcpjsonServers": [ "laravel-boost", "herd" ], "sandbox": { - "enabled": true, - "autoAllowBashIfSandboxed": true - }, - "permissions": { - "allow": [ - "Bash(git add:*)", - "Bash(git commit:*)" - ] + "enabled": false, + "autoAllowBashIfSandboxed": false } } diff --git a/.claude/skills/developing-with-fortify/SKILL.md b/.claude/skills/developing-with-fortify/SKILL.md new file mode 100644 index 00000000..2ff71a4b --- /dev/null +++ b/.claude/skills/developing-with-fortify/SKILL.md @@ -0,0 +1,116 @@ +--- +name: developing-with-fortify +description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications. +--- + +# Laravel Fortify Development + +Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications. + +## Documentation + +Use `search-docs` for detailed Laravel Fortify patterns and documentation. + +## Usage + +- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints +- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.) +- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field +- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.) +- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc. + +## Available Features + +Enable in `config/fortify.php` features array: + +- `Features::registration()` - User registration +- `Features::resetPasswords()` - Password reset via email +- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail` +- `Features::updateProfileInformation()` - Profile updates +- `Features::updatePasswords()` - Password changes +- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes + +> Use `search-docs` for feature configuration options and customization patterns. + +## Setup Workflows + +### Two-Factor Authentication Setup + +``` +- [ ] Add TwoFactorAuthenticatable trait to User model +- [ ] Enable feature in config/fortify.php +- [ ] Run migrations for 2FA columns +- [ ] Set up view callbacks in FortifyServiceProvider +- [ ] Create 2FA management UI +- [ ] Test QR code and recovery codes +``` + +> Use `search-docs` for TOTP implementation and recovery code handling patterns. + +### Email Verification Setup + +``` +- [ ] Enable emailVerification feature in config +- [ ] Implement MustVerifyEmail interface on User model +- [ ] Set up verifyEmailView callback +- [ ] Add verified middleware to protected routes +- [ ] Test verification email flow +``` + +> Use `search-docs` for MustVerifyEmail implementation patterns. + +### Password Reset Setup + +``` +- [ ] Enable resetPasswords feature in config +- [ ] Set up requestPasswordResetLinkView callback +- [ ] Set up resetPasswordView callback +- [ ] Define password.reset named route (if views disabled) +- [ ] Test reset email and link flow +``` + +> Use `search-docs` for custom password reset flow patterns. + +### SPA Authentication Setup + +``` +- [ ] Set 'views' => false in config/fortify.php +- [ ] Install and configure Laravel Sanctum +- [ ] Use 'web' guard in fortify config +- [ ] Set up CSRF token handling +- [ ] Test XHR authentication flows +``` + +> Use `search-docs` for integration and SPA authentication patterns. + +## Best Practices + +### Custom Authentication Logic + +Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects. + +### Registration Customization + +Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields. + +### Rate Limiting + +Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination. + +## Key Endpoints + +| Feature | Method | Endpoint | +|------------------------|----------|---------------------------------------------| +| Login | POST | `/login` | +| Logout | POST | `/logout` | +| Register | POST | `/register` | +| Password Reset Request | POST | `/forgot-password` | +| Password Reset | POST | `/reset-password` | +| Email Verify Notice | GET | `/email/verify` | +| Resend Verification | POST | `/email/verification-notification` | +| Password Confirm | POST | `/user/confirm-password` | +| Enable 2FA | POST | `/user/two-factor-authentication` | +| Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` | +| 2FA Challenge | POST | `/two-factor-challenge` | +| Get QR Code | GET | `/user/two-factor-qr-code` | +| Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` | \ No newline at end of file diff --git a/.claude/skills/fluxui-development/SKILL.md b/.claude/skills/fluxui-development/SKILL.md new file mode 100644 index 00000000..4b5aabb1 --- /dev/null +++ b/.claude/skills/fluxui-development/SKILL.md @@ -0,0 +1,81 @@ +--- +name: fluxui-development +description: "Use this skill for Flux UI development in Livewire applications only. Trigger when working with components, building or customizing Livewire component UIs, creating forms, modals, tables, or other interactive elements. Covers: flux: components (buttons, inputs, modals, forms, tables, date-pickers, kanban, badges, tooltips, etc.), component composition, Tailwind CSS styling, Heroicons/Lucide icon integration, validation patterns, responsive design, and theming. Do not use for non-Livewire frameworks or non-component styling." +license: MIT +metadata: + author: laravel +--- + +# Flux UI Development + +## Documentation + +Use `search-docs` for detailed Flux UI patterns and documentation. + +## Basic Usage + +This project uses the free edition of Flux UI, which includes all free components and variants but not Pro components. + +Flux UI is a component library for Livewire built with Tailwind CSS. It provides components that are easy to use and customize. + +Use Flux UI components when available. Fall back to standard Blade components when no Flux component exists for your needs. + + +```blade +Click me +``` + +## Available Components (Free Edition) + +Available: avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, profile, radio, select, separator, skeleton, switch, text, textarea, tooltip + +## Icons + +Flux includes [Heroicons](https://heroicons.com/) as its default icon set. Search for exact icon names on the Heroicons site - do not guess or invent icon names. + + +```blade +Export +``` + +For icons not available in Heroicons, use [Lucide](https://lucide.dev/). Import the icons you need with the Artisan command: + +```bash +php artisan flux:icon crown grip-vertical github +``` + +## Common Patterns + +### Form Fields + + +```blade + + Email + + + +``` + +### Modals + + +```blade + + Title +

Content

+
+``` + +## Verification + +1. Check component renders correctly +2. Test interactive states +3. Verify mobile responsiveness + +## Common Pitfalls + +- Trying to use Pro-only components in the free edition +- Not checking if a Flux component exists before creating custom implementations +- Forgetting to use the `search-docs` tool for component-specific documentation +- Not following existing project patterns for Flux usage \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/SKILL.md b/.claude/skills/laravel-best-practices/SKILL.md new file mode 100644 index 00000000..aca32c9c --- /dev/null +++ b/.claude/skills/laravel-best-practices/SKILL.md @@ -0,0 +1,190 @@ +--- +name: laravel-best-practices +description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns." +license: MIT +metadata: + author: laravel +--- + +# Laravel Best Practices + +Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`. + +## Consistency First + +Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern. + +Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides. + +## Quick Reference + +### 1. Database Performance → `rules/db-performance.md` + +- Eager load with `with()` to prevent N+1 queries +- Enable `Model::preventLazyLoading()` in development +- Select only needed columns, avoid `SELECT *` +- `chunk()` / `chunkById()` for large datasets +- Index columns used in `WHERE`, `ORDER BY`, `JOIN` +- `withCount()` instead of loading relations to count +- `cursor()` for memory-efficient read-only iteration +- Never query in Blade templates + +### 2. Advanced Query Patterns → `rules/advanced-queries.md` + +- `addSelect()` subqueries over eager-loading entire has-many for a single value +- Dynamic relationships via subquery FK + `belongsTo` +- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries +- `setRelation()` to prevent circular N+1 queries +- `whereIn` + `pluck()` over `whereHas` for better index usage +- Two simple queries can beat one complex query +- Compound indexes matching `orderBy` column order +- Correlated subqueries in `orderBy` for has-many sorting (avoid joins) + +### 3. Security → `rules/security.md` + +- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates +- No raw SQL with user input — use Eloquent or query builder +- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes +- Validate MIME type, extension, and size for file uploads +- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields + +### 4. Caching → `rules/caching.md` + +- `Cache::remember()` over manual get/put +- `Cache::flexible()` for stale-while-revalidate on high-traffic data +- `Cache::memo()` to avoid redundant cache hits within a request +- Cache tags to invalidate related groups +- `Cache::add()` for atomic conditional writes +- `once()` to memoize per-request or per-object lifetime +- `Cache::lock()` / `lockForUpdate()` for race conditions +- Failover cache stores in production + +### 5. Eloquent Patterns → `rules/eloquent.md` + +- Correct relationship types with return type hints +- Local scopes for reusable query constraints +- Global scopes sparingly — document their existence +- Attribute casts in the `casts()` method +- Cast date columns, use Carbon instances in templates +- `whereBelongsTo($model)` for cleaner queries +- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries + +### 6. Validation & Forms → `rules/validation.md` + +- Form Request classes, not inline validation +- Array notation `['required', 'email']` for new code; follow existing convention +- `$request->validated()` only — never `$request->all()` +- `Rule::when()` for conditional validation +- `after()` instead of `withValidator()` + +### 7. Configuration → `rules/config.md` + +- `env()` only inside config files +- `App::environment()` or `app()->isProduction()` +- Config, lang files, and constants over hardcoded text + +### 8. Testing Patterns → `rules/testing.md` + +- `LazilyRefreshDatabase` over `RefreshDatabase` for speed +- `assertModelExists()` over raw `assertDatabaseHas()` +- Factory states and sequences over manual overrides +- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before +- `recycle()` to share relationship instances across factories + +### 9. Queue & Job Patterns → `rules/queue-jobs.md` + +- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]` +- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release +- Always implement `failed()`; with `retryUntil()`, set `$tries = 0` +- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs +- Horizon for complex multi-queue scenarios + +### 10. Routing & Controllers → `rules/routing.md` + +- Implicit route model binding +- Scoped bindings for nested resources +- `Route::resource()` or `apiResource()` +- Methods under 10 lines — extract to actions/services +- Type-hint Form Requests for auto-validation + +### 11. HTTP Client → `rules/http-client.md` + +- Explicit `timeout` and `connectTimeout` on every request +- `retry()` with exponential backoff for external APIs +- Check response status or use `throw()` +- `Http::pool()` for concurrent independent requests +- `Http::fake()` and `preventStrayRequests()` in tests + +### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md` + +- Event discovery over manual registration; `event:cache` in production +- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions +- Queue notifications and mailables with `ShouldQueue` +- On-demand notifications for non-user recipients +- `HasLocalePreference` on notifiable models +- `assertQueued()` not `assertSent()` for queued mailables +- Markdown mailables for transactional emails + +### 13. Error Handling → `rules/error-handling.md` + +- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern +- `ShouldntReport` for exceptions that should never log +- Throttle high-volume exceptions to protect log sinks +- `dontReportDuplicates()` for multi-catch scenarios +- Force JSON rendering for API routes +- Structured context via `context()` on exception classes + +### 14. Task Scheduling → `rules/scheduling.md` + +- `withoutOverlapping()` on variable-duration tasks +- `onOneServer()` on multi-server deployments +- `runInBackground()` for concurrent long tasks +- `environments()` to restrict to appropriate environments +- `takeUntilTimeout()` for time-bounded processing +- Schedule groups for shared configuration + +### 15. Architecture → `rules/architecture.md` + +- Single-purpose Action classes; dependency injection over `app()` helper +- Prefer official Laravel packages and follow conventions, don't override defaults +- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety +- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution + +### 16. Migrations → `rules/migrations.md` + +- Generate migrations with `php artisan make:migration` +- `constrained()` for foreign keys +- Never modify migrations that have run in production +- Add indexes in the migration, not as an afterthought +- Mirror column defaults in model `$attributes` +- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes +- One concern per migration — never mix DDL and DML + +### 17. Collections → `rules/collections.md` + +- Higher-order messages for simple collection operations +- `cursor()` vs. `lazy()` — choose based on relationship needs +- `lazyById()` when updating records while iterating +- `toQuery()` for bulk operations on collections + +### 18. Blade & Views → `rules/blade-views.md` + +- `$attributes->merge()` in component templates +- Blade components over `@include`; `@pushOnce` for per-component scripts +- View Composers for shared view data +- `@aware` for deeply nested component props + +### 19. Conventions & Style → `rules/style.md` + +- Follow Laravel naming conventions for all entities +- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions +- No JS/CSS in Blade, no HTML in PHP classes +- Code should be readable; comments only for config files + +## How to Apply + +Always use a sub-agent to read rule files and explore this skill's content. + +1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10) +2. Check sibling files for existing patterns — follow those first per Consistency First +3. Verify API syntax with `search-docs` for the installed Laravel version \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/advanced-queries.md b/.claude/skills/laravel-best-practices/rules/advanced-queries.md new file mode 100644 index 00000000..920714a1 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/advanced-queries.md @@ -0,0 +1,106 @@ +# Advanced Query Patterns + +## Use `addSelect()` Subqueries for Single Values from Has-Many + +Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries. + +```php +public function scopeWithLastLoginAt($query): void +{ + $query->addSelect([ + 'last_login_at' => Login::select('created_at') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1), + ])->withCasts(['last_login_at' => 'datetime']); +} +``` + +## Create Dynamic Relationships via Subquery FK + +Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection. + +```php +public function lastLogin(): BelongsTo +{ + return $this->belongsTo(Login::class); +} + +public function scopeWithLastLogin($query): void +{ + $query->addSelect([ + 'last_login_id' => Login::select('id') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1), + ])->with('lastLogin'); +} +``` + +## Use Conditional Aggregates Instead of Multiple Count Queries + +Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values. + +```php +$statuses = Feature::toBase() + ->selectRaw("count(case when status = 'Requested' then 1 end) as requested") + ->selectRaw("count(case when status = 'Planned' then 1 end) as planned") + ->selectRaw("count(case when status = 'Completed' then 1 end) as completed") + ->first(); +``` + +## Use `setRelation()` to Prevent Circular N+1 + +When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries. + +```php +$feature->load('comments.user'); +$feature->comments->each->setRelation('feature', $feature); +``` + +## Prefer `whereIn` + Subquery Over `whereHas` + +`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory. + +Incorrect (correlated EXISTS re-executes per row): + +```php +$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); +``` + +Correct (index-friendly subquery, no PHP memory overhead): + +```php +$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); +``` + +## Sometimes Two Simple Queries Beat One Complex Query + +Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index. + +## Use Compound Indexes Matching `orderBy` Column Order + +When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index. + +```php +// Migration +$table->index(['last_name', 'first_name']); + +// Query — column order must match the index +User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); +``` + +## Use Correlated Subqueries for Has-Many Ordering + +When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading. + +```php +public function scopeOrderByLastLogin($query): void +{ + $query->orderByDesc(Login::select('created_at') + ->whereColumn('user_id', 'users.id') + ->latest() + ->take(1) + ); +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/architecture.md b/.claude/skills/laravel-best-practices/rules/architecture.md new file mode 100644 index 00000000..6112a635 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/architecture.md @@ -0,0 +1,202 @@ +# Architecture Best Practices + +## Single-Purpose Action Classes + +Extract discrete business operations into invokable Action classes. + +```php +class CreateOrderAction +{ + public function __construct(private InventoryService $inventory) {} + + public function execute(array $data): Order + { + $order = Order::create($data); + $this->inventory->reserve($order); + + return $order; + } +} +``` + +## Use Dependency Injection + +Always use constructor injection. Avoid `app()` or `resolve()` inside classes. + +Incorrect: +```php +class OrderController extends Controller +{ + public function store(StoreOrderRequest $request) + { + $service = app(OrderService::class); + + return $service->create($request->validated()); + } +} +``` + +Correct: +```php +class OrderController extends Controller +{ + public function __construct(private OrderService $service) {} + + public function store(StoreOrderRequest $request) + { + return $this->service->create($request->validated()); + } +} +``` + +## Code to Interfaces + +Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. + +Incorrect (concrete dependency): +```php +class OrderService +{ + public function __construct(private StripeGateway $gateway) {} +} +``` + +Correct (interface dependency): +```php +interface PaymentGateway +{ + public function charge(int $amount, string $customerId): PaymentResult; +} + +class OrderService +{ + public function __construct(private PaymentGateway $gateway) {} +} +``` + +Bind in a service provider: + +```php +$this->app->bind(PaymentGateway::class, StripeGateway::class); +``` + +## Default Sort by Descending + +When no explicit order is specified, sort by `id` or `created_at` descending. Without an explicit `ORDER BY`, row order is undefined. + +Incorrect: +```php +$posts = Post::paginate(); +``` + +Correct: +```php +$posts = Post::latest()->paginate(); +``` + +## Use Atomic Locks for Race Conditions + +Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. + +```php +Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) { + $order->process(); +}); + +// Or at query level +$product = Product::where('id', $id)->lockForUpdate()->first(); +``` + +## Use `mb_*` String Functions + +When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters. + +Incorrect: +```php +strlen('José'); // 5 (bytes, not characters) +strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte +``` + +Correct: +```php +mb_strlen('José'); // 4 (characters) +mb_strtolower('MÜNCHEN'); // 'münchen' + +// Prefer Laravel's Str helpers when available +Str::length('José'); // 4 +Str::lower('MÜNCHEN'); // 'münchen' +``` + +## Use `defer()` for Post-Response Work + +For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead. + +Incorrect (job overhead for trivial work): +```php +dispatch(new LogPageView($page)); +``` + +Correct (runs after response, same process): +```php +defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()])); +``` + +Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work. + +## Use `Context` for Request-Scoped Data + +The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. + +```php +// In middleware +Context::add('tenant_id', $request->header('X-Tenant-ID')); + +// Anywhere later — controllers, jobs, log context +$tenantId = Context::get('tenant_id'); +``` + +Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`. + +## Use `Concurrency::run()` for Parallel Execution + +Run independent operations in parallel using child processes — no async libraries needed. + +```php +use Illuminate\Support\Facades\Concurrency; + +[$users, $orders] = Concurrency::run([ + fn () => User::count(), + fn () => Order::where('status', 'pending')->count(), +]); +``` + +Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially. + +## Convention Over Configuration + +Follow Laravel conventions. Don't override defaults unnecessarily. + +Incorrect: +```php +class Customer extends Model +{ + protected $table = 'Customer'; + protected $primaryKey = 'customer_id'; + + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id'); + } +} +``` + +Correct: +```php +class Customer extends Model +{ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/blade-views.md b/.claude/skills/laravel-best-practices/rules/blade-views.md new file mode 100644 index 00000000..c6f8aaf1 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/blade-views.md @@ -0,0 +1,36 @@ +# Blade & Views Best Practices + +## Use `$attributes->merge()` in Component Templates + +Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. + +```blade +
merge(['class' => 'alert alert-'.$type]) }}> + {{ $message }} +
+``` + +## Use `@pushOnce` for Per-Component Scripts + +If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once. + +## Prefer Blade Components Over `@include` + +`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. + +## Use View Composers for Shared View Data + +If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. + +## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) + +A single view can return either the full page or just a fragment, keeping routing clean. + +```php +return view('dashboard', compact('users')) + ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); +``` + +## Use `@aware` for Deeply Nested Component Props + +Avoids re-passing parent props through every level of nested components. \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/caching.md b/.claude/skills/laravel-best-practices/rules/caching.md new file mode 100644 index 00000000..e65146dc --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/caching.md @@ -0,0 +1,70 @@ +# Caching Best Practices + +## Use `Cache::remember()` Instead of Manual Get/Put + +Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions. + +Incorrect: +```php +$val = Cache::get('stats'); +if (! $val) { + $val = $this->computeStats(); + Cache::put('stats', $val, 60); +} +``` + +Correct: +```php +$val = Cache::remember('stats', 60, fn () => $this->computeStats()); +``` + +## Use `Cache::flexible()` for Stale-While-Revalidate + +On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background. + +Incorrect: `Cache::remember('users', 300, fn () => User::all());` + +Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function. + +## Use `Cache::memo()` to Avoid Redundant Hits Within a Request + +If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory. + +`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. + +## Use Cache Tags to Invalidate Related Groups + +Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`. + +```php +Cache::tags(['user-1'])->flush(); +``` + +## Use `Cache::add()` for Atomic Conditional Writes + +`add()` only writes if the key does not exist — atomic, no race condition between checking and writing. + +Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` + +Correct: `Cache::add('lock', true, 10);` + +## Use `once()` for Per-Request Memoization + +`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory. + +```php +public function roles(): Collection +{ + return once(fn () => $this->loadRoles()); +} +``` + +Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching. + +## Configure Failover Cache Stores in Production + +If Redis goes down, the app falls back to a secondary store automatically. + +```php +'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/collections.md b/.claude/skills/laravel-best-practices/rules/collections.md new file mode 100644 index 00000000..14f683d3 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/collections.md @@ -0,0 +1,44 @@ +# Collection Best Practices + +## Use Higher-Order Messages for Simple Operations + +Incorrect: +```php +$users->each(function (User $user) { + $user->markAsVip(); +}); +``` + +Correct: `$users->each->markAsVip();` + +Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. + +## Choose `cursor()` vs. `lazy()` Correctly + +- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). +- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. + +Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. + +Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. + +## Use `lazyById()` When Updating Records While Iterating + +`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation. + +## Use `toQuery()` for Bulk Operations on Collections + +Avoids manual `whereIn` construction. + +Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` + +Correct: `$users->toQuery()->update([...]);` + +## Use `#[CollectedBy]` for Custom Collection Classes + +More declarative than overriding `newCollection()`. + +```php +#[CollectedBy(UserCollection::class)] +class User extends Model {} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/config.md b/.claude/skills/laravel-best-practices/rules/config.md new file mode 100644 index 00000000..193155d6 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/config.md @@ -0,0 +1,73 @@ +# Configuration Best Practices + +## `env()` Only in Config Files + +Direct `env()` calls may return `null` when config is cached. + +Incorrect: +```php +$key = env('API_KEY'); +``` + +Correct: +```php +// config/services.php +'key' => env('API_KEY'), + +// Application code +$key = config('services.key'); +``` + +## Use Encrypted Env or External Secrets + +Never store production secrets in plain `.env` files in version control. + +Incorrect: +```bash + +# .env committed to repo or shared in Slack + +STRIPE_SECRET=sk_live_abc123 +AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI +``` + +Correct: +```bash +php artisan env:encrypt --env=production --readable +php artisan env:decrypt --env=production +``` + +For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime. + +## Use `App::environment()` for Environment Checks + +Incorrect: +```php +if (env('APP_ENV') === 'production') { +``` + +Correct: +```php +if (app()->isProduction()) { +// or +if (App::environment('production')) { +``` + +## Use Constants and Language Files + +Use class constants instead of hardcoded magic strings for model states, types, and statuses. + +```php +// Incorrect +return $this->type === 'normal'; + +// Correct +return $this->type === self::TYPE_NORMAL; +``` + +If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there. + +```php +// Only when lang files already exist in the project +return back()->with('message', __('app.article_added')); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/db-performance.md b/.claude/skills/laravel-best-practices/rules/db-performance.md new file mode 100644 index 00000000..8fb71937 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/db-performance.md @@ -0,0 +1,192 @@ +# Database Performance Best Practices + +## Always Eager Load Relationships + +Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. + +Incorrect (N+1 — executes 1 + N queries): +```php +$posts = Post::all(); +foreach ($posts as $post) { + echo $post->author->name; +} +``` + +Correct (2 queries total): +```php +$posts = Post::with('author')->get(); +foreach ($posts as $post) { + echo $post->author->name; +} +``` + +Constrain eager loads to select only needed columns (always include the foreign key): + +```php +$users = User::with(['posts' => function ($query) { + $query->select('id', 'user_id', 'title') + ->where('published', true) + ->latest() + ->limit(10); +}])->get(); +``` + +## Prevent Lazy Loading in Development + +Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development. + +```php +public function boot(): void +{ + Model::preventLazyLoading(! app()->isProduction()); +} +``` + +Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. + +## Select Only Needed Columns + +Avoid `SELECT *` — especially when tables have large text or JSON columns. + +Incorrect: +```php +$posts = Post::with('author')->get(); +``` + +Correct: +```php +$posts = Post::select('id', 'title', 'user_id', 'created_at') + ->with(['author:id,name,avatar']) + ->get(); +``` + +When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match. + +## Chunk Large Datasets + +Never load thousands of records at once. Use chunking for batch processing. + +Incorrect: +```php +$users = User::all(); +foreach ($users as $user) { + $user->notify(new WeeklyDigest); +} +``` + +Correct: +```php +User::where('subscribed', true)->chunk(200, function ($users) { + foreach ($users as $user) { + $user->notify(new WeeklyDigest); + } +}); +``` + +Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: + +```php +User::where('active', false)->chunkById(200, function ($users) { + $users->each->delete(); +}); +``` + +## Add Database Indexes + +Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. + +Incorrect: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); + $table->timestamps(); +}); +``` + +Correct: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->index()->constrained(); + $table->string('status')->index(); + $table->timestamps(); + $table->index(['status', 'created_at']); +}); +``` + +Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). + +## Use `withCount()` for Counting Relations + +Never load entire collections just to count them. + +Incorrect: +```php +$posts = Post::all(); +foreach ($posts as $post) { + echo $post->comments->count(); +} +``` + +Correct: +```php +$posts = Post::withCount('comments')->get(); +foreach ($posts as $post) { + echo $post->comments_count; +} +``` + +Conditional counting: + +```php +$posts = Post::withCount([ + 'comments', + 'comments as approved_comments_count' => function ($query) { + $query->where('approved', true); + }, +])->get(); +``` + +## Use `cursor()` for Memory-Efficient Iteration + +For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. + +Incorrect: +```php +$users = User::where('active', true)->get(); +``` + +Correct: +```php +foreach (User::where('active', true)->cursor() as $user) { + ProcessUser::dispatch($user->id); +} +``` + +Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records. + +## No Queries in Blade Templates + +Never execute queries in Blade templates. Pass data from controllers. + +Incorrect: +```blade +@foreach (User::all() as $user) + {{ $user->profile->name }} +@endforeach +``` + +Correct: +```php +// Controller +$users = User::with('profile')->get(); +return view('users.index', compact('users')); +``` + +```blade +@foreach ($users as $user) + {{ $user->profile->name }} +@endforeach +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/eloquent.md b/.claude/skills/laravel-best-practices/rules/eloquent.md new file mode 100644 index 00000000..09cd66a0 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/eloquent.md @@ -0,0 +1,148 @@ +# Eloquent Best Practices + +## Use Correct Relationship Types + +Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. + +```php +public function comments(): HasMany +{ + return $this->hasMany(Comment::class); +} + +public function author(): BelongsTo +{ + return $this->belongsTo(User::class, 'user_id'); +} +``` + +## Use Local Scopes for Reusable Queries + +Extract reusable query constraints into local scopes to avoid duplication. + +Incorrect: +```php +$active = User::where('verified', true)->whereNotNull('activated_at')->get(); +$articles = Article::whereHas('user', function ($q) { + $q->where('verified', true)->whereNotNull('activated_at'); +})->get(); +``` + +Correct: +```php +public function scopeActive(Builder $query): Builder +{ + return $query->where('verified', true)->whereNotNull('activated_at'); +} + +// Usage +$active = User::active()->get(); +$articles = Article::whereHas('user', fn ($q) => $q->active())->get(); +``` + +## Apply Global Scopes Sparingly + +Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy. + +Incorrect (global scope for a conditional filter): +```php +class PublishedScope implements Scope +{ + public function apply(Builder $builder, Model $model): void + { + $builder->where('published', true); + } +} +// Now admin panels, reports, and background jobs all silently skip drafts +``` + +Correct (local scope you opt into): +```php +public function scopePublished(Builder $query): Builder +{ + return $query->where('published', true); +} + +Post::published()->paginate(); // Explicit +Post::paginate(); // Admin sees all +``` + +## Define Attribute Casts + +Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion. + +```php +protected function casts(): array +{ + return [ + 'is_active' => 'boolean', + 'metadata' => 'array', + 'total' => 'decimal:2', + ]; +} +``` + +## Cast Date Columns Properly + +Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. + +Incorrect: +```blade +{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} +``` + +Correct: +```php +protected function casts(): array +{ + return [ + 'ordered_at' => 'datetime', + ]; +} +``` + +```blade +{{ $order->ordered_at->toDateString() }} +{{ $order->ordered_at->format('m-d') }} +``` + +## Use `whereBelongsTo()` for Relationship Queries + +Cleaner than manually specifying foreign keys. + +Incorrect: +```php +Post::where('user_id', $user->id)->get(); +``` + +Correct: +```php +Post::whereBelongsTo($user)->get(); +Post::whereBelongsTo($user, 'author')->get(); +``` + +## Avoid Hardcoded Table Names in Queries + +Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string). + +Incorrect: +```php +DB::table('users')->where('active', true)->get(); + +$query->join('companies', 'companies.id', '=', 'users.company_id'); + +DB::select('SELECT * FROM orders WHERE status = ?', ['pending']); +``` + +Correct — reference the model's table: +```php +DB::table((new User)->getTable())->where('active', true)->get(); + +// Even better — use Eloquent or the query builder instead of raw SQL +User::where('active', true)->get(); +Order::where('status', 'pending')->get(); +``` + +Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable. + +**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration. \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/error-handling.md b/.claude/skills/laravel-best-practices/rules/error-handling.md new file mode 100644 index 00000000..bb8e7a38 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/error-handling.md @@ -0,0 +1,72 @@ +# Error Handling Best Practices + +## Exception Reporting and Rendering + +There are two valid approaches — choose one and apply it consistently across the project. + +**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: + +```php +class InvalidOrderException extends Exception +{ + public function report(): void { /* custom reporting */ } + + public function render(Request $request): Response + { + return response()->view('errors.invalid-order', status: 422); + } +} +``` + +**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: + +```php +->withExceptions(function (Exceptions $exceptions) { + $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); + $exceptions->render(function (InvalidOrderException $e, Request $request) { + return response()->view('errors.invalid-order', status: 422); + }); +}) +``` + +Check the existing codebase and follow whichever pattern is already established. + +## Use `ShouldntReport` for Exceptions That Should Never Log + +More discoverable than listing classes in `dontReport()`. + +```php +class PodcastProcessingException extends Exception implements ShouldntReport {} +``` + +## Throttle High-Volume Exceptions + +A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. + +## Enable `dontReportDuplicates()` + +Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. + +## Force JSON Error Rendering for API Routes + +Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. + +```php +$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { + return $request->is('api/*') || $request->expectsJson(); +}); +``` + +## Add Context to Exception Classes + +Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry. + +```php +class InvalidOrderException extends Exception +{ + public function context(): array + { + return ['order_id' => $this->orderId]; + } +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/events-notifications.md b/.claude/skills/laravel-best-practices/rules/events-notifications.md new file mode 100644 index 00000000..47fcf324 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/events-notifications.md @@ -0,0 +1,52 @@ +# Events & Notifications Best Practices + +## Rely on Event Discovery + +Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. + +## Run `event:cache` in Production Deploy + +Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. + +## Use `ShouldDispatchAfterCommit` Inside Transactions + +Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. + +```php +class OrderShipped implements ShouldDispatchAfterCommit {} +``` + +## Always Queue Notifications + +Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. + +```php +class InvoicePaid extends Notification implements ShouldQueue +{ + use Queueable; +} +``` + +## Use `afterCommit()` on Notifications in Transactions + +Same race condition as events — call `afterCommit()` to delay dispatch until the transaction commits. + +```php +$user->notify((new InvoicePaid($invoice))->afterCommit()); +``` + +## Route Notification Channels to Dedicated Queues + +Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. + +## Use On-Demand Notifications for Non-User Recipients + +Avoid creating dummy models to send notifications to arbitrary addresses. + +```php +Notification::route('mail', 'admin@example.com')->notify(new SystemAlert()); +``` + +## Implement `HasLocalePreference` on Notifiable Models + +Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed. \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/http-client.md b/.claude/skills/laravel-best-practices/rules/http-client.md new file mode 100644 index 00000000..fd37ddb9 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/http-client.md @@ -0,0 +1,160 @@ +# HTTP Client Best Practices + +## Always Set Explicit Timeouts + +The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. + +Incorrect: +```php +$response = Http::get('https://api.example.com/users'); +``` + +Correct: +```php +$response = Http::timeout(5) + ->connectTimeout(3) + ->get('https://api.example.com/users'); +``` + +For service-specific clients, define timeouts in a macro: + +```php +Http::macro('github', function () { + return Http::baseUrl('https://api.github.com') + ->timeout(10) + ->connectTimeout(3) + ->withToken(config('services.github.token')); +}); + +$response = Http::github()->get('/repos/laravel/framework'); +``` + +## Use Retry with Backoff for External APIs + +External APIs have transient failures. Use `retry()` with increasing delays. + +Incorrect: +```php +$response = Http::post('https://api.stripe.com/v1/charges', $data); + +if ($response->failed()) { + throw new PaymentFailedException('Charge failed'); +} +``` + +Correct: +```php +$response = Http::retry([100, 500, 1000]) + ->timeout(10) + ->post('https://api.stripe.com/v1/charges', $data); +``` + +Only retry on specific errors: + +```php +$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException && $exception->response->serverError()); +})->post('https://api.example.com/data'); +``` + +## Handle Errors Explicitly + +The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. + +Incorrect: +```php +$response = Http::get('https://api.example.com/users/1'); +$user = $response->json(); // Could be an error body +``` + +Correct: +```php +$response = Http::timeout(5) + ->get('https://api.example.com/users/1') + ->throw(); + +$user = $response->json(); +``` + +For graceful degradation: + +```php +$response = Http::get('https://api.example.com/users/1'); + +if ($response->successful()) { + return $response->json(); +} + +if ($response->notFound()) { + return null; +} + +$response->throw(); +``` + +## Use Request Pooling for Concurrent Requests + +When making multiple independent API calls, use `Http::pool()` instead of sequential calls. + +Incorrect: +```php +$users = Http::get('https://api.example.com/users')->json(); +$posts = Http::get('https://api.example.com/posts')->json(); +$comments = Http::get('https://api.example.com/comments')->json(); +``` + +Correct: +```php +use Illuminate\Http\Client\Pool; + +$responses = Http::pool(fn (Pool $pool) => [ + $pool->as('users')->get('https://api.example.com/users'), + $pool->as('posts')->get('https://api.example.com/posts'), + $pool->as('comments')->get('https://api.example.com/comments'), +]); + +$users = $responses['users']->json(); +$posts = $responses['posts']->json(); +``` + +## Fake HTTP Calls in Tests + +Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. + +Incorrect: +```php +it('syncs user from API', function () { + $service = new UserSyncService; + $service->sync(1); // Hits the real API +}); +``` + +Correct: +```php +it('syncs user from API', function () { + Http::preventStrayRequests(); + + Http::fake([ + 'api.example.com/users/1' => Http::response([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]), + ]); + + $service = new UserSyncService; + $service->sync(1); + + Http::assertSent(function (Request $request) { + return $request->url() === 'https://api.example.com/users/1'; + }); +}); +``` + +Test failure scenarios too: + +```php +Http::fake([ + 'api.example.com/*' => Http::failedConnection(), +]); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/mail.md b/.claude/skills/laravel-best-practices/rules/mail.md new file mode 100644 index 00000000..2435d9cc --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/mail.md @@ -0,0 +1,27 @@ +# Mail Best Practices + +## Implement `ShouldQueue` on the Mailable Class + +Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it. + +## Use `afterCommit()` on Mailables Inside Transactions + +A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. + +## Use `assertQueued()` Not `assertSent()` for Queued Mailables + +`Mail::assertSent()` only catches synchronous mail. Queued mailables fail `assertSent` with a "Did you mean to use assertQueued()?" hint. + +Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. + +Correct: `Mail::assertQueued(OrderShipped::class);` + +## Use Markdown Mailables for Transactional Emails + +Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. + +## Separate Content Tests from Sending Tests + +Content tests: instantiate the mailable directly, call `assertSeeInHtml()`. +Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`. +Don't mix them — it conflates concerns and makes tests brittle. \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/migrations.md b/.claude/skills/laravel-best-practices/rules/migrations.md new file mode 100644 index 00000000..de25aa39 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/migrations.md @@ -0,0 +1,121 @@ +# Migration Best Practices + +## Generate Migrations with Artisan + +Always use `php artisan make:migration` for consistent naming and timestamps. + +Incorrect (manually created file): +```php +// database/migrations/posts_migration.php ← wrong naming, no timestamp +``` + +Correct (Artisan-generated): +```bash +php artisan make:migration create_posts_table +php artisan make:migration add_slug_to_posts_table +``` + +## Use `constrained()` for Foreign Keys + +Automatic naming and referential integrity. + +```php +$table->foreignId('user_id')->constrained()->cascadeOnDelete(); + +// Non-standard names +$table->foreignId('author_id')->constrained('users'); +``` + +## Never Modify Deployed Migrations + +Once a migration has run in production, treat it as immutable. Create a new migration to change the table. + +Incorrect (editing a deployed migration): +```php +// 2024_01_01_create_posts_table.php — already in production +$table->string('slug')->unique(); // ← added after deployment +``` + +Correct (new migration to alter): +```php +// 2024_03_15_add_slug_to_posts_table.php +Schema::table('posts', function (Blueprint $table) { + $table->string('slug')->unique()->after('title'); +}); +``` + +## Add Indexes in the Migration + +Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. + +Incorrect: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); + $table->timestamps(); +}); +``` + +Correct: +```php +Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained()->index(); + $table->string('status')->index(); + $table->timestamp('shipped_at')->nullable()->index(); + $table->timestamps(); +}); +``` + +## Mirror Defaults in Model `$attributes` + +When a column has a database default, mirror it in the model so new instances have correct values before saving. + +```php +// Migration +$table->string('status')->default('pending'); + +// Model +protected $attributes = [ + 'status' => 'pending', +]; +``` + +## Write Reversible `down()` Methods by Default + +Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments. + +```php +public function down(): void +{ + Schema::table('posts', function (Blueprint $table) { + $table->dropColumn('slug'); + }); +} +``` + +For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported. + +## Keep Migrations Focused + +One concern per migration. Never mix DDL (schema changes) and DML (data manipulation). + +Incorrect (partial failure creates unrecoverable state): +```php +public function up(): void +{ + Schema::create('settings', function (Blueprint $table) { ... }); + DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); +} +``` + +Correct (separate migrations): +```php +// Migration 1: create_settings_table +Schema::create('settings', function (Blueprint $table) { ... }); + +// Migration 2: seed_default_settings +DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/queue-jobs.md b/.claude/skills/laravel-best-practices/rules/queue-jobs.md new file mode 100644 index 00000000..f7aa548b --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/queue-jobs.md @@ -0,0 +1,144 @@ +# Queue & Job Best Practices + +## Set `retry_after` Greater Than `timeout` + +If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution. + +Incorrect (`retry_after` ≤ `timeout`): +```php +class ProcessReport implements ShouldQueue +{ + public $timeout = 120; +} + +// config/queue.php — retry_after: 90 ← job retried while still running! +``` + +Correct (`retry_after` > `timeout`): +```php +class ProcessReport implements ShouldQueue +{ + public $timeout = 120; +} + +// config/queue.php — retry_after: 180 ← safely longer than any job timeout +``` + +## Use Exponential Backoff + +Use progressively longer delays between retries to avoid hammering failing services. + +Incorrect (fixed retry interval): +```php +class SyncWithStripe implements ShouldQueue +{ + public $tries = 3; + // Default: retries immediately, overwhelming the API +} +``` + +Correct (exponential backoff): +```php +class SyncWithStripe implements ShouldQueue +{ + public $tries = 3; + public $backoff = [1, 5, 10]; +} +``` + +## Implement `ShouldBeUnique` + +Prevent duplicate job processing. + +```php +class GenerateInvoice implements ShouldQueue, ShouldBeUnique +{ + public function uniqueId(): string + { + return $this->order->id; + } + + public $uniqueFor = 3600; +} +``` + +## Always Implement `failed()` + +Handle errors explicitly — don't rely on silent failure. + +```php +public function failed(?Throwable $exception): void +{ + $this->podcast->update(['status' => 'failed']); + Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); +} +``` + +## Rate Limit External API Calls in Jobs + +Use `RateLimited` middleware to throttle jobs calling third-party APIs. + +```php +public function middleware(): array +{ + return [new RateLimited('external-api')]; +} +``` + +## Batch Related Jobs + +Use `Bus::batch()` when jobs should succeed or fail together. + +```php +Bus::batch([ + new ImportCsvChunk($chunk1), + new ImportCsvChunk($chunk2), +]) +->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) +->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed')) +->dispatch(); +``` + +## `retryUntil()` Needs `$tries = 0` + +When using time-based retry limits, set `$tries = 0` to avoid premature failure. + +```php +public $tries = 0; + +public function retryUntil(): \DateTimeInterface +{ + return now()->addHours(4); +} +``` + +## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release + +`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue. + +```php +class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing +{ + // Lock releases when processing begins, not when it finishes +} +``` + +## Use Horizon for Complex Queue Scenarios + +Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities. + +```php +// config/horizon.php +'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'low'], + 'balance' => 'auto', + 'minProcesses' => 1, + 'maxProcesses' => 10, + 'tries' => 3, + ], + ], +], +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/routing.md b/.claude/skills/laravel-best-practices/rules/routing.md new file mode 100644 index 00000000..977d136e --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/routing.md @@ -0,0 +1,99 @@ +# Routing & Controllers Best Practices + +## Use Implicit Route Model Binding + +Let Laravel resolve models automatically from route parameters. + +Incorrect: +```php +public function show(int $id) +{ + $post = Post::findOrFail($id); +} +``` + +Correct: +```php +public function show(Post $post) +{ + return view('posts.show', ['post' => $post]); +} +``` + +## Use Scoped Bindings for Nested Resources + +Enforce parent-child relationships automatically. + +```php +Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { + // $post is automatically scoped to $user +})->scopeBindings(); +``` + +## Use Resource Controllers + +Use `Route::resource()` or `apiResource()` for RESTful endpoints. + +```php +Route::resource('posts', PostController::class); +// In routes/api.php — the /api prefix is applied automatically +Route::apiResource('posts', Api\PostController::class); +``` + +## Keep Controllers Thin + +Aim for under 10 lines per method. Extract business logic to action or service classes. + +Incorrect: +```php +public function store(Request $request) +{ + $validated = $request->validate([...]); + if ($request->hasFile('image')) { + $request->file('image')->move(public_path('images')); + } + $post = Post::create($validated); + $post->tags()->sync($validated['tags']); + event(new PostCreated($post)); + return redirect()->route('posts.show', $post); +} +``` + +Correct: +```php +public function store(StorePostRequest $request, CreatePostAction $create) +{ + $post = $create->execute($request->validated()); + + return redirect()->route('posts.show', $post); +} +``` + +## Type-Hint Form Requests + +Type-hinting Form Requests triggers automatic validation and authorization before the method executes. + +Incorrect: +```php +public function store(Request $request): RedirectResponse +{ + $validated = $request->validate([ + 'title' => ['required', 'max:255'], + 'body' => ['required'], + ]); + + Post::create($validated); + + return redirect()->route('posts.index'); +} +``` + +Correct: +```php +public function store(StorePostRequest $request): RedirectResponse +{ + Post::create($request->validated()); + + return redirect()->route('posts.index'); +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/scheduling.md b/.claude/skills/laravel-best-practices/rules/scheduling.md new file mode 100644 index 00000000..dfaefa26 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/scheduling.md @@ -0,0 +1,39 @@ +# Task Scheduling Best Practices + +## Use `withoutOverlapping()` on Variable-Duration Tasks + +Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. + +## Use `onOneServer()` on Multi-Server Deployments + +Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). + +## Use `runInBackground()` for Concurrent Long Tasks + +By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. + +## Use `environments()` to Restrict Tasks + +Prevent accidental execution of production-only tasks (billing, reporting) on staging. + +```php +Schedule::command('billing:charge')->monthly()->environments(['production']); +``` + +## Use `takeUntilTimeout()` for Time-Bounded Processing + +A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. + +## Use Schedule Groups for Shared Configuration + +Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. + +```php +Schedule::daily() + ->onOneServer() + ->timezone('America/New_York') + ->group(function () { + Schedule::command('emails:send --force'); + Schedule::command('emails:prune'); + }); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/security.md b/.claude/skills/laravel-best-practices/rules/security.md new file mode 100644 index 00000000..909ff91a --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/security.md @@ -0,0 +1,198 @@ +# Security Best Practices + +## Mass Assignment Protection + +Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). + +Incorrect: +```php +class User extends Model +{ + protected $guarded = []; // All fields are mass assignable +} +``` + +Correct: +```php +class User extends Model +{ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; +} +``` + +Never use `$guarded = []` on models that accept user input. + +## Authorize Every Action + +Use policies or gates in controllers. Never skip authorization. + +Incorrect: +```php +public function update(UpdatePostRequest $request, Post $post) +{ + $post->update($request->validated()); +} +``` + +Correct: +```php +public function update(UpdatePostRequest $request, Post $post) +{ + Gate::authorize('update', $post); + + $post->update($request->validated()); +} +``` + +Or via Form Request: + +```php +public function authorize(): bool +{ + return $this->user()->can('update', $this->route('post')); +} +``` + +## Prevent SQL Injection + +Always use parameter binding. Never interpolate user input into queries. + +Incorrect: +```php +DB::select("SELECT * FROM users WHERE name = '{$request->name}'"); +``` + +Correct: +```php +User::where('name', $request->name)->get(); + +// Raw expressions with bindings +User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get(); +``` + +## Escape Output to Prevent XSS + +Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. + +Incorrect: +```blade +{!! $user->bio !!} +``` + +Correct: +```blade +{{ $user->bio }} +``` + +## CSRF Protection + +Include `@csrf` in all POST/PUT/DELETE Blade forms. In Inertia apps, the `@csrf` directive is automatically applied. + +Incorrect: +```blade +
+ +
+``` + +Correct: +```blade +
+ @csrf + +
+``` + +## Rate Limit Auth and API Routes + +Apply `throttle` middleware to authentication and API routes. + +```php +RateLimiter::for('login', function (Request $request) { + return Limit::perMinute(5)->by($request->ip()); +}); + +Route::post('/login', LoginController::class)->middleware('throttle:login'); +``` + +## Validate File Uploads + +Validate extension, MIME type, and size. The `mimes` rule checks extensions; use `mimetypes` for actual MIME type validation. Never trust client-provided filenames. + +```php +public function rules(): array +{ + return [ + 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], + ]; +} +``` + +Store with generated filenames: + +```php +$path = $request->file('avatar')->store('avatars', 'public'); +``` + +## Keep Secrets Out of Code + +Never commit `.env`. Access secrets via `config()` only. + +Incorrect: +```php +$key = env('API_KEY'); +``` + +Correct: +```php +// config/services.php +'api_key' => env('API_KEY'), + +// In application code +$key = config('services.api_key'); +``` + +## Audit Dependencies + +Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. + +```bash +composer audit +``` + +## Encrypt Sensitive Database Fields + +Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. + +Incorrect: +```php +class Integration extends Model +{ + protected function casts(): array + { + return [ + 'api_key' => 'string', + ]; + } +} +``` + +Correct: +```php +class Integration extends Model +{ + protected $hidden = ['api_key', 'api_secret']; + + protected function casts(): array + { + return [ + 'api_key' => 'encrypted', + 'api_secret' => 'encrypted', + ]; + } +} +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/style.md b/.claude/skills/laravel-best-practices/rules/style.md new file mode 100644 index 00000000..67af9891 --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/style.md @@ -0,0 +1,125 @@ +# Conventions & Style + +## Follow Laravel Naming Conventions + +| What | Convention | Good | Bad | +|------|-----------|------|-----| +| Controller | singular | `ArticleController` | `ArticlesController` | +| Model | singular | `User` | `Users` | +| Table | plural, snake_case | `article_comments` | `articleComments` | +| Pivot table | singular alphabetical | `article_user` | `user_article` | +| Column | snake_case, no model name | `meta_title` | `article_meta_title` | +| Foreign key | singular model + `_id` | `article_id` | `articles_id` | +| Route | plural | `articles/1` | `article/1` | +| Route name | snake_case with dots | `users.show_active` | `users.show-active` | +| Method | camelCase | `getAll` | `get_all` | +| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` | +| Collection | descriptive, plural | `$activeUsers` | `$data` | +| Object | descriptive, singular | `$activeUser` | `$users` | +| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` | +| Config | snake_case | `google_calendar.php` | `googleCalendar.php` | +| Enum | singular | `UserType` | `UserTypes` | + +## Prefer Shorter Readable Syntax + +| Verbose | Shorter | +|---------|---------| +| `Session::get('cart')` | `session('cart')` | +| `$request->session()->get('cart')` | `session('cart')` | +| `$request->input('name')` | `$request->name` | +| `return Redirect::back()` | `return back()` | +| `Carbon::now()` | `now()` | +| `App::make('Class')` | `app('Class')` | +| `->where('column', '=', 1)` | `->where('column', 1)` | +| `->orderBy('created_at', 'desc')` | `->latest()` | +| `->orderBy('created_at', 'asc')` | `->oldest()` | +| `->first()->name` | `->value('name')` | + +## Use Laravel String & Array Helpers + +Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them. + +Strings — use `Str` and fluent `Str::of()` over raw PHP: +```php +// Incorrect +$slug = strtolower(str_replace(' ', '-', $title)); +$short = substr($text, 0, 100) . '...'; +$class = substr(strrchr('App\Models\User', '\'), 1); + +// Correct +$slug = Str::slug($title); +$short = Str::limit($text, 100); +$class = class_basename('App\Models\User'); +``` + +Fluent strings — chain operations for complex transformations: +```php +// Incorrect +$result = strtolower(trim(str_replace('_', '-', $input))); + +// Correct +$result = Str::of($input)->trim()->replace('_', '-')->lower(); +``` + +Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`. + +Arrays — use `Arr` over raw PHP: +```php +// Incorrect +$name = isset($array['user']['name']) ? $array['user']['name'] : 'default'; + +// Correct +$name = Arr::get($array, 'user.name', 'default'); +``` + +Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`. + +Numbers — use `Number` for display formatting: +```php +Number::format(1000000); // "1,000,000" +Number::currency(1500, 'USD'); // "$1,500.00" +Number::abbreviate(1000000); // "1M" +Number::fileSize(1024 * 1024); // "1 MB" +Number::percentage(75.5); // "75.5%" +``` + +URIs — use `Uri` for URL manipulation: +```php +$uri = Uri::of('https://example.com/search') + ->withQuery(['q' => 'laravel', 'page' => 1]); +``` + +Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining. + +Use `search-docs` for the full list of available methods — these helpers are extensive. + +## No Inline JS/CSS in Blade + +Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes. + +Incorrect: +```blade +let article = `{{ json_encode($article) }}`; +``` + +Correct: +```blade + +``` + +Pass data to JS via data attributes or use a dedicated PHP-to-JS package. + +## No Unnecessary Comments + +Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected. + +Incorrect: +```php +// Check if there are any joins +if (count((array) $builder->getQuery()->joins) > 0) +``` + +Correct: +```php +if ($this->hasJoins()) +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/testing.md b/.claude/skills/laravel-best-practices/rules/testing.md new file mode 100644 index 00000000..287b083b --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/testing.md @@ -0,0 +1,43 @@ +# Testing Best Practices + +## Use `LazilyRefreshDatabase` Over `RefreshDatabase` + +`RefreshDatabase` migrates once per process and wraps each test in a rolled-back transaction. `LazilyRefreshDatabase` skips even that first migration if the schema is already up to date. + +## Use Model Assertions Over Raw Database Assertions + +Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);` + +Correct: `$this->assertModelExists($user);` + +More expressive, type-safe, and fails with clearer messages. + +## Use Factory States and Sequences + +Named states make tests self-documenting. Sequences eliminate repetitive setup. + +Incorrect: `User::factory()->create(['email_verified_at' => null]);` + +Correct: `User::factory()->unverified()->create();` + +## Use `Exceptions::fake()` to Assert Exception Reporting + +Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally. + +## Call `Event::fake()` After Factory Setup + +Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models. + +Incorrect: `Event::fake(); $user = User::factory()->create();` + +Correct: `$user = User::factory()->create(); Event::fake();` + +## Use `recycle()` to Share Relationship Instances Across Factories + +Without `recycle()`, nested factories create separate instances of the same conceptual entity. + +```php +Ticket::factory() + ->recycle(Airline::factory()->create()) + ->create(); +``` \ No newline at end of file diff --git a/.claude/skills/laravel-best-practices/rules/validation.md b/.claude/skills/laravel-best-practices/rules/validation.md new file mode 100644 index 00000000..a20202ff --- /dev/null +++ b/.claude/skills/laravel-best-practices/rules/validation.md @@ -0,0 +1,75 @@ +# Validation & Forms Best Practices + +## Use Form Request Classes + +Extract validation from controllers into dedicated Form Request classes. + +Incorrect: +```php +public function store(Request $request) +{ + $request->validate([ + 'title' => 'required|max:255', + 'body' => 'required', + ]); +} +``` + +Correct: +```php +public function store(StorePostRequest $request) +{ + Post::create($request->validated()); +} +``` + +## Array vs. String Notation for Rules + +Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses. + +```php +// Preferred for new code +'email' => ['required', 'email', Rule::unique('users')], + +// Follow existing convention if the project uses string notation +'email' => 'required|email|unique:users', +``` + +## Always Use `validated()` + +Get only validated data. Never use `$request->all()` for mass operations. + +Incorrect: +```php +Post::create($request->all()); +``` + +Correct: +```php +Post::create($request->validated()); +``` + +## Use `Rule::when()` for Conditional Validation + +```php +'company_name' => [ + Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), +], +``` + +## Use the `after()` Method for Custom Validation + +Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. + +```php +public function after(): array +{ + return [ + function (Validator $validator) { + if ($this->quantity > Product::find($this->product_id)?->stock) { + $validator->errors()->add('quantity', 'Not enough stock.'); + } + }, + ]; +} +``` \ No newline at end of file diff --git a/.claude/skills/livewire-development/SKILL.md b/.claude/skills/livewire-development/SKILL.md new file mode 100644 index 00000000..c009dae6 --- /dev/null +++ b/.claude/skills/livewire-development/SKILL.md @@ -0,0 +1,156 @@ +--- +name: livewire-development +description: "Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, wire:sort, or islands, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, drag-and-drop, loading states, migrating from Livewire 3 to 4, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire." +license: MIT +metadata: + author: laravel +--- + +# Livewire Development + +## Documentation + +Use `search-docs` for detailed Livewire 4 patterns and documentation. + +## Basic Usage + +### Creating Components + +```bash + +# Single-file component (default in v4) + +php artisan make:livewire create-post + +# Multi-file component + +php artisan make:livewire create-post --mfc + +# Class-based component (v3 style) + +php artisan make:livewire create-post --class + +# With namespace + +php artisan make:livewire Posts/CreatePost +``` + +### Converting Between Formats + +Use `php artisan livewire:convert create-post` to convert between single-file, multi-file, and class-based formats. + +### Choosing a Component Format + +Before creating a component, check `config/livewire.php` for directory overrides, which change where files are stored. Then, look at existing files in those directories (defaulting to `app/Livewire/` and `resources/views/livewire/`) to match the established convention. + +### Component Format Reference + +| Format | Flag | Class Path | View Path | +|--------|------|------------|-----------| +| Single-file (SFC) | default | — | `resources/views/livewire/create-post.blade.php` (PHP + Blade in one file) | +| Multi-file (MFC) | `--mfc` | `app/Livewire/CreatePost.php` | `resources/views/livewire/create-post.blade.php` | +| Class-based | `--class` | `app/Livewire/CreatePost.php` | `resources/views/livewire/create-post.blade.php` | +| View-based | ⚡ prefix | — | `resources/views/livewire/create-post.blade.php` (Blade-only with functional state) | + +Namespaced components map to subdirectories: `make:livewire Posts/CreatePost` creates files at `app/Livewire/Posts/CreatePost.php` and `resources/views/livewire/posts/create-post.blade.php`. + +### Single-File Component Example + + +```php +count++; + } +} +?> + +
+ +
+``` + +## Livewire 4 Specifics + +### Key Changes From Livewire 3 + +These things changed in Livewire 4, but may not have been updated in this application. Verify this application's setup to ensure you follow existing conventions. + +- Use `Route::livewire()` for full-page components (e.g., `Route::livewire('/posts/create', CreatePost::class)`); config keys renamed: `layout` → `component_layout`, `lazy_placeholder` → `component_placeholder`. +- `wire:model` now ignores child events by default (use `wire:model.deep` for old behavior); `wire:scroll` renamed to `wire:navigate:scroll`. +- Component tags must be properly closed; `wire:transition` now uses View Transitions API (modifiers removed). +- JavaScript: `$wire.$js('name', fn)` → `$wire.$js.name = fn`; `commit`/`request` hooks → `interceptMessage()`/`interceptRequest()`. + +### New Features + +- Component formats: single-file (SFC), multi-file (MFC), view-based components. +- Islands (`@island`) for isolated updates; async actions (`wire:click.async`, `#[Async]`) for parallel execution. +- Deferred/bundled loading: `defer`, `lazy.bundle` for optimized component loading. + +| Feature | Usage | Purpose | +|---------|-------|---------| +| Islands | `@island(name: 'stats')` | Isolated update regions | +| Async | `wire:click.async` or `#[Async]` | Non-blocking actions | +| Deferred | `defer` attribute | Load after page render | +| Bundled | `lazy.bundle` | Load multiple together | + +### New Directives + +- `wire:sort`, `wire:intersect`, `wire:ref`, `.renderless`, `.preserve-scroll` are available for use. +- `data-loading` attribute automatically added to elements triggering network requests. + +| Directive | Purpose | +|-----------|---------| +| `wire:sort` | Drag-and-drop sorting | +| `wire:intersect` | Viewport intersection detection | +| `wire:ref` | Element references for JS | +| `.renderless` | Component without rendering | +| `.preserve-scroll` | Preserve scroll position | + +## Best Practices + +- Always use `wire:key` in loops +- Use `wire:loading` for loading states +- Use `wire:model.live` for instant updates (default is debounced) +- Validate and authorize in actions (treat like HTTP requests) + +## Configuration + +- `smart_wire_keys` defaults to `true`; new configs: `component_locations`, `component_namespaces`, `make_command`, `csp_safe`. + +## Alpine & JavaScript + +- `wire:transition` uses browser View Transitions API; `$errors` and `$intercept` magic properties available. +- Non-blocking `wire:poll` and parallel `wire:model.live` updates improve performance. + +For interceptors and hooks, see [reference/javascript-hooks.md](reference/javascript-hooks.md). + +## Testing + + +```php +Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1); +``` + +## Verification + +1. Browser console: Check for JS errors +2. Network tab: Verify Livewire requests return 200 +3. Ensure `wire:key` on all `@foreach` loops + +## Common Pitfalls + +- Missing `wire:key` in loops → unexpected re-rendering +- Expecting `wire:model` real-time → use `wire:model.live` +- Unclosed component tags → syntax errors in v4 +- Using deprecated config keys or JS hooks +- Including Alpine.js separately (already bundled in Livewire 4) \ No newline at end of file diff --git a/.claude/skills/livewire-development/reference/javascript-hooks.md b/.claude/skills/livewire-development/reference/javascript-hooks.md new file mode 100644 index 00000000..d6a44170 --- /dev/null +++ b/.claude/skills/livewire-development/reference/javascript-hooks.md @@ -0,0 +1,39 @@ +# Livewire 4 JavaScript Integration + +## Interceptor System (v4) + +### Intercept Messages + +```js +Livewire.interceptMessage(({ component, message, onFinish, onSuccess, onError }) => { + onFinish(() => { /* After response, before processing */ }); + onSuccess(({ payload }) => { /* payload.snapshot, payload.effects */ }); + onError(() => { /* Server errors */ }); +}); +``` + +### Intercept Requests + +```js +Livewire.interceptRequest(({ request, onResponse, onSuccess, onError, onFailure }) => { + onResponse(({ response }) => { /* When received */ }); + onSuccess(({ response, responseJson }) => { /* Success */ }); + onError(({ response, responseBody, preventDefault }) => { /* 4xx/5xx */ }); + onFailure(({ error }) => { /* Network failures */ }); +}); +``` + +### Component-Scoped Interceptors + +```blade + +``` + +## Magic Properties + +- `$errors` - Access validation errors from JavaScript +- `$intercept` - Component-scoped interceptors \ No newline at end of file diff --git a/.claude/skills/pest-testing/SKILL.md b/.claude/skills/pest-testing/SKILL.md new file mode 100644 index 00000000..323d4723 --- /dev/null +++ b/.claude/skills/pest-testing/SKILL.md @@ -0,0 +1,159 @@ +--- +name: pest-testing +description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." +license: MIT +metadata: + author: laravel +--- + +# Pest Testing 4 + +## Documentation + +Use `search-docs` for detailed Pest 4 patterns and documentation. + +## Basic Usage + +### Creating Tests + +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. + +### Test Organization + +- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories. +- Browser tests: `tests/Browser/` directory. +- Do NOT remove tests without approval - these are core application code. + +### Basic Test Structure + +Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`. + + +```php +it('is true', function () { + expect(true)->toBeTrue(); +}); +``` + +### Running Tests + +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. + +## Assertions + +Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: + + +```php +it('returns all', function () { + $this->postJson('/api/docs', [])->assertSuccessful(); +}); +``` + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Mocking + +Import mock function before use: `use function Pest\Laravel\mock;` + +## Datasets + +Use datasets for repetitive tests (validation rules, etc.): + + +```php +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +``` + +## Pest 4 Features + +| Feature | Purpose | +|---------|---------| +| Browser Testing | Full integration tests in real browsers | +| Smoke Testing | Validate multiple pages quickly | +| Visual Regression | Compare screenshots for visual changes | +| Test Sharding | Parallel CI runs | +| Architecture Testing | Enforce code conventions | + +### Browser Test Example + +Browser tests run in real browsers for full integration testing: + +- Browser tests live in `tests/Browser/`. +- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories. +- Use `RefreshDatabase` for clean state per test. +- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures. +- Test on multiple browsers (Chrome, Firefox, Safari) if requested. +- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested. +- Switch color schemes (light/dark mode) when appropriate. +- Take screenshots or pause tests for debugging. + + +```php +it('may reset the password', function () { + Notification::fake(); + + $this->actingAs(User::factory()->create()); + + $page = visit('/sign-in'); + + $page->assertSee('Sign In') + ->assertNoJavaScriptErrors() + ->click('Forgot Password?') + ->fill('email', 'nuno@laravel.com') + ->click('Send Reset Link') + ->assertSee('We have emailed your password reset link!'); + + Notification::assertSent(ResetPassword::class); +}); +``` + +### Smoke Testing + +Quickly validate multiple pages have no JavaScript errors: + + +```php +$pages = visit(['/', '/about', '/contact']); + +$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); +``` + +### Visual Regression Testing + +Capture and compare screenshots to detect visual changes. + +### Test Sharding + +Split tests across parallel processes for faster CI runs. + +### Architecture Testing + +Pest 4 includes architecture testing (from Pest 3): + + +```php +arch('controllers') + ->expect('App\Http\Controllers') + ->toExtendNothing() + ->toHaveSuffix('Controller'); +``` + +## Common Pitfalls + +- Not importing `use function Pest\Laravel\mock;` before using mock +- Using `assertStatus(200)` instead of `assertSuccessful()` +- Forgetting datasets for repetitive validation tests +- Deleting tests without approval +- Forgetting `assertNoJavaScriptErrors()` in browser tests \ No newline at end of file diff --git a/.claude/skills/tailwindcss-development/SKILL.md b/.claude/skills/tailwindcss-development/SKILL.md new file mode 100644 index 00000000..7c8e295e --- /dev/null +++ b/.claude/skills/tailwindcss-development/SKILL.md @@ -0,0 +1,119 @@ +--- +name: tailwindcss-development +description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS." +license: MIT +metadata: + author: laravel +--- + +# Tailwind CSS Development + +## Documentation + +Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation. + +## Basic Usage + +- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns. +- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue). +- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically. + +## Tailwind CSS v4 Specifics + +- Always use Tailwind CSS v4 and avoid deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. + +### CSS-First Configuration + +In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed: + + +```css +@theme { + --color-brand: oklch(0.72 0.11 178); +} +``` + +### Import Syntax + +In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3: + + +```diff +- @tailwind base; +- @tailwind components; +- @tailwind utilities; ++ @import "tailwindcss"; +``` + +### Replaced Utilities + +Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric. + +| Deprecated | Replacement | +|------------|-------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | + +## Spacing + +Use `gap` utilities instead of margins for spacing between siblings: + + +```html +
+
Item 1
+
Item 2
+
+``` + +## Dark Mode + +If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant: + + +```html +
+ Content adapts to color scheme +
+``` + +## Common Patterns + +### Flexbox Layout + + +```html +
+
Left content
+
Right content
+
+``` + +### Grid Layout + + +```html +
+
Card 1
+
Card 2
+
Card 3
+
+``` + +## Common Pitfalls + +- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.) +- Using `@tailwind` directives instead of `@import "tailwindcss"` +- Trying to use `tailwind.config.js` instead of CSS `@theme` directive +- Using margins for spacing between siblings instead of gap utilities +- Forgetting to add dark mode variants when the project uses dark mode \ No newline at end of file diff --git a/.mcp.json b/.mcp.json index 0ad95248..b2d6bef5 100644 --- a/.mcp.json +++ b/.mcp.json @@ -3,7 +3,7 @@ "laravel-boost": { "command": "php", "args": [ - "./artisan", + "artisan", "boost:mcp" ] }, diff --git a/.playwright-mcp/console-2026-04-12T19-33-49-701Z.log b/.playwright-mcp/console-2026-04-12T19-33-49-701Z.log new file mode 100644 index 00000000..ef88e0aa --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-33-49-701Z.log @@ -0,0 +1,2 @@ +[ 422ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/:50 +[ 491ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://shop.test/favicon.ico:0 diff --git a/.playwright-mcp/console-2026-04-12T19-33-56-875Z.log b/.playwright-mcp/console-2026-04-12T19-33-56-875Z.log new file mode 100644 index 00000000..115497d3 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-33-56-875Z.log @@ -0,0 +1 @@ +[ 59ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/collections:50 diff --git a/.playwright-mcp/console-2026-04-12T19-34-21-759Z.log b/.playwright-mcp/console-2026-04-12T19-34-21-759Z.log new file mode 100644 index 00000000..7402d2d8 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-34-21-759Z.log @@ -0,0 +1 @@ +[ 54ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/collections:50 diff --git a/.playwright-mcp/console-2026-04-12T19-34-27-319Z.log b/.playwright-mcp/console-2026-04-12T19-34-27-319Z.log new file mode 100644 index 00000000..4307b2a3 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-34-27-319Z.log @@ -0,0 +1 @@ +[ 56ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/products/demo-shirt:50 diff --git a/.playwright-mcp/console-2026-04-12T19-34-39-764Z.log b/.playwright-mcp/console-2026-04-12T19-34-39-764Z.log new file mode 100644 index 00000000..644bda8d --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-34-39-764Z.log @@ -0,0 +1,2 @@ +[ 58ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/checkout:50 +[ 39675ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/checkout/confirmation/1001:50 diff --git a/.playwright-mcp/console-2026-04-12T19-35-29-489Z.log b/.playwright-mcp/console-2026-04-12T19-35-29-489Z.log new file mode 100644 index 00000000..e03d705a --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-35-29-489Z.log @@ -0,0 +1 @@ +[ 48ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/account/register:50 diff --git a/.playwright-mcp/console-2026-04-12T19-37-32-349Z.log b/.playwright-mcp/console-2026-04-12T19-37-32-349Z.log new file mode 100644 index 00000000..e0457e1e --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-37-32-349Z.log @@ -0,0 +1 @@ +[ 60ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/account/register:50 diff --git a/.playwright-mcp/console-2026-04-12T19-37-37-035Z.log b/.playwright-mcp/console-2026-04-12T19-37-37-035Z.log new file mode 100644 index 00000000..5a1307a7 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-37-37-035Z.log @@ -0,0 +1 @@ +[ 51ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/account/login:50 diff --git a/.playwright-mcp/console-2026-04-12T19-37-39-399Z.log b/.playwright-mcp/console-2026-04-12T19-37-39-399Z.log new file mode 100644 index 00000000..9fe30e11 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-37-39-399Z.log @@ -0,0 +1 @@ +[ 60ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/account/login:50 diff --git a/.playwright-mcp/console-2026-04-12T19-50-11-319Z.log b/.playwright-mcp/console-2026-04-12T19-50-11-319Z.log new file mode 100644 index 00000000..d47e04da --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-50-11-319Z.log @@ -0,0 +1,2 @@ +[ 59ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/login:45 +[ 10581ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/account/login:50 diff --git a/.playwright-mcp/console-2026-04-12T19-50-43-086Z.log b/.playwright-mcp/console-2026-04-12T19-50-43-086Z.log new file mode 100644 index 00000000..33d73b7f --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-50-43-086Z.log @@ -0,0 +1 @@ +[ 73ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/dashboard:52 diff --git a/.playwright-mcp/console-2026-04-12T19-50-46-483Z.log b/.playwright-mcp/console-2026-04-12T19-50-46-483Z.log new file mode 100644 index 00000000..49ccb477 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-50-46-483Z.log @@ -0,0 +1 @@ +[ 103ms] [ERROR] Failed to load resource: the server responded with a status of 405 (Method Not Allowed) @ http://shop.test/logout:0 diff --git a/.playwright-mcp/console-2026-04-12T19-51-05-175Z.log b/.playwright-mcp/console-2026-04-12T19-51-05-175Z.log new file mode 100644 index 00000000..8601fff0 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-51-05-175Z.log @@ -0,0 +1 @@ +[ 61ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin:45 diff --git a/.playwright-mcp/console-2026-04-12T19-51-11-228Z.log b/.playwright-mcp/console-2026-04-12T19-51-11-228Z.log new file mode 100644 index 00000000..4782b81c --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-51-11-228Z.log @@ -0,0 +1 @@ +[ 59ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/products:45 diff --git a/.playwright-mcp/console-2026-04-12T19-51-19-642Z.log b/.playwright-mcp/console-2026-04-12T19-51-19-642Z.log new file mode 100644 index 00000000..6d13237c --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-51-19-642Z.log @@ -0,0 +1 @@ +[ 66ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/orders/1:45 diff --git a/.playwright-mcp/console-2026-04-12T19-51-22-100Z.log b/.playwright-mcp/console-2026-04-12T19-51-22-100Z.log new file mode 100644 index 00000000..e6450a37 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-51-22-100Z.log @@ -0,0 +1 @@ +[ 59ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/products/create:45 diff --git a/.playwright-mcp/console-2026-04-12T19-51-26-071Z.log b/.playwright-mcp/console-2026-04-12T19-51-26-071Z.log new file mode 100644 index 00000000..5bf68a25 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T19-51-26-071Z.log @@ -0,0 +1 @@ +[ 66ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/discounts/create:45 diff --git a/.playwright-mcp/console-2026-04-12T20-16-49-621Z.log b/.playwright-mcp/console-2026-04-12T20-16-49-621Z.log new file mode 100644 index 00000000..f76cd380 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-16-49-621Z.log @@ -0,0 +1 @@ +[ 91ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/dashboard:52 diff --git a/.playwright-mcp/console-2026-04-12T20-16-52-734Z.log b/.playwright-mcp/console-2026-04-12T20-16-52-734Z.log new file mode 100644 index 00000000..32d8d3f4 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-16-52-734Z.log @@ -0,0 +1 @@ +[ 65ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/pages:45 diff --git a/.playwright-mcp/console-2026-04-12T20-16-59-169Z.log b/.playwright-mcp/console-2026-04-12T20-16-59-169Z.log new file mode 100644 index 00000000..d421edd1 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-16-59-169Z.log @@ -0,0 +1 @@ +[ 60ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/settings:45 diff --git a/.playwright-mcp/console-2026-04-12T20-17-01-728Z.log b/.playwright-mcp/console-2026-04-12T20-17-01-728Z.log new file mode 100644 index 00000000..b1e3db67 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-17-01-728Z.log @@ -0,0 +1 @@ +[ 56ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/analytics:45 diff --git a/.playwright-mcp/console-2026-04-12T20-17-04-224Z.log b/.playwright-mcp/console-2026-04-12T20-17-04-224Z.log new file mode 100644 index 00000000..d465c13d --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-17-04-224Z.log @@ -0,0 +1 @@ +[ 57ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/developers:45 diff --git a/.playwright-mcp/console-2026-04-12T20-17-06-673Z.log b/.playwright-mcp/console-2026-04-12T20-17-06-673Z.log new file mode 100644 index 00000000..3797c10f --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-17-06-673Z.log @@ -0,0 +1 @@ +[ 59ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/navigation:45 diff --git a/.playwright-mcp/console-2026-04-12T20-20-44-469Z.log b/.playwright-mcp/console-2026-04-12T20-20-44-469Z.log new file mode 100644 index 00000000..54393d21 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-20-44-469Z.log @@ -0,0 +1,2 @@ +[ 61ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/:50 +[ 12344ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/products/classic-tee:50 diff --git a/.playwright-mcp/console-2026-04-12T20-21-14-027Z.log b/.playwright-mcp/console-2026-04-12T20-21-14-027Z.log new file mode 100644 index 00000000..01dda443 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-21-14-027Z.log @@ -0,0 +1,2 @@ +[ 54ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/checkout:50 +[ 52615ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/checkout/confirmation/1:50 diff --git a/.playwright-mcp/console-2026-04-12T20-22-31-874Z.log b/.playwright-mcp/console-2026-04-12T20-22-31-874Z.log new file mode 100644 index 00000000..b8dbdb29 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-22-31-874Z.log @@ -0,0 +1 @@ +[ 65ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/dashboard:52 diff --git a/.playwright-mcp/console-2026-04-12T20-22-59-809Z.log b/.playwright-mcp/console-2026-04-12T20-22-59-809Z.log new file mode 100644 index 00000000..b86b6be4 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-22-59-809Z.log @@ -0,0 +1 @@ +[ 64ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin:45 diff --git a/.playwright-mcp/console-2026-04-12T20-23-09-968Z.log b/.playwright-mcp/console-2026-04-12T20-23-09-968Z.log new file mode 100644 index 00000000..3f8458ad --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-23-09-968Z.log @@ -0,0 +1 @@ +[ 64ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/products:45 diff --git a/.playwright-mcp/console-2026-04-12T20-23-24-602Z.log b/.playwright-mcp/console-2026-04-12T20-23-24-602Z.log new file mode 100644 index 00000000..91e6ada9 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-23-24-602Z.log @@ -0,0 +1 @@ +[ 58ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/orders:45 diff --git a/.playwright-mcp/console-2026-04-12T20-23-34-126Z.log b/.playwright-mcp/console-2026-04-12T20-23-34-126Z.log new file mode 100644 index 00000000..c94ee1cd --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-23-34-126Z.log @@ -0,0 +1 @@ +[ 70ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/settings/shipping:45 diff --git a/.playwright-mcp/console-2026-04-12T20-23-49-243Z.log b/.playwright-mcp/console-2026-04-12T20-23-49-243Z.log new file mode 100644 index 00000000..53b98925 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-23-49-243Z.log @@ -0,0 +1 @@ +[ 58ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/developers:45 diff --git a/.playwright-mcp/console-2026-04-12T20-23-59-855Z.log b/.playwright-mcp/console-2026-04-12T20-23-59-855Z.log new file mode 100644 index 00000000..e13da19e --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-23-59-855Z.log @@ -0,0 +1 @@ +[ 62ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/pages:45 diff --git a/.playwright-mcp/console-2026-04-12T20-24-08-314Z.log b/.playwright-mcp/console-2026-04-12T20-24-08-314Z.log new file mode 100644 index 00000000..fe3c8dc3 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-24-08-314Z.log @@ -0,0 +1 @@ +[ 59ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/analytics:45 diff --git a/.playwright-mcp/console-2026-04-12T20-24-13-024Z.log b/.playwright-mcp/console-2026-04-12T20-24-13-024Z.log new file mode 100644 index 00000000..d3aae899 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-24-13-024Z.log @@ -0,0 +1 @@ +[ 53ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/collections/featured:50 diff --git a/.playwright-mcp/console-2026-04-12T20-28-09-852Z.log b/.playwright-mcp/console-2026-04-12T20-28-09-852Z.log new file mode 100644 index 00000000..2563eec6 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-28-09-852Z.log @@ -0,0 +1 @@ +[ 83ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin:45 diff --git a/.playwright-mcp/console-2026-04-12T20-28-14-286Z.log b/.playwright-mcp/console-2026-04-12T20-28-14-286Z.log new file mode 100644 index 00000000..f3f3b84b --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-28-14-286Z.log @@ -0,0 +1 @@ +[ 59ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/:50 diff --git a/.playwright-mcp/console-2026-04-12T20-28-27-005Z.log b/.playwright-mcp/console-2026-04-12T20-28-27-005Z.log new file mode 100644 index 00000000..c1a42533 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-28-27-005Z.log @@ -0,0 +1 @@ +[ 54ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/products/hoodie:50 diff --git a/.playwright-mcp/console-2026-04-12T20-28-32-463Z.log b/.playwright-mcp/console-2026-04-12T20-28-32-463Z.log new file mode 100644 index 00000000..709efa2d --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-28-32-463Z.log @@ -0,0 +1,2 @@ +[ 56ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/account/login:50 +[ 25573ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/account:50 diff --git a/.playwright-mcp/console-2026-04-12T20-29-06-914Z.log b/.playwright-mcp/console-2026-04-12T20-29-06-914Z.log new file mode 100644 index 00000000..ef20cd7c --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-29-06-914Z.log @@ -0,0 +1 @@ +[ 54ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/account/orders:50 diff --git a/.playwright-mcp/console-2026-04-12T20-29-18-849Z.log b/.playwright-mcp/console-2026-04-12T20-29-18-849Z.log new file mode 100644 index 00000000..77978b28 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-29-18-849Z.log @@ -0,0 +1 @@ +[ 65ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/orders:45 diff --git a/.playwright-mcp/console-2026-04-12T20-29-29-648Z.log b/.playwright-mcp/console-2026-04-12T20-29-29-648Z.log new file mode 100644 index 00000000..b355add5 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-29-29-648Z.log @@ -0,0 +1 @@ +[ 64ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/orders/1:45 diff --git a/.playwright-mcp/console-2026-04-12T20-32-45-747Z.log b/.playwright-mcp/console-2026-04-12T20-32-45-747Z.log new file mode 100644 index 00000000..5f25608c --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-32-45-747Z.log @@ -0,0 +1 @@ +[ 56ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/:50 diff --git a/.playwright-mcp/console-2026-04-12T20-32-58-546Z.log b/.playwright-mcp/console-2026-04-12T20-32-58-546Z.log new file mode 100644 index 00000000..cf7b7baa --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-32-58-546Z.log @@ -0,0 +1 @@ +[ 56ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/products/sneakers:50 diff --git a/.playwright-mcp/console-2026-04-12T20-33-11-871Z.log b/.playwright-mcp/console-2026-04-12T20-33-11-871Z.log new file mode 100644 index 00000000..a24ac338 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-33-11-871Z.log @@ -0,0 +1 @@ +[ 68ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin:45 diff --git a/.playwright-mcp/console-2026-04-12T20-33-43-718Z.log b/.playwright-mcp/console-2026-04-12T20-33-43-718Z.log new file mode 100644 index 00000000..bd2d02b4 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-33-43-718Z.log @@ -0,0 +1 @@ +[ 65ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/products:45 diff --git a/.playwright-mcp/console-2026-04-12T20-33-53-668Z.log b/.playwright-mcp/console-2026-04-12T20-33-53-668Z.log new file mode 100644 index 00000000..54c8dcdd --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-33-53-668Z.log @@ -0,0 +1 @@ +[ 70ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/orders/2:45 diff --git a/.playwright-mcp/console-2026-04-12T20-34-07-281Z.log b/.playwright-mcp/console-2026-04-12T20-34-07-281Z.log new file mode 100644 index 00000000..0e90e9a1 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-34-07-281Z.log @@ -0,0 +1 @@ +[ 67ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/settings/shipping:45 diff --git a/.playwright-mcp/console-2026-04-12T20-34-19-135Z.log b/.playwright-mcp/console-2026-04-12T20-34-19-135Z.log new file mode 100644 index 00000000..c64cb149 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-34-19-135Z.log @@ -0,0 +1 @@ +[ 57ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/products/mug:50 diff --git a/.playwright-mcp/console-2026-04-12T20-34-35-623Z.log b/.playwright-mcp/console-2026-04-12T20-34-35-623Z.log new file mode 100644 index 00000000..359ce201 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-34-35-623Z.log @@ -0,0 +1 @@ +[ 69ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/cart:50 diff --git a/.playwright-mcp/console-2026-04-12T20-34-55-233Z.log b/.playwright-mcp/console-2026-04-12T20-34-55-233Z.log new file mode 100644 index 00000000..c4f0b197 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-34-55-233Z.log @@ -0,0 +1 @@ +[ 53ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/products/mug:50 diff --git a/.playwright-mcp/console-2026-04-12T20-36-14-122Z.log b/.playwright-mcp/console-2026-04-12T20-36-14-122Z.log new file mode 100644 index 00000000..c0eef855 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-36-14-122Z.log @@ -0,0 +1,8 @@ +[ 58ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/checkout:50 +[ 13042ms] [LOG] 0 @ http://shop.test/checkout:109 +[ 13044ms] [LOG] 1 @ http://shop.test/checkout:109 +[ 13044ms] [LOG] 2 @ http://shop.test/checkout:109 +[ 13044ms] [LOG] 3 @ http://shop.test/checkout:109 +[ 13044ms] [LOG] 4 @ http://shop.test/checkout:109 +[ 13044ms] [LOG] 5 @ http://shop.test/checkout:109 +[ 48604ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/checkout/confirmation/1001:50 diff --git a/.playwright-mcp/console-2026-04-12T20-37-25-505Z.log b/.playwright-mcp/console-2026-04-12T20-37-25-505Z.log new file mode 100644 index 00000000..63345d68 --- /dev/null +++ b/.playwright-mcp/console-2026-04-12T20-37-25-505Z.log @@ -0,0 +1 @@ +[ 69ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/admin/orders:45 diff --git a/.playwright-mcp/page-2026-04-12T19-33-50-195Z.yml b/.playwright-mcp/page-2026-04-12T19-33-50-195Z.yml new file mode 100644 index 00000000..e8c51a3b --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-33-50-195Z.yml @@ -0,0 +1,37 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Laravel" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - paragraph [ref=e18]: New season + - heading "Thoughtfully made, honestly priced." [level=1] [ref=e19] + - paragraph [ref=e20]: A curated collection of timeless goods designed to last. Explore our latest arrivals and find something you will love. + - generic [ref=e21]: + - link "Shop the collection" [ref=e22] [cursor=pointer]: + - /url: "#featured" + - link "What is new" [ref=e23] [cursor=pointer]: + - /url: "#recent" + - generic [ref=e24]: + - generic [ref=e26]: + - heading "New arrivals" [level=2] [ref=e27] + - paragraph [ref=e28]: Fresh goods, just in. + - paragraph [ref=e30]: No products yet. Check back soon. + - contentinfo [ref=e31]: + - generic [ref=e33]: + - navigation "Footer navigation" + - paragraph [ref=e34]: (c) Shop + - button "Open cart" [ref=e37]: + - img [ref=e38] + - generic [ref=e40]: Cart + - generic [ref=e41]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-33-56-957Z.yml b/.playwright-mcp/page-2026-04-12T19-33-56-957Z.yml new file mode 100644 index 00000000..1dbd4d32 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-33-56-957Z.yml @@ -0,0 +1,28 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - paragraph [ref=e18]: Shop + - heading "All collections" [level=1] [ref=e19] + - paragraph [ref=e20]: Browse every curated edit across the store. + - paragraph [ref=e22]: No collections yet. + - contentinfo [ref=e23]: + - generic [ref=e25]: + - navigation "Footer navigation" + - paragraph [ref=e26]: (c) Shop + - button "Open cart" [ref=e29]: + - img [ref=e30] + - generic [ref=e32]: Cart + - generic [ref=e33]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-34-21-832Z.yml b/.playwright-mcp/page-2026-04-12T19-34-21-832Z.yml new file mode 100644 index 00000000..4ad42d57 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-34-21-832Z.yml @@ -0,0 +1,33 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - paragraph [ref=e18]: Shop + - heading "All collections" [level=1] [ref=e19] + - paragraph [ref=e20]: Browse every curated edit across the store. + - link "Demo Edit Demo collection 1 product" [ref=e22] [cursor=pointer]: + - /url: http://shop.test/collections/demo-edit + - generic [ref=e23]: + - heading "Demo Edit" [level=2] [ref=e24] + - generic [ref=e25]: Demo collection + - paragraph [ref=e26]: 1 product + - contentinfo [ref=e27]: + - generic [ref=e29]: + - navigation "Footer navigation" + - paragraph [ref=e30]: (c) Shop + - button "Open cart" [ref=e33]: + - img [ref=e34] + - generic [ref=e36]: Cart + - generic [ref=e37]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-34-27-395Z.yml b/.playwright-mcp/page-2026-04-12T19-34-27-395Z.yml new file mode 100644 index 00000000..bb346881 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-34-27-395Z.yml @@ -0,0 +1,36 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Co + - heading "Demo Shirt" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 29.99 + - generic [ref=e25]: + - generic [ref=e26]: Quantity + - generic [ref=e27]: + - button "Decrease quantity" [ref=e28]: "-" + - generic [ref=e29]: "1" + - button "Increase quantity" [ref=e30]: + + - button "Add to cart" [ref=e31]: + - generic [ref=e32]: Add to cart + - paragraph [ref=e34]: A nice demo. + - contentinfo [ref=e35]: + - generic [ref=e37]: + - navigation "Footer navigation" + - paragraph [ref=e38]: (c) Shop + - button "Open cart" [ref=e41]: + - img [ref=e42] + - generic [ref=e44]: Cart + - generic [ref=e45]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-34-34-736Z.yml b/.playwright-mcp/page-2026-04-12T19-34-34-736Z.yml new file mode 100644 index 00000000..0ffae611 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-34-34-736Z.yml @@ -0,0 +1,58 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Co + - heading "Demo Shirt" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 29.99 + - generic [ref=e25]: + - generic [ref=e26]: Quantity + - generic [ref=e27]: + - button "Decrease quantity" [ref=e28]: "-" + - generic [ref=e29]: "1" + - button "Increase quantity" [ref=e30]: + + - generic [ref=e46]: Added to cart + - button "Add to cart" [ref=e31]: + - generic [ref=e32]: Add to cart + - paragraph [ref=e34]: A nice demo. + - contentinfo [ref=e35]: + - generic [ref=e37]: + - navigation "Footer navigation" + - paragraph [ref=e38]: (c) Shop + - generic [ref=e40]: + - button "Open cart" [ref=e41]: + - img [ref=e42] + - generic [ref=e44]: Cart + - generic [ref=e45]: "1" + - dialog "Shopping cart" [ref=e48]: + - banner [ref=e49]: + - heading "Your cart" [level=2] [ref=e50] + - button "Close cart" [ref=e51]: + - img [ref=e52] + - list [ref=e55]: + - listitem [ref=e56]: + - generic [ref=e58]: + - paragraph [ref=e59]: Demo Shirt + - paragraph [ref=e60]: Qty 1 + - paragraph [ref=e61]: EUR 29.99 + - contentinfo [ref=e62]: + - generic [ref=e63]: + - generic [ref=e64]: Subtotal + - generic [ref=e65]: EUR 29.99 + - generic [ref=e66]: + - link "View cart" [ref=e67] [cursor=pointer]: + - /url: http://shop.test/cart + - link "Checkout" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/checkout \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-34-39-853Z.yml b/.playwright-mcp/page-2026-04-12T19-34-39-853Z.yml new file mode 100644 index 00000000..b11734ed --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-34-39-853Z.yml @@ -0,0 +1,93 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Contact and shipping address" [level=2] [ref=e34] + - generic [ref=e35]: + - generic [ref=e36]: + - generic [ref=e37]: Email + - textbox "Email" [ref=e38] + - generic [ref=e39]: + - generic [ref=e40]: + - generic [ref=e41]: First name + - textbox "First name" [ref=e42] + - generic [ref=e43]: + - generic [ref=e44]: Last name + - textbox "Last name" [ref=e45] + - generic [ref=e46]: + - generic [ref=e47]: Address + - textbox "Address" [ref=e48] + - generic [ref=e49]: + - generic [ref=e50]: Apartment, suite (optional) + - textbox "Apartment, suite (optional)" [ref=e51] + - generic [ref=e52]: + - generic [ref=e53]: + - generic [ref=e54]: City + - textbox "City" [ref=e55] + - generic [ref=e56]: + - generic [ref=e57]: Postal code + - textbox "Postal code" [ref=e58] + - generic [ref=e59]: + - generic [ref=e60]: Country + - combobox "Country" [ref=e61]: + - option "Germany" [selected] + - option "Austria" + - option "Switzerland" + - option "France" + - option "Netherlands" + - option "United States" + - option "United Kingdom" + - generic [ref=e62]: + - checkbox "Billing address is the same as shipping" [checked] [ref=e63] + - generic [ref=e64]: Billing address is the same as shipping + - button "Continue to shipping" [ref=e65] + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Demo Shirt + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 29.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 29.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 0.00 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 29.99 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-35-00-685Z.yml b/.playwright-mcp/page-2026-04-12T19-35-00-685Z.yml new file mode 100644 index 00000000..6e73047c --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-35-00-685Z.yml @@ -0,0 +1,63 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Shipping method" [level=2] [ref=e96] + - generic [ref=e98] [cursor=pointer]: + - generic [ref=e99]: + - radio "Standard EUR 5.99" [ref=e100] + - generic [ref=e101]: Standard + - generic [ref=e102]: EUR 5.99 + - generic [ref=e103]: + - button "Back" [ref=e104] + - button "Continue to payment" [ref=e105] + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Demo Shirt + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 29.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 29.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 0.00 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 29.99 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-35-08-184Z.yml b/.playwright-mcp/page-2026-04-12T19-35-08-184Z.yml new file mode 100644 index 00000000..1b0ebf43 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-35-08-184Z.yml @@ -0,0 +1,63 @@ +- generic [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Shipping method" [level=2] [ref=e96] + - generic [ref=e98] [cursor=pointer]: + - generic [ref=e99]: + - radio "Standard EUR 5.99" [checked] [active] [ref=e100] + - generic [ref=e101]: Standard + - generic [ref=e102]: EUR 5.99 + - generic [ref=e103]: + - button "Back" [ref=e104] + - button "Continue to payment" [ref=e105] + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Demo Shirt + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 29.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 29.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 0.00 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 29.99 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-35-12-839Z.yml b/.playwright-mcp/page-2026-04-12T19-35-12-839Z.yml new file mode 100644 index 00000000..a47e7e85 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-35-12-839Z.yml @@ -0,0 +1,80 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Payment" [level=2] [ref=e106] + - generic [ref=e107]: + - generic [ref=e108]: + - radio "Credit card" [checked] [ref=e109] + - generic [ref=e110]: Credit card + - generic [ref=e111]: + - radio "PayPal" [ref=e112] + - generic [ref=e113]: PayPal + - generic [ref=e114]: + - radio "Bank transfer" [ref=e115] + - generic [ref=e116]: Bank transfer + - generic [ref=e117]: "Magic test card: 4242 4242 4242 4242, expiry 12/30, CVC 123." + - generic [ref=e118]: + - generic [ref=e119]: Card number + - textbox "Card number" [ref=e120]: "4242424242424242" + - generic [ref=e121]: + - generic [ref=e122]: + - generic [ref=e123]: Expiry (MM/YY) + - textbox "Expiry (MM/YY)" [ref=e124]: 12/30 + - generic [ref=e125]: + - generic [ref=e126]: CVC + - textbox "CVC" [ref=e127]: "123" + - generic [ref=e128]: + - button "Back" [ref=e129] + - button "Place order" [ref=e130]: + - generic [ref=e131]: Place order + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Demo Shirt + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 29.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 29.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 5.99 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 35.98 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-35-20-454Z.yml b/.playwright-mcp/page-2026-04-12T19-35-20-454Z.yml new file mode 100644 index 00000000..01f4e36b --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-35-20-454Z.yml @@ -0,0 +1,60 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - img [ref=e19] + - heading "Thank you for your order" [level=1] [ref=e21] + - paragraph [ref=e22]: "Order #1001 is confirmed. A receipt has been sent to buyer@example.com." + - generic [ref=e23]: + - generic [ref=e24]: + - heading "Order details" [level=2] [ref=e25] + - list [ref=e26]: + - listitem [ref=e27]: + - generic [ref=e30]: + - paragraph [ref=e31]: Demo Shirt + - paragraph [ref=e32]: Qty 1 + - paragraph [ref=e33]: EUR 29.99 + - generic [ref=e34]: + - generic [ref=e35]: + - generic [ref=e36]: Subtotal + - generic [ref=e37]: EUR 29.99 + - generic [ref=e38]: + - generic [ref=e39]: Shipping + - generic [ref=e40]: EUR 5.99 + - generic [ref=e41]: + - generic [ref=e42]: Total + - generic [ref=e43]: EUR 35.98 + - complementary [ref=e44]: + - generic [ref=e45]: + - heading "Shipping address" [level=3] [ref=e46] + - generic [ref=e47]: + - text: Jane Doe + - text: Alexanderplatz 1 + - text: Berlin 10178 + - text: DE + - generic [ref=e48]: + - heading "Status" [level=3] [ref=e49] + - paragraph [ref=e50]: "Payment: paid" + - paragraph [ref=e51]: "Fulfillment: unfulfilled" + - link "Continue shopping" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/storefront + - contentinfo [ref=e53]: + - generic [ref=e55]: + - navigation "Footer navigation" + - paragraph [ref=e56]: (c) Shop + - button "Open cart" [ref=e59]: + - img [ref=e60] + - generic [ref=e62]: Cart + - generic [ref=e63]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-35-29-561Z.yml b/.playwright-mcp/page-2026-04-12T19-35-29-561Z.yml new file mode 100644 index 00000000..bf7a34bb --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-35-29-561Z.yml @@ -0,0 +1,47 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - heading "Create account" [level=1] [ref=e18] + - paragraph [ref=e19]: Sign up to track orders and check out faster. + - generic [ref=e20]: + - generic [ref=e21]: + - generic [ref=e22]: Name + - textbox "Name" [ref=e23] + - generic [ref=e24]: + - generic [ref=e25]: Email + - textbox "Email" [ref=e26] + - generic [ref=e27]: + - generic [ref=e28]: Password + - textbox "Password" [ref=e29] + - generic [ref=e30]: + - generic [ref=e31]: Confirm password + - textbox "Confirm password" [ref=e32] + - generic [ref=e33]: + - checkbox "Send me product news and offers" [ref=e34] + - generic [ref=e35]: Send me product news and offers + - button "Create account" [ref=e36] + - paragraph [ref=e37]: + - text: Already have an account? + - link "Sign in" [ref=e38] [cursor=pointer]: + - /url: http://shop.test/account/login + - contentinfo [ref=e39]: + - generic [ref=e41]: + - navigation "Footer navigation" + - paragraph [ref=e42]: (c) Shop + - button "Open cart" [ref=e45]: + - img [ref=e46] + - generic [ref=e48]: Cart + - generic [ref=e49]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-37-32-440Z.yml b/.playwright-mcp/page-2026-04-12T19-37-32-440Z.yml new file mode 100644 index 00000000..bf7a34bb --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-37-32-440Z.yml @@ -0,0 +1,47 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - heading "Create account" [level=1] [ref=e18] + - paragraph [ref=e19]: Sign up to track orders and check out faster. + - generic [ref=e20]: + - generic [ref=e21]: + - generic [ref=e22]: Name + - textbox "Name" [ref=e23] + - generic [ref=e24]: + - generic [ref=e25]: Email + - textbox "Email" [ref=e26] + - generic [ref=e27]: + - generic [ref=e28]: Password + - textbox "Password" [ref=e29] + - generic [ref=e30]: + - generic [ref=e31]: Confirm password + - textbox "Confirm password" [ref=e32] + - generic [ref=e33]: + - checkbox "Send me product news and offers" [ref=e34] + - generic [ref=e35]: Send me product news and offers + - button "Create account" [ref=e36] + - paragraph [ref=e37]: + - text: Already have an account? + - link "Sign in" [ref=e38] [cursor=pointer]: + - /url: http://shop.test/account/login + - contentinfo [ref=e39]: + - generic [ref=e41]: + - navigation "Footer navigation" + - paragraph [ref=e42]: (c) Shop + - button "Open cart" [ref=e45]: + - img [ref=e46] + - generic [ref=e48]: Cart + - generic [ref=e49]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-37-37-107Z.yml b/.playwright-mcp/page-2026-04-12T19-37-37-107Z.yml new file mode 100644 index 00000000..b3428855 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-37-37-107Z.yml @@ -0,0 +1,41 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - heading "Sign in" [level=1] [ref=e18] + - paragraph [ref=e19]: Access your orders and saved addresses. + - generic [ref=e20]: + - generic [ref=e21]: + - generic [ref=e22]: Email + - textbox "Email" [ref=e23] + - generic [ref=e24]: + - generic [ref=e25]: Password + - textbox "Password" [ref=e26] + - generic [ref=e27]: + - checkbox "Remember me" [ref=e28] + - generic [ref=e29]: Remember me + - button "Sign in" [ref=e30] + - paragraph [ref=e31]: + - text: New customer? + - link "Create an account" [ref=e32] [cursor=pointer]: + - /url: http://shop.test/account/register + - contentinfo [ref=e33]: + - generic [ref=e35]: + - navigation "Footer navigation" + - paragraph [ref=e36]: (c) Shop + - button "Open cart" [ref=e39]: + - img [ref=e40] + - generic [ref=e42]: Cart + - generic [ref=e43]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-37-39-481Z.yml b/.playwright-mcp/page-2026-04-12T19-37-39-481Z.yml new file mode 100644 index 00000000..b3428855 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-37-39-481Z.yml @@ -0,0 +1,41 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - heading "Sign in" [level=1] [ref=e18] + - paragraph [ref=e19]: Access your orders and saved addresses. + - generic [ref=e20]: + - generic [ref=e21]: + - generic [ref=e22]: Email + - textbox "Email" [ref=e23] + - generic [ref=e24]: + - generic [ref=e25]: Password + - textbox "Password" [ref=e26] + - generic [ref=e27]: + - checkbox "Remember me" [ref=e28] + - generic [ref=e29]: Remember me + - button "Sign in" [ref=e30] + - paragraph [ref=e31]: + - text: New customer? + - link "Create an account" [ref=e32] [cursor=pointer]: + - /url: http://shop.test/account/register + - contentinfo [ref=e33]: + - generic [ref=e35]: + - navigation "Footer navigation" + - paragraph [ref=e36]: (c) Shop + - button "Open cart" [ref=e39]: + - img [ref=e40] + - generic [ref=e42]: Cart + - generic [ref=e43]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-50-11-405Z.yml b/.playwright-mcp/page-2026-04-12T19-50-11-405Z.yml new file mode 100644 index 00000000..99bbc029 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-50-11-405Z.yml @@ -0,0 +1,17 @@ +- generic [ref=e6]: + - generic [ref=e7]: + - heading "Admin sign in" [level=1] [ref=e8] + - paragraph [ref=e9]: Enter your email and password to access the admin panel + - generic [ref=e10]: + - generic [ref=e11]: + - generic [ref=e12]: Email address + - textbox "Email address" [active] [ref=e14] + - generic [ref=e15]: + - generic [ref=e16]: Password + - textbox "Password" [ref=e18] + - generic [ref=e19]: + - checkbox "Remember me" [ref=e20] + - generic [ref=e22]: Remember me + - button "Log in" [ref=e23]: + - img [ref=e25] + - generic [ref=e28]: Log in \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-50-22-913Z.yml b/.playwright-mcp/page-2026-04-12T19-50-22-913Z.yml new file mode 100644 index 00000000..b3428855 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-50-22-913Z.yml @@ -0,0 +1,41 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - heading "Sign in" [level=1] [ref=e18] + - paragraph [ref=e19]: Access your orders and saved addresses. + - generic [ref=e20]: + - generic [ref=e21]: + - generic [ref=e22]: Email + - textbox "Email" [ref=e23] + - generic [ref=e24]: + - generic [ref=e25]: Password + - textbox "Password" [ref=e26] + - generic [ref=e27]: + - checkbox "Remember me" [ref=e28] + - generic [ref=e29]: Remember me + - button "Sign in" [ref=e30] + - paragraph [ref=e31]: + - text: New customer? + - link "Create an account" [ref=e32] [cursor=pointer]: + - /url: http://shop.test/account/register + - contentinfo [ref=e33]: + - generic [ref=e35]: + - navigation "Footer navigation" + - paragraph [ref=e36]: (c) Shop + - button "Open cart" [ref=e39]: + - img [ref=e40] + - generic [ref=e42]: Cart + - generic [ref=e43]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-50-43-197Z.yml b/.playwright-mcp/page-2026-04-12T19-50-43-197Z.yml new file mode 100644 index 00000000..3dfc1d08 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-50-43-197Z.yml @@ -0,0 +1,32 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Laravel Starter Kit" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/dashboard + - img [ref=e6] + - generic [ref=e8]: Laravel Starter Kit + - navigation [ref=e9]: + - generic [ref=e10]: + - generic [ref=e12]: Platform + - link "Dashboard" [ref=e15] [cursor=pointer]: + - /url: http://shop.test/dashboard + - img [ref=e17] + - generic [ref=e19]: Dashboard + - navigation [ref=e21]: + - link "Repository" [ref=e23] [cursor=pointer]: + - /url: https://github.com/laravel/livewire-starter-kit + - img [ref=e25] + - generic [ref=e30]: Repository + - link "Documentation" [ref=e32] [cursor=pointer]: + - /url: https://laravel.com/docs/starter-kits#livewire + - img [ref=e34] + - generic [ref=e36]: Documentation + - button "TU Test User" [ref=e38]: + - generic [ref=e41]: TU + - generic [ref=e42]: Test User + - img [ref=e44] + - generic [ref=e48]: + - generic [ref=e49]: + - img [ref=e51] + - img [ref=e54] + - img [ref=e57] + - img [ref=e60] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-50-46-885Z.yml b/.playwright-mcp/page-2026-04-12T19-50-46-885Z.yml new file mode 100644 index 00000000..d40d64c7 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-50-46-885Z.yml @@ -0,0 +1,112 @@ +- generic [ref=e2]: + - generic [ref=e4]: + - generic [ref=e5]: + - img [ref=e7] + - generic [ref=e10]: Method Not Allowed + - button "Copy as Markdown" [ref=e11] [cursor=pointer]: + - img [ref=e12] + - generic [ref=e15]: Copy as Markdown + - generic [ref=e18]: + - generic [ref=e19]: + - heading "Symfony\\Component\\HttpKernel\\Exception\\MethodNotAllowedHttpException" [level=1] [ref=e20] + - generic [ref=e22]: vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php:131 + - paragraph [ref=e23]: "The GET method is not supported for route logout. Supported methods: POST." + - generic [ref=e24]: + - generic [ref=e25]: + - generic [ref=e26]: + - generic [ref=e27]: LARAVEL + - generic [ref=e28]: 12.51.0 + - generic [ref=e29]: + - generic [ref=e30]: PHP + - generic [ref=e31]: 8.4.17 + - generic [ref=e32]: + - img [ref=e33] + - text: UNHANDLED + - generic [ref=e36]: CODE 0 + - generic [ref=e38]: + - generic [ref=e39]: + - img [ref=e40] + - text: "405" + - generic [ref=e43]: + - img [ref=e44] + - text: GET + - generic [ref=e47]: http://shop.test/logout + - button [ref=e48] [cursor=pointer]: + - img [ref=e49] + - generic [ref=e53]: + - generic [ref=e54]: + - generic [ref=e55]: + - img [ref=e57] + - heading "Exception trace" [level=3] [ref=e60] + - generic [ref=e61]: + - generic [ref=e63] [cursor=pointer]: + - img [ref=e64] + - generic [ref=e68]: 33 vendor frames + - button [ref=e69]: + - img [ref=e70] + - generic [ref=e74]: + - generic [ref=e75] [cursor=pointer]: + - generic [ref=e78]: + - code [ref=e82]: + - generic [ref=e83]: public/index.php + - generic [ref=e85]: public/index.php:20 + - button [ref=e87]: + - img [ref=e88] + - code [ref=e96]: + - generic [ref=e97]: "15" + - generic [ref=e98]: 16// Bootstrap Laravel and handle the request... + - generic [ref=e99]: 17/** @var Application $app */ + - generic [ref=e100]: 18$app = require_once __DIR__.'/../bootstrap/app.php'; + - generic [ref=e101]: "19" + - generic [ref=e102]: 20$app->handleRequest(Request::capture()); + - generic [ref=e103]: "21" + - generic [ref=e105] [cursor=pointer]: + - img [ref=e106] + - generic [ref=e110]: 1 vendor frame + - button [ref=e111]: + - img [ref=e112] + - generic [ref=e116]: + - generic [ref=e118]: + - img [ref=e120] + - heading "Queries" [level=3] [ref=e122] + - generic [ref=e124]: // No queries executed + - generic [ref=e126]: + - generic [ref=e127]: + - heading "Headers" [level=2] [ref=e128] + - generic [ref=e129]: + - generic [ref=e130]: + - generic [ref=e131]: cookie + - generic [ref=e133]: XSRF-TOKEN=eyJpdiI6IjhjMENyZVlaMmtCa1paZ3FoS0pYOFE9PSIsInZhbHVlIjoiTWJNTHJoK0xmVHlISHlMUmdPTllwcWd3ZC83VXdtQ3BsQ0g1cVoxZnczTndDUXVxNjdiZTgzMnJDLzgxOFJVcmowb3dtVkc5UWVmSVpIL2V3VVU4NkJCazl5ZXVTUEk4bXR6SHo5T3Q3Z3RiczN3M2FoanNJODVCdHFjWjhmY0oiLCJtYWMiOiJiZjUyNmI4MTVhY2Q2ODVkYzQ5ZmZkYTk3NDBjYTk3NDE1MGRlZDFmYTcyYzcxZTA4MjI2OGE1OWJmZjk4NDJjIiwidGFnIjoiIn0%3D; shop_session=eyJpdiI6IjFGOWxRdlVkWEJMbkRuc0FoeUdYOFE9PSIsInZhbHVlIjoiSWREbzZaOWdLV0lIVjVWeGFTV2FzYjl3UmoraVo4Q1JrQjdicGMxaEJRRUxEbnVGQnp4aW5pQmRoRXMvSDRaMTFsbDhFQ2NlYzNKemgxM2lFeGJFcXZERmt0YjZTS2NlUkxJUG9uOHJUYm9nWUlkWUk3aG1mR3JrVitRSlNkYkQiLCJtYWMiOiIxMzdlYWI3YmZlYmM2NmE4Y2Q5NjBhOTNjOTJiZThhMWU2OWYyYmI1NDk5YTE2NDk5NTZhY2Y4NzRlM2U5YjdlIiwidGFnIjoiIn0%3D + - generic [ref=e134]: + - generic [ref=e135]: accept-language + - generic [ref=e137]: en-GB,en-US;q=0.9,en;q=0.8 + - generic [ref=e138]: + - generic [ref=e139]: accept-encoding + - generic [ref=e141]: gzip, deflate + - generic [ref=e142]: + - generic [ref=e143]: accept + - generic [ref=e145]: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 + - generic [ref=e146]: + - generic [ref=e147]: user-agent + - generic [ref=e149]: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 + - generic [ref=e150]: + - generic [ref=e151]: upgrade-insecure-requests + - generic [ref=e153]: "1" + - generic [ref=e154]: + - generic [ref=e155]: connection + - generic [ref=e157]: keep-alive + - generic [ref=e158]: + - generic [ref=e159]: host + - generic [ref=e161]: shop.test + - generic [ref=e162]: + - heading "Body" [level=2] [ref=e163] + - generic [ref=e164]: // No request body + - generic [ref=e165]: + - heading "Routing" [level=2] [ref=e166] + - generic [ref=e168]: // No routing context + - generic [ref=e169]: + - heading "Routing parameters" [level=2] [ref=e170] + - generic [ref=e171]: // No routing parameters + - generic [ref=e174]: + - img [ref=e176] + - img [ref=e3214] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-51-05-269Z.yml b/.playwright-mcp/page-2026-04-12T19-51-05-269Z.yml new file mode 100644 index 00000000..0b706ccd --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-51-05-269Z.yml @@ -0,0 +1,87 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Nolan Inc Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - button "TU Test User" [ref=e59]: + - generic [ref=e62]: TU + - generic [ref=e63]: Test User + - img [ref=e65] + - generic [ref=e69]: + - generic [ref=e70]: + - generic [ref=e71]: Dashboard + - combobox [ref=e72]: + - option "Last 7 days" + - option "Last 30 days" [selected] + - option "Last 90 days" + - generic [ref=e73]: + - generic [ref=e74]: + - generic [ref=e75]: Total sales + - generic [ref=e76]: "35.98" + - generic [ref=e77]: + - generic [ref=e78]: Orders + - generic [ref=e79]: "1" + - generic [ref=e80]: + - generic [ref=e81]: Average order value + - generic [ref=e82]: "35.98" + - generic [ref=e83]: + - generic [ref=e84]: Conversion rate + - generic [ref=e85]: N/A + - generic [ref=e86]: + - generic [ref=e87]: Sales over time + - generic [ref=e88]: Charts coming soon + - generic [ref=e89]: + - generic [ref=e90]: Recent orders + - table [ref=e94]: + - rowgroup [ref=e95]: + - row "Order Customer Total Status" [ref=e96]: + - columnheader "Order" [ref=e97]: + - generic [ref=e98]: Order + - columnheader "Customer" [ref=e99]: + - generic [ref=e100]: Customer + - columnheader "Total" [ref=e101]: + - generic [ref=e102]: Total + - columnheader "Status" [ref=e103]: + - generic [ref=e104]: Status + - rowgroup [ref=e105]: + - row "#1001 buyer@example.com 35.98 EUR paid" [ref=e106]: + - cell "#1001" [ref=e107]: + - link "#1001" [ref=e108] [cursor=pointer]: + - /url: http://shop.test/admin/orders/1 + - cell "buyer@example.com" [ref=e109] + - cell "35.98 EUR" [ref=e110] + - cell "paid" [ref=e111]: + - generic [ref=e112]: paid \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-51-11-341Z.yml b/.playwright-mcp/page-2026-04-12T19-51-11-341Z.yml new file mode 100644 index 00000000..de631467 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-51-11-341Z.yml @@ -0,0 +1,83 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Nolan Inc Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - button "TU Test User" [ref=e59]: + - generic [ref=e62]: TU + - generic [ref=e63]: Test User + - img [ref=e65] + - generic [ref=e69]: + - generic [ref=e70]: + - generic [ref=e71]: Products + - link "New product" [ref=e72] [cursor=pointer]: + - /url: http://shop.test/admin/products/create + - img [ref=e73] + - generic [ref=e75]: New product + - generic [ref=e76]: + - generic [ref=e77]: + - generic: + - img + - textbox "Search products..." [ref=e78] + - combobox [ref=e80]: + - option "All statuses" [selected] + - option "Draft" + - option "Active" + - option "Archived" + - table [ref=e84]: + - rowgroup [ref=e85]: + - row "Title Status Vendor Variants" [ref=e86]: + - columnheader "Title" [ref=e87]: + - generic [ref=e88]: Title + - columnheader "Status" [ref=e89]: + - generic [ref=e90]: Status + - columnheader "Vendor" [ref=e91]: + - generic [ref=e92]: Vendor + - columnheader "Variants" [ref=e93]: + - generic [ref=e94]: Variants + - columnheader [ref=e95] + - rowgroup [ref=e96]: + - row "Demo Shirt active Demo Co 1 Edit" [ref=e97]: + - cell "Demo Shirt" [ref=e98]: + - link "Demo Shirt" [ref=e99] [cursor=pointer]: + - /url: http://shop.test/admin/products/1/edit + - cell "active" [ref=e100]: + - generic [ref=e101]: active + - cell "Demo Co" [ref=e102] + - cell "1" [ref=e103] + - cell "Edit" [ref=e104]: + - link "Edit" [ref=e105] [cursor=pointer]: + - /url: http://shop.test/admin/products/1/edit \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-51-19-748Z.yml b/.playwright-mcp/page-2026-04-12T19-51-19-748Z.yml new file mode 100644 index 00000000..da1a9612 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-51-19-748Z.yml @@ -0,0 +1,105 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Nolan Inc Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - button "TU Test User" [ref=e59]: + - generic [ref=e62]: TU + - generic [ref=e63]: Test User + - img [ref=e65] + - generic [ref=e69]: + - generic [ref=e70]: + - generic [ref=e71]: + - generic [ref=e72]: "Order #1001" + - generic [ref=e73]: + - generic [ref=e74]: paid + - generic [ref=e75]: unfulfilled + - generic [ref=e76]: Apr 12, 2026 19:35 + - generic [ref=e77]: + - button "Fulfill items" [ref=e78]: + - img [ref=e80] + - generic [ref=e83]: Fulfill items + - button "Refund" [ref=e84]: + - img [ref=e86] + - generic [ref=e89]: Refund + - generic [ref=e90]: + - generic [ref=e91]: + - generic [ref=e92]: + - generic [ref=e93]: Items + - table [ref=e96]: + - rowgroup [ref=e97]: + - row "Product SKU Qty Total" [ref=e98]: + - columnheader "Product" [ref=e99]: + - generic [ref=e100]: Product + - columnheader "SKU" [ref=e101]: + - generic [ref=e102]: SKU + - columnheader "Qty" [ref=e103]: + - generic [ref=e104]: Qty + - columnheader "Total" [ref=e105]: + - generic [ref=e106]: Total + - rowgroup [ref=e107]: + - row "Demo Shirt DEMO-001 1 29.99 EUR" [ref=e108]: + - cell "Demo Shirt" [ref=e109] + - cell "DEMO-001" [ref=e110] + - cell "1" [ref=e111] + - cell "29.99 EUR" [ref=e112] + - generic [ref=e113]: + - generic [ref=e114]: + - generic [ref=e115]: Subtotal + - generic [ref=e116]: "29.99" + - generic [ref=e117]: + - generic [ref=e118]: Shipping + - generic [ref=e119]: "5.99" + - generic [ref=e120]: + - generic [ref=e121]: Tax + - generic [ref=e122]: "0.00" + - generic [ref=e123]: + - generic [ref=e124]: Total + - generic [ref=e125]: "35.98" + - generic [ref=e126]: + - generic [ref=e127]: Fulfillments + - paragraph [ref=e128]: No fulfillments yet. + - generic [ref=e129]: + - generic [ref=e130]: + - generic [ref=e131]: Customer + - generic [ref=e132]: + - generic [ref=e133]: buyer@example.com + - generic [ref=e134]: buyer@example.com + - generic [ref=e135]: + - generic [ref=e136]: Payment + - generic [ref=e137]: + - generic [ref=e138]: "Method: credit_card" + - generic [ref=e139]: "Status: paid" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-51-22-195Z.yml b/.playwright-mcp/page-2026-04-12T19-51-22-195Z.yml new file mode 100644 index 00000000..34629e56 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-51-22-195Z.yml @@ -0,0 +1,100 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Nolan Inc Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - button "TU Test User" [ref=e59]: + - generic [ref=e62]: TU + - generic [ref=e63]: Test User + - img [ref=e65] + - generic [ref=e69]: + - generic [ref=e70]: + - generic [ref=e71]: New product + - link "Back" [ref=e72] [cursor=pointer]: + - /url: http://shop.test/admin/products + - generic [ref=e73]: + - generic [ref=e74]: + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Title + - textbox "Title" [ref=e79]: + - /placeholder: Short sleeve t-shirt + - generic [ref=e81]: + - generic [ref=e82]: Handle + - textbox "Handle" [ref=e84]: + - /placeholder: short-sleeve-t-shirt + - generic [ref=e86]: + - generic [ref=e87]: Description + - textbox "Description" [ref=e88] + - generic [ref=e89]: + - generic [ref=e90]: Pricing & Inventory + - generic [ref=e91]: + - generic [ref=e92]: + - generic [ref=e93]: Price (cents) + - spinbutton "Price (cents)" [ref=e95]: "0" + - generic [ref=e96]: + - generic [ref=e97]: SKU + - textbox "SKU" [ref=e99] + - generic [ref=e100]: + - generic [ref=e101]: Inventory + - spinbutton "Inventory" [ref=e103]: "0" + - generic [ref=e104]: + - generic [ref=e105]: + - generic [ref=e106]: Status + - generic [ref=e108]: + - generic [ref=e109]: Status + - combobox "Status" [ref=e110]: + - option "Draft" [selected] + - option "Active" + - option "Archived" + - generic [ref=e111]: + - generic [ref=e112]: Organization + - generic [ref=e113]: + - generic [ref=e114]: + - generic [ref=e115]: Vendor + - textbox "Vendor" [ref=e117] + - generic [ref=e118]: + - generic [ref=e119]: Product type + - textbox "Product type" [ref=e121] + - generic [ref=e122]: + - generic [ref=e123]: Tags (comma separated) + - textbox "Tags (comma separated)" [ref=e125] + - generic [ref=e126]: + - link "Cancel" [ref=e127] [cursor=pointer]: + - /url: http://shop.test/admin/products + - button "Save product" [ref=e128]: + - img [ref=e130] + - generic [ref=e133]: Save product \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T19-51-26-178Z.yml b/.playwright-mcp/page-2026-04-12T19-51-26-178Z.yml new file mode 100644 index 00000000..f969e4be --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T19-51-26-178Z.yml @@ -0,0 +1,101 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Nolan Inc Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Nolan Inc Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - button "TU Test User" [ref=e59]: + - generic [ref=e62]: TU + - generic [ref=e63]: Test User + - img [ref=e65] + - generic [ref=e69]: + - generic [ref=e70]: + - generic [ref=e71]: New discount + - link "Back" [ref=e72] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - generic [ref=e73]: + - generic [ref=e74]: + - generic [ref=e75]: + - generic [ref=e76]: Details + - generic [ref=e77]: + - generic [ref=e78]: + - generic [ref=e79]: Type + - combobox "Type" [ref=e80]: + - option "Code" [selected] + - option "Automatic" + - generic [ref=e81]: + - generic [ref=e82]: Code + - textbox "Code" [ref=e84]: + - /placeholder: SAVE20 + - generic [ref=e85]: + - generic [ref=e86]: Value type + - combobox "Value type" [ref=e87]: + - option "Percentage" [selected] + - option "Fixed amount" + - option "Free shipping" + - generic [ref=e88]: + - generic [ref=e89]: Value (%) + - spinbutton "Value (%)" [ref=e91]: "0" + - generic [ref=e92]: + - generic [ref=e93]: Minimum purchase (cents) + - spinbutton "Minimum purchase (cents)" [ref=e95] + - generic [ref=e96]: + - generic [ref=e97]: Active dates + - generic [ref=e98]: + - generic [ref=e99]: + - generic [ref=e100]: Starts at + - textbox "Starts at" [ref=e102] + - generic [ref=e103]: + - generic [ref=e104]: Ends at + - textbox "Ends at" [ref=e106] + - generic [ref=e108]: + - generic [ref=e109]: Status + - generic [ref=e110]: + - generic [ref=e111]: + - generic [ref=e112]: Status + - combobox "Status" [ref=e113]: + - option "Draft" [selected] + - option "Active" + - option "Disabled" + - option "Expired" + - generic [ref=e114]: + - generic [ref=e115]: Usage limit + - spinbutton "Usage limit" [ref=e117] + - generic [ref=e118]: + - link "Cancel" [ref=e119] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - button "Save discount" [ref=e120]: + - img [ref=e122] + - generic [ref=e125]: Save discount \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-16-49-750Z.yml b/.playwright-mcp/page-2026-04-12T20-16-49-750Z.yml new file mode 100644 index 00000000..87220a4c --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-16-49-750Z.yml @@ -0,0 +1,32 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Laravel Starter Kit" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/dashboard + - img [ref=e6] + - generic [ref=e8]: Laravel Starter Kit + - navigation [ref=e9]: + - generic [ref=e10]: + - generic [ref=e12]: Platform + - link "Dashboard" [ref=e15] [cursor=pointer]: + - /url: http://shop.test/dashboard + - img [ref=e17] + - generic [ref=e19]: Dashboard + - navigation [ref=e21]: + - link "Repository" [ref=e23] [cursor=pointer]: + - /url: https://github.com/laravel/livewire-starter-kit + - img [ref=e25] + - generic [ref=e30]: Repository + - link "Documentation" [ref=e32] [cursor=pointer]: + - /url: https://laravel.com/docs/starter-kits#livewire + - img [ref=e34] + - generic [ref=e36]: Documentation + - button "SA Shop Admin" [ref=e38]: + - generic [ref=e41]: SA + - generic [ref=e42]: Shop Admin + - img [ref=e44] + - generic [ref=e48]: + - generic [ref=e49]: + - img [ref=e51] + - img [ref=e54] + - img [ref=e57] + - img [ref=e60] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-16-52-856Z.yml b/.playwright-mcp/page-2026-04-12T20-16-52-856Z.yml new file mode 100644 index 00000000..723ccd4f --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-16-52-856Z.yml @@ -0,0 +1,147 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Pages + - link "New page" [ref=e127] [cursor=pointer]: + - /url: http://shop.test/admin/pages/create + - img [ref=e128] + - generic [ref=e130]: New page + - generic [ref=e131]: + - generic: + - img + - textbox "Search pages..." [ref=e132] + - table [ref=e137]: + - rowgroup [ref=e138]: + - row "Title Handle Status Updated" [ref=e139]: + - columnheader "Title" [ref=e140]: + - generic [ref=e141]: Title + - columnheader "Handle" [ref=e142]: + - generic [ref=e143]: Handle + - columnheader "Status" [ref=e144]: + - generic [ref=e145]: Status + - columnheader "Updated" [ref=e146]: + - generic [ref=e147]: Updated + - columnheader [ref=e148] + - rowgroup [ref=e149]: + - row "About Us about-us published 1 minute ago Edit Delete" [ref=e150]: + - cell "About Us" [ref=e151]: + - link "About Us" [ref=e152] [cursor=pointer]: + - /url: http://shop.test/admin/pages/1/edit + - cell "about-us" [ref=e153] + - cell "published" [ref=e154]: + - generic [ref=e155]: published + - cell "1 minute ago" [ref=e156] + - cell "Edit Delete" [ref=e157]: + - generic [ref=e158]: + - link "Edit" [ref=e159] [cursor=pointer]: + - /url: http://shop.test/admin/pages/1/edit + - button "Delete" [ref=e160]: + - img [ref=e162] + - generic [ref=e165]: Delete + - row "Contact contact published 1 minute ago Edit Delete" [ref=e166]: + - cell "Contact" [ref=e167]: + - link "Contact" [ref=e168] [cursor=pointer]: + - /url: http://shop.test/admin/pages/2/edit + - cell "contact" [ref=e169] + - cell "published" [ref=e170]: + - generic [ref=e171]: published + - cell "1 minute ago" [ref=e172] + - cell "Edit Delete" [ref=e173]: + - generic [ref=e174]: + - link "Edit" [ref=e175] [cursor=pointer]: + - /url: http://shop.test/admin/pages/2/edit + - button "Delete" [ref=e176]: + - img [ref=e178] + - generic [ref=e181]: Delete + - row "FAQ faq published 1 minute ago Edit Delete" [ref=e182]: + - cell "FAQ" [ref=e183]: + - link "FAQ" [ref=e184] [cursor=pointer]: + - /url: http://shop.test/admin/pages/3/edit + - cell "faq" [ref=e185] + - cell "published" [ref=e186]: + - generic [ref=e187]: published + - cell "1 minute ago" [ref=e188] + - cell "Edit Delete" [ref=e189]: + - generic [ref=e190]: + - link "Edit" [ref=e191] [cursor=pointer]: + - /url: http://shop.test/admin/pages/3/edit + - button "Delete" [ref=e192]: + - img [ref=e194] + - generic [ref=e197]: Delete \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-16-59-264Z.yml b/.playwright-mcp/page-2026-04-12T20-16-59-264Z.yml new file mode 100644 index 00000000..2b969d83 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-16-59-264Z.yml @@ -0,0 +1,106 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: Settings + - generic [ref=e126]: + - button "General" [ref=e127] + - link "Shipping" [ref=e128] [cursor=pointer]: + - /url: http://shop.test/admin/settings/shipping + - link "Taxes" [ref=e129] [cursor=pointer]: + - /url: http://shop.test/admin/settings/taxes + - generic [ref=e130]: + - generic [ref=e131]: + - generic [ref=e132]: Store name + - textbox "Store name" [ref=e134]: Demo Store + - generic [ref=e135]: + - generic [ref=e136]: + - generic [ref=e137]: Currency + - textbox "Currency" [ref=e139]: EUR + - generic [ref=e140]: + - generic [ref=e141]: Locale + - textbox "Locale" [ref=e143]: en + - generic [ref=e144]: + - generic [ref=e145]: Timezone + - textbox "Timezone" [ref=e147]: Europe/Berlin + - button "Save settings" [ref=e149]: + - img [ref=e151] + - generic [ref=e154]: Save settings + - generic [ref=e155]: + - generic [ref=e156]: Notifications + - paragraph [ref=e157]: Notification channel configuration is coming soon. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-17-01-819Z.yml b/.playwright-mcp/page-2026-04-12T20-17-01-819Z.yml new file mode 100644 index 00000000..71fba4a2 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-17-01-819Z.yml @@ -0,0 +1,104 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Analytics + - generic [ref=e127]: + - generic [ref=e128]: + - generic [ref=e129]: Start date + - textbox "Start date" [ref=e131]: 2026-03-14 + - generic [ref=e133]: + - generic [ref=e134]: End date + - textbox "End date" [ref=e136]: 2026-04-12 + - generic [ref=e138]: + - generic [ref=e139]: + - paragraph [ref=e140]: Revenue + - paragraph [ref=e141]: 0.00 EUR + - generic [ref=e142]: + - paragraph [ref=e143]: Orders + - paragraph [ref=e144]: "0" + - generic [ref=e145]: + - paragraph [ref=e146]: AOV + - paragraph [ref=e147]: 0.00 EUR + - generic [ref=e148]: + - paragraph [ref=e149]: Visits + - paragraph [ref=e150]: "0" + - generic [ref=e151]: + - generic [ref=e153]: Daily breakdown + - generic [ref=e154]: No data for this range. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-17-04-313Z.yml b/.playwright-mcp/page-2026-04-12T20-17-04-313Z.yml new file mode 100644 index 00000000..75998fca --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-17-04-313Z.yml @@ -0,0 +1,110 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: Developers + - generic [ref=e126]: + - generic [ref=e127]: API tokens + - paragraph [ref=e128]: Personal access tokens for the Admin API. + - generic [ref=e129]: + - generic [ref=e130]: + - generic [ref=e131]: Token name + - textbox "Token name" [ref=e133]: + - /placeholder: My integration + - button "Create token" [ref=e134]: + - img [ref=e136] + - img [ref=e139] + - generic [ref=e141]: Create token + - paragraph [ref=e143]: No tokens yet. + - generic [ref=e144]: + - generic [ref=e145]: Webhook subscriptions + - paragraph [ref=e146]: HTTP endpoints notified when events occur. + - generic [ref=e147]: + - generic [ref=e148]: + - generic [ref=e149]: Event type + - textbox "Event type" [ref=e151]: + - /placeholder: order.placed + - generic [ref=e152]: + - generic [ref=e153]: URL + - textbox "URL" [ref=e155]: + - /placeholder: https://example.com/webhook + - button "Add webhook" [ref=e157]: + - img [ref=e159] + - img [ref=e162] + - generic [ref=e164]: Add webhook + - paragraph [ref=e166]: No webhooks yet. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-17-06-775Z.yml b/.playwright-mcp/page-2026-04-12T20-17-06-775Z.yml new file mode 100644 index 00000000..10d27131 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-17-06-775Z.yml @@ -0,0 +1,153 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e126]: Navigation + - generic [ref=e127]: + - generic [ref=e128]: Create menu + - generic [ref=e129]: + - generic [ref=e130]: + - generic [ref=e131]: Title + - textbox "Title" [ref=e133]: + - /placeholder: Main menu + - button "Add menu" [ref=e134]: + - img [ref=e136] + - img [ref=e139] + - generic [ref=e141]: Add menu + - generic [ref=e143]: + - generic [ref=e144]: + - generic [ref=e145]: + - generic [ref=e146]: Main Menu + - paragraph [ref=e147]: main-menu + - generic [ref=e148]: + - button "Add item" [ref=e149]: + - img [ref=e151] + - img [ref=e154] + - generic [ref=e156]: Add item + - button "Delete" [ref=e157]: + - img [ref=e159] + - generic [ref=e162]: Delete + - list [ref=e164]: + - listitem [ref=e165]: + - generic [ref=e166]: Home (link) + - generic [ref=e167]: + - button [ref=e168]: + - img [ref=e170] + - img [ref=e173] + - button [ref=e175]: + - img [ref=e177] + - img [ref=e180] + - button "Remove" [ref=e182]: + - img [ref=e184] + - generic [ref=e187]: Remove + - listitem [ref=e188]: + - generic [ref=e189]: Collections (link) + - generic [ref=e190]: + - button [ref=e191]: + - img [ref=e193] + - img [ref=e196] + - button [ref=e198]: + - img [ref=e200] + - img [ref=e203] + - button "Remove" [ref=e205]: + - img [ref=e207] + - generic [ref=e210]: Remove + - listitem [ref=e211]: + - generic [ref=e212]: About (link) + - generic [ref=e213]: + - button [ref=e214]: + - img [ref=e216] + - img [ref=e219] + - button [ref=e221]: + - img [ref=e223] + - img [ref=e226] + - button "Remove" [ref=e228]: + - img [ref=e230] + - generic [ref=e233]: Remove + - listitem [ref=e234]: + - generic [ref=e235]: Contact (link) + - generic [ref=e236]: + - button [ref=e237]: + - img [ref=e239] + - img [ref=e242] + - button [ref=e244]: + - img [ref=e246] + - img [ref=e249] + - button "Remove" [ref=e251]: + - img [ref=e253] + - generic [ref=e256]: Remove \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-20-44-557Z.yml b/.playwright-mcp/page-2026-04-12T20-20-44-557Z.yml new file mode 100644 index 00000000..db185f62 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-20-44-557Z.yml @@ -0,0 +1,82 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - paragraph [ref=e18]: New season + - heading "Thoughtfully made, honestly priced." [level=1] [ref=e19] + - paragraph [ref=e20]: A curated collection of timeless goods designed to last. Explore our latest arrivals and find something you will love. + - generic [ref=e21]: + - link "Shop the collection" [ref=e22] [cursor=pointer]: + - /url: "#featured" + - link "What is new" [ref=e23] [cursor=pointer]: + - /url: "#recent" + - generic [ref=e24]: + - generic [ref=e26]: + - heading "Featured collections" [level=2] [ref=e27] + - paragraph [ref=e28]: Hand-picked edits for every occasion. + - generic [ref=e29]: + - link "Featured Shop now" [ref=e30] [cursor=pointer]: + - /url: /collections/featured + - generic [ref=e31]: + - heading "Featured" [level=3] [ref=e32] + - generic [ref=e33]: Shop now + - link "Sale Shop now" [ref=e34] [cursor=pointer]: + - /url: /collections/sale + - generic [ref=e35]: + - heading "Sale" [level=3] [ref=e36] + - generic [ref=e37]: Shop now + - generic [ref=e38]: + - generic [ref=e40]: + - heading "New arrivals" [level=2] [ref=e41] + - paragraph [ref=e42]: Fresh goods, just in. + - generic [ref=e43]: + - link "Cap EUR 24.99" [ref=e44] [cursor=pointer]: + - /url: /products/cap + - generic [ref=e46]: + - heading "Cap" [level=3] [ref=e47] + - paragraph [ref=e48]: EUR 24.99 + - link "Tote Bag EUR 14.99" [ref=e49] [cursor=pointer]: + - /url: /products/tote-bag + - generic [ref=e51]: + - heading "Tote Bag" [level=3] [ref=e52] + - paragraph [ref=e53]: EUR 14.99 + - link "Classic Tee EUR 19.99" [ref=e54] [cursor=pointer]: + - /url: /products/classic-tee + - generic [ref=e56]: + - heading "Classic Tee" [level=3] [ref=e57] + - paragraph [ref=e58]: EUR 19.99 + - link "Hoodie EUR 49.99" [ref=e59] [cursor=pointer]: + - /url: /products/hoodie + - generic [ref=e61]: + - heading "Hoodie" [level=3] [ref=e62] + - paragraph [ref=e63]: EUR 49.99 + - link "Sneakers EUR 79.99" [ref=e64] [cursor=pointer]: + - /url: /products/sneakers + - generic [ref=e66]: + - heading "Sneakers" [level=3] [ref=e67] + - paragraph [ref=e68]: EUR 79.99 + - link "Mug EUR 9.99" [ref=e69] [cursor=pointer]: + - /url: /products/mug + - generic [ref=e71]: + - heading "Mug" [level=3] [ref=e72] + - paragraph [ref=e73]: EUR 9.99 + - contentinfo [ref=e74]: + - generic [ref=e76]: + - navigation "Footer navigation" + - paragraph [ref=e77]: (c) Shop + - button "Open cart" [ref=e80]: + - img [ref=e81] + - generic [ref=e83]: Cart + - generic [ref=e84]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-20-57-839Z.yml b/.playwright-mcp/page-2026-04-12T20-20-57-839Z.yml new file mode 100644 index 00000000..c02b5ec6 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-20-57-839Z.yml @@ -0,0 +1,42 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Brand + - heading "Classic Tee" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 19.99 + - group "Variant" [ref=e25]: + - generic [ref=e26]: Variant + - generic [ref=e27]: + - button "TEE-M" [ref=e28] + - button "TEE-L" [ref=e29] + - button "TEE-S" [ref=e30] + - generic [ref=e31]: + - generic [ref=e32]: Quantity + - generic [ref=e33]: + - button "Decrease quantity" [ref=e34]: "-" + - generic [ref=e35]: "1" + - button "Increase quantity" [ref=e36]: + + - button "Add to cart" [ref=e37]: + - generic [ref=e38]: Add to cart + - paragraph [ref=e40]: Classic Tee description. + - contentinfo [ref=e41]: + - generic [ref=e43]: + - navigation "Footer navigation" + - paragraph [ref=e44]: (c) Shop + - button "Open cart" [ref=e47]: + - img [ref=e48] + - generic [ref=e50]: Cart + - generic [ref=e51]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-21-07-187Z.yml b/.playwright-mcp/page-2026-04-12T20-21-07-187Z.yml new file mode 100644 index 00000000..eaca9122 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-21-07-187Z.yml @@ -0,0 +1,64 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Brand + - heading "Classic Tee" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 19.99 + - group "Variant" [ref=e25]: + - generic [ref=e26]: Variant + - generic [ref=e27]: + - button "TEE-M" [ref=e28] + - button "TEE-L" [ref=e29] + - button "TEE-S" [ref=e30] + - generic [ref=e31]: + - generic [ref=e32]: Quantity + - generic [ref=e33]: + - button "Decrease quantity" [ref=e34]: "-" + - generic [ref=e35]: "1" + - button "Increase quantity" [ref=e36]: + + - generic [ref=e52]: Added to cart + - button "Add to cart" [ref=e37]: + - generic [ref=e38]: Add to cart + - paragraph [ref=e40]: Classic Tee description. + - contentinfo [ref=e41]: + - generic [ref=e43]: + - navigation "Footer navigation" + - paragraph [ref=e44]: (c) Shop + - generic [ref=e46]: + - button "Open cart" [ref=e47]: + - img [ref=e48] + - generic [ref=e50]: Cart + - generic [ref=e51]: "1" + - dialog "Shopping cart" [ref=e54]: + - banner [ref=e55]: + - heading "Your cart" [level=2] [ref=e56] + - button "Close cart" [ref=e57]: + - img [ref=e58] + - list [ref=e61]: + - listitem [ref=e62]: + - generic [ref=e64]: + - paragraph [ref=e65]: Classic Tee + - paragraph [ref=e66]: Qty 1 + - paragraph [ref=e67]: EUR 19.99 + - contentinfo [ref=e68]: + - generic [ref=e69]: + - generic [ref=e70]: Subtotal + - generic [ref=e71]: EUR 19.99 + - generic [ref=e72]: + - link "View cart" [ref=e73] [cursor=pointer]: + - /url: http://shop.test/cart + - link "Checkout" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/checkout \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-21-14-109Z.yml b/.playwright-mcp/page-2026-04-12T20-21-14-109Z.yml new file mode 100644 index 00000000..876cfd1b --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-21-14-109Z.yml @@ -0,0 +1,93 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Contact and shipping address" [level=2] [ref=e34] + - generic [ref=e35]: + - generic [ref=e36]: + - generic [ref=e37]: Email + - textbox "Email" [ref=e38] + - generic [ref=e39]: + - generic [ref=e40]: + - generic [ref=e41]: First name + - textbox "First name" [ref=e42] + - generic [ref=e43]: + - generic [ref=e44]: Last name + - textbox "Last name" [ref=e45] + - generic [ref=e46]: + - generic [ref=e47]: Address + - textbox "Address" [ref=e48] + - generic [ref=e49]: + - generic [ref=e50]: Apartment, suite (optional) + - textbox "Apartment, suite (optional)" [ref=e51] + - generic [ref=e52]: + - generic [ref=e53]: + - generic [ref=e54]: City + - textbox "City" [ref=e55] + - generic [ref=e56]: + - generic [ref=e57]: Postal code + - textbox "Postal code" [ref=e58] + - generic [ref=e59]: + - generic [ref=e60]: Country + - combobox "Country" [ref=e61]: + - option "Germany" [selected] + - option "Austria" + - option "Switzerland" + - option "France" + - option "Netherlands" + - option "United States" + - option "United Kingdom" + - generic [ref=e62]: + - checkbox "Billing address is the same as shipping" [checked] [ref=e63] + - generic [ref=e64]: Billing address is the same as shipping + - button "Continue to shipping" [ref=e65] + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Classic Tee + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 19.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 19.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 0.00 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 19.99 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-21-36-535Z.yml b/.playwright-mcp/page-2026-04-12T20-21-36-535Z.yml new file mode 100644 index 00000000..a5ec3f6a --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-21-36-535Z.yml @@ -0,0 +1,63 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Shipping method" [level=2] [ref=e96] + - generic [ref=e98] [cursor=pointer]: + - generic [ref=e99]: + - radio "Standard EUR 5.99" [ref=e100] + - generic [ref=e101]: Standard + - generic [ref=e102]: EUR 5.99 + - generic [ref=e103]: + - button "Back" [ref=e104] + - button "Continue to payment" [ref=e105] + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Classic Tee + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 19.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 19.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 0.00 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 19.99 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-21-51-215Z.yml b/.playwright-mcp/page-2026-04-12T20-21-51-215Z.yml new file mode 100644 index 00000000..2f37234c --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-21-51-215Z.yml @@ -0,0 +1,63 @@ +- generic [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Shipping method" [level=2] [ref=e96] + - generic [ref=e98] [cursor=pointer]: + - generic [ref=e99]: + - radio "Standard EUR 5.99" [checked] [active] [ref=e100] + - generic [ref=e101]: Standard + - generic [ref=e102]: EUR 5.99 + - generic [ref=e103]: + - button "Back" [ref=e104] + - button "Continue to payment" [ref=e105] + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Classic Tee + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 19.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 19.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 0.00 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 19.99 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-21-56-826Z.yml b/.playwright-mcp/page-2026-04-12T20-21-56-826Z.yml new file mode 100644 index 00000000..37d12f06 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-21-56-826Z.yml @@ -0,0 +1,80 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Payment" [level=2] [ref=e106] + - generic [ref=e107]: + - generic [ref=e108]: + - radio "Credit card" [checked] [ref=e109] + - generic [ref=e110]: Credit card + - generic [ref=e111]: + - radio "PayPal" [ref=e112] + - generic [ref=e113]: PayPal + - generic [ref=e114]: + - radio "Bank transfer" [ref=e115] + - generic [ref=e116]: Bank transfer + - generic [ref=e117]: "Magic test card: 4242 4242 4242 4242, expiry 12/30, CVC 123." + - generic [ref=e118]: + - generic [ref=e119]: Card number + - textbox "Card number" [ref=e120]: "4242424242424242" + - generic [ref=e121]: + - generic [ref=e122]: + - generic [ref=e123]: Expiry (MM/YY) + - textbox "Expiry (MM/YY)" [ref=e124]: 12/30 + - generic [ref=e125]: + - generic [ref=e126]: CVC + - textbox "CVC" [ref=e127]: "123" + - generic [ref=e128]: + - button "Back" [ref=e129] + - button "Place order" [ref=e130]: + - generic [ref=e131]: Place order + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Classic Tee + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 19.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 19.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 5.99 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 25.98 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-22-07-655Z.yml b/.playwright-mcp/page-2026-04-12T20-22-07-655Z.yml new file mode 100644 index 00000000..b18fe956 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-22-07-655Z.yml @@ -0,0 +1,63 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - img [ref=e19] + - heading "Thank you for your order" [level=1] [ref=e21] + - paragraph [ref=e22]: "Order #1 is confirmed. A receipt has been sent to review@shop.test." + - generic [ref=e23]: + - generic [ref=e24]: + - heading "Order details" [level=2] [ref=e25] + - list [ref=e26]: + - listitem [ref=e27]: + - generic [ref=e30]: + - paragraph [ref=e31]: Classic Tee + - paragraph [ref=e32]: Qty 1 + - paragraph [ref=e33]: EUR 19.99 + - generic [ref=e34]: + - generic [ref=e35]: + - generic [ref=e36]: Subtotal + - generic [ref=e37]: EUR 19.99 + - generic [ref=e38]: + - generic [ref=e39]: Shipping + - generic [ref=e40]: EUR 5.99 + - generic [ref=e41]: + - generic [ref=e42]: Tax + - generic [ref=e43]: EUR 4.15 + - generic [ref=e44]: + - generic [ref=e45]: Total + - generic [ref=e46]: EUR 25.98 + - complementary [ref=e47]: + - generic [ref=e48]: + - heading "Shipping address" [level=3] [ref=e49] + - generic [ref=e50]: + - text: Review Tester + - text: Demo Strasse 1 + - text: Berlin 10115 + - text: DE + - generic [ref=e51]: + - heading "Status" [level=3] [ref=e52] + - paragraph [ref=e53]: "Payment: paid" + - paragraph [ref=e54]: "Fulfillment: unfulfilled" + - link "Continue shopping" [ref=e55] [cursor=pointer]: + - /url: http://shop.test/storefront + - contentinfo [ref=e56]: + - generic [ref=e58]: + - navigation "Footer navigation" + - paragraph [ref=e59]: (c) Shop + - button "Open cart" [ref=e62]: + - img [ref=e63] + - generic [ref=e65]: Cart + - generic [ref=e66]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-22-31-968Z.yml b/.playwright-mcp/page-2026-04-12T20-22-31-968Z.yml new file mode 100644 index 00000000..87220a4c --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-22-31-968Z.yml @@ -0,0 +1,32 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Laravel Starter Kit" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/dashboard + - img [ref=e6] + - generic [ref=e8]: Laravel Starter Kit + - navigation [ref=e9]: + - generic [ref=e10]: + - generic [ref=e12]: Platform + - link "Dashboard" [ref=e15] [cursor=pointer]: + - /url: http://shop.test/dashboard + - img [ref=e17] + - generic [ref=e19]: Dashboard + - navigation [ref=e21]: + - link "Repository" [ref=e23] [cursor=pointer]: + - /url: https://github.com/laravel/livewire-starter-kit + - img [ref=e25] + - generic [ref=e30]: Repository + - link "Documentation" [ref=e32] [cursor=pointer]: + - /url: https://laravel.com/docs/starter-kits#livewire + - img [ref=e34] + - generic [ref=e36]: Documentation + - button "SA Shop Admin" [ref=e38]: + - generic [ref=e41]: SA + - generic [ref=e42]: Shop Admin + - img [ref=e44] + - generic [ref=e48]: + - generic [ref=e49]: + - img [ref=e51] + - img [ref=e54] + - img [ref=e57] + - img [ref=e60] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-22-59-910Z.yml b/.playwright-mcp/page-2026-04-12T20-22-59-910Z.yml new file mode 100644 index 00000000..08d41c6b --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-22-59-910Z.yml @@ -0,0 +1,147 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Dashboard + - combobox [ref=e127]: + - option "Last 7 days" + - option "Last 30 days" [selected] + - option "Last 90 days" + - generic [ref=e128]: + - generic [ref=e129]: + - generic [ref=e130]: Total sales + - generic [ref=e131]: "103.92" + - generic [ref=e132]: + - generic [ref=e133]: Orders + - generic [ref=e134]: "4" + - generic [ref=e135]: + - generic [ref=e136]: Average order value + - generic [ref=e137]: "25.98" + - generic [ref=e138]: + - generic [ref=e139]: Conversion rate + - generic [ref=e140]: N/A + - generic [ref=e141]: + - generic [ref=e142]: Sales over time + - generic [ref=e143]: Charts coming soon + - generic [ref=e144]: + - generic [ref=e145]: Recent orders + - table [ref=e149]: + - rowgroup [ref=e150]: + - row "Order Customer Total Status" [ref=e151]: + - columnheader "Order" [ref=e152]: + - generic [ref=e153]: Order + - columnheader "Customer" [ref=e154]: + - generic [ref=e155]: Customer + - columnheader "Total" [ref=e156]: + - generic [ref=e157]: Total + - columnheader "Status" [ref=e158]: + - generic [ref=e159]: Status + - rowgroup [ref=e160]: + - row "#1 review@shop.test 25.98 EUR paid" [ref=e161]: + - cell "#1" [ref=e162]: + - link "#1" [ref=e163] [cursor=pointer]: + - /url: http://shop.test/admin/orders/4 + - cell "review@shop.test" [ref=e164] + - cell "25.98 EUR" [ref=e165] + - cell "paid" [ref=e166]: + - generic [ref=e167]: paid + - row "D-1001 alice@shop.test 30.98 EUR pending" [ref=e168]: + - cell "D-1001" [ref=e169]: + - link "D-1001" [ref=e170] [cursor=pointer]: + - /url: http://shop.test/admin/orders/1 + - cell "alice@shop.test" [ref=e171] + - cell "30.98 EUR" [ref=e172] + - cell "pending" [ref=e173]: + - generic [ref=e174]: pending + - row "D-1002 bob@shop.test 20.98 EUR paid" [ref=e175]: + - cell "D-1002" [ref=e176]: + - link "D-1002" [ref=e177] [cursor=pointer]: + - /url: http://shop.test/admin/orders/2 + - cell "bob@shop.test" [ref=e178] + - cell "20.98 EUR" [ref=e179] + - cell "paid" [ref=e180]: + - generic [ref=e181]: paid + - row "D-1003 carol@shop.test 25.98 EUR refunded" [ref=e182]: + - cell "D-1003" [ref=e183]: + - link "D-1003" [ref=e184] [cursor=pointer]: + - /url: http://shop.test/admin/orders/3 + - cell "carol@shop.test" [ref=e185] + - cell "25.98 EUR" [ref=e186] + - cell "refunded" [ref=e187]: + - generic [ref=e188]: refunded \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-23-10-091Z.yml b/.playwright-mcp/page-2026-04-12T20-23-10-091Z.yml new file mode 100644 index 00000000..c9c07aee --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-23-10-091Z.yml @@ -0,0 +1,174 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Products + - link "New product" [ref=e127] [cursor=pointer]: + - /url: http://shop.test/admin/products/create + - img [ref=e128] + - generic [ref=e130]: New product + - generic [ref=e131]: + - generic [ref=e132]: + - generic: + - img + - textbox "Search products..." [ref=e133] + - combobox [ref=e135]: + - option "All statuses" [selected] + - option "Draft" + - option "Active" + - option "Archived" + - table [ref=e139]: + - rowgroup [ref=e140]: + - row "Title Status Vendor Variants" [ref=e141]: + - columnheader "Title" [ref=e142]: + - generic [ref=e143]: Title + - columnheader "Status" [ref=e144]: + - generic [ref=e145]: Status + - columnheader "Vendor" [ref=e146]: + - generic [ref=e147]: Vendor + - columnheader "Variants" [ref=e148]: + - generic [ref=e149]: Variants + - columnheader [ref=e150] + - rowgroup [ref=e151]: + - row "Cap active Demo Brand 1 Edit" [ref=e152]: + - cell "Cap" [ref=e153]: + - link "Cap" [ref=e154] [cursor=pointer]: + - /url: http://shop.test/admin/products/3/edit + - cell "active" [ref=e155]: + - generic [ref=e156]: active + - cell "Demo Brand" [ref=e157] + - cell "1" [ref=e158] + - cell "Edit" [ref=e159]: + - link "Edit" [ref=e160] [cursor=pointer]: + - /url: http://shop.test/admin/products/3/edit + - row "Tote Bag active Demo Brand 1 Edit" [ref=e161]: + - cell "Tote Bag" [ref=e162]: + - link "Tote Bag" [ref=e163] [cursor=pointer]: + - /url: http://shop.test/admin/products/4/edit + - cell "active" [ref=e164]: + - generic [ref=e165]: active + - cell "Demo Brand" [ref=e166] + - cell "1" [ref=e167] + - cell "Edit" [ref=e168]: + - link "Edit" [ref=e169] [cursor=pointer]: + - /url: http://shop.test/admin/products/4/edit + - row "Classic Tee active Demo Brand 3 Edit" [ref=e170]: + - cell "Classic Tee" [ref=e171]: + - link "Classic Tee" [ref=e172] [cursor=pointer]: + - /url: http://shop.test/admin/products/1/edit + - cell "active" [ref=e173]: + - generic [ref=e174]: active + - cell "Demo Brand" [ref=e175] + - cell "3" [ref=e176] + - cell "Edit" [ref=e177]: + - link "Edit" [ref=e178] [cursor=pointer]: + - /url: http://shop.test/admin/products/1/edit + - row "Hoodie active Demo Brand 2 Edit" [ref=e179]: + - cell "Hoodie" [ref=e180]: + - link "Hoodie" [ref=e181] [cursor=pointer]: + - /url: http://shop.test/admin/products/2/edit + - cell "active" [ref=e182]: + - generic [ref=e183]: active + - cell "Demo Brand" [ref=e184] + - cell "2" [ref=e185] + - cell "Edit" [ref=e186]: + - link "Edit" [ref=e187] [cursor=pointer]: + - /url: http://shop.test/admin/products/2/edit + - row "Sneakers active Demo Brand 3 Edit" [ref=e188]: + - cell "Sneakers" [ref=e189]: + - link "Sneakers" [ref=e190] [cursor=pointer]: + - /url: http://shop.test/admin/products/5/edit + - cell "active" [ref=e191]: + - generic [ref=e192]: active + - cell "Demo Brand" [ref=e193] + - cell "3" [ref=e194] + - cell "Edit" [ref=e195]: + - link "Edit" [ref=e196] [cursor=pointer]: + - /url: http://shop.test/admin/products/5/edit + - row "Mug active Demo Brand 1 Edit" [ref=e197]: + - cell "Mug" [ref=e198]: + - link "Mug" [ref=e199] [cursor=pointer]: + - /url: http://shop.test/admin/products/6/edit + - cell "active" [ref=e200]: + - generic [ref=e201]: active + - cell "Demo Brand" [ref=e202] + - cell "1" [ref=e203] + - cell "Edit" [ref=e204]: + - link "Edit" [ref=e205] [cursor=pointer]: + - /url: http://shop.test/admin/products/6/edit \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-23-24-715Z.yml b/.playwright-mcp/page-2026-04-12T20-23-24-715Z.yml new file mode 100644 index 00000000..2b825ce1 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-23-24-715Z.yml @@ -0,0 +1,156 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: Orders + - generic [ref=e126]: + - generic [ref=e127]: + - generic: + - img + - textbox "Search orders..." [ref=e128] + - combobox [ref=e130]: + - option "All payments" [selected] + - option "Pending" + - option "Paid" + - option "Refunded" + - option "Partially refunded" + - combobox [ref=e131]: + - option "All fulfillment" [selected] + - option "Unfulfilled" + - option "Partial" + - option "Fulfilled" + - table [ref=e135]: + - rowgroup [ref=e136]: + - row "Order Customer Total Payment Fulfillment Date" [ref=e137]: + - columnheader "Order" [ref=e138]: + - generic [ref=e139]: Order + - columnheader "Customer" [ref=e140]: + - generic [ref=e141]: Customer + - columnheader "Total" [ref=e142]: + - generic [ref=e143]: Total + - columnheader "Payment" [ref=e144]: + - generic [ref=e145]: Payment + - columnheader "Fulfillment" [ref=e146]: + - generic [ref=e147]: Fulfillment + - columnheader "Date" [ref=e148]: + - generic [ref=e149]: Date + - rowgroup [ref=e150]: + - row "#1 review@shop.test 25.98 EUR paid unfulfilled Apr 12, 2026" [ref=e151]: + - cell "#1" [ref=e152]: + - link "#1" [ref=e153] [cursor=pointer]: + - /url: http://shop.test/admin/orders/4 + - cell "review@shop.test" [ref=e154] + - cell "25.98 EUR" [ref=e155] + - cell "paid" [ref=e156]: + - generic [ref=e157]: paid + - cell "unfulfilled" [ref=e158]: + - generic [ref=e159]: unfulfilled + - cell "Apr 12, 2026" [ref=e160] + - row "D-1001 alice@shop.test 30.98 EUR pending unfulfilled Apr 11, 2026" [ref=e161]: + - cell "D-1001" [ref=e162]: + - link "D-1001" [ref=e163] [cursor=pointer]: + - /url: http://shop.test/admin/orders/1 + - cell "alice@shop.test" [ref=e164] + - cell "30.98 EUR" [ref=e165] + - cell "pending" [ref=e166]: + - generic [ref=e167]: pending + - cell "unfulfilled" [ref=e168]: + - generic [ref=e169]: unfulfilled + - cell "Apr 11, 2026" [ref=e170] + - row "D-1002 bob@shop.test 20.98 EUR paid fulfilled Apr 10, 2026" [ref=e171]: + - cell "D-1002" [ref=e172]: + - link "D-1002" [ref=e173] [cursor=pointer]: + - /url: http://shop.test/admin/orders/2 + - cell "bob@shop.test" [ref=e174] + - cell "20.98 EUR" [ref=e175] + - cell "paid" [ref=e176]: + - generic [ref=e177]: paid + - cell "fulfilled" [ref=e178]: + - generic [ref=e179]: fulfilled + - cell "Apr 10, 2026" [ref=e180] + - row "D-1003 carol@shop.test 25.98 EUR refunded fulfilled Apr 09, 2026" [ref=e181]: + - cell "D-1003" [ref=e182]: + - link "D-1003" [ref=e183] [cursor=pointer]: + - /url: http://shop.test/admin/orders/3 + - cell "carol@shop.test" [ref=e184] + - cell "25.98 EUR" [ref=e185] + - cell "refunded" [ref=e186]: + - generic [ref=e187]: refunded + - cell "fulfilled" [ref=e188]: + - generic [ref=e189]: fulfilled + - cell "Apr 09, 2026" [ref=e190] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-23-34-236Z.yml b/.playwright-mcp/page-2026-04-12T20-23-34-236Z.yml new file mode 100644 index 00000000..4f27a8dd --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-23-34-236Z.yml @@ -0,0 +1,123 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Shipping + - generic [ref=e127]: + - link "Back" [ref=e128] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - button "New zone" [ref=e129]: + - img [ref=e131] + - img [ref=e134] + - generic [ref=e136]: New zone + - generic [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - generic [ref=e141]: Europe + - generic [ref=e142]: + - generic [ref=e143]: DE + - generic [ref=e144]: AT + - generic [ref=e145]: CH + - generic [ref=e146]: + - button "Add rate" [ref=e147]: + - img [ref=e149] + - img [ref=e152] + - generic [ref=e154]: Add rate + - button "Delete" [ref=e155]: + - img [ref=e157] + - generic [ref=e160]: Delete + - table [ref=e164]: + - rowgroup [ref=e165]: + - row "Name Type Amount" [ref=e166]: + - columnheader "Name" [ref=e167]: + - generic [ref=e168]: Name + - columnheader "Type" [ref=e169]: + - generic [ref=e170]: Type + - columnheader "Amount" [ref=e171]: + - generic [ref=e172]: Amount + - columnheader [ref=e173] + - rowgroup [ref=e174]: + - row "Standard flat 5.99 Remove" [ref=e175]: + - cell "Standard" [ref=e176] + - cell "flat" [ref=e177] + - cell "5.99" [ref=e178] + - cell "Remove" [ref=e179]: + - button "Remove" [ref=e180]: + - img [ref=e182] + - generic [ref=e185]: Remove \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-23-49-336Z.yml b/.playwright-mcp/page-2026-04-12T20-23-49-336Z.yml new file mode 100644 index 00000000..75998fca --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-23-49-336Z.yml @@ -0,0 +1,110 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: Developers + - generic [ref=e126]: + - generic [ref=e127]: API tokens + - paragraph [ref=e128]: Personal access tokens for the Admin API. + - generic [ref=e129]: + - generic [ref=e130]: + - generic [ref=e131]: Token name + - textbox "Token name" [ref=e133]: + - /placeholder: My integration + - button "Create token" [ref=e134]: + - img [ref=e136] + - img [ref=e139] + - generic [ref=e141]: Create token + - paragraph [ref=e143]: No tokens yet. + - generic [ref=e144]: + - generic [ref=e145]: Webhook subscriptions + - paragraph [ref=e146]: HTTP endpoints notified when events occur. + - generic [ref=e147]: + - generic [ref=e148]: + - generic [ref=e149]: Event type + - textbox "Event type" [ref=e151]: + - /placeholder: order.placed + - generic [ref=e152]: + - generic [ref=e153]: URL + - textbox "URL" [ref=e155]: + - /placeholder: https://example.com/webhook + - button "Add webhook" [ref=e157]: + - img [ref=e159] + - img [ref=e162] + - generic [ref=e164]: Add webhook + - paragraph [ref=e166]: No webhooks yet. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-23-59-972Z.yml b/.playwright-mcp/page-2026-04-12T20-23-59-972Z.yml new file mode 100644 index 00000000..ab4891c6 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-23-59-972Z.yml @@ -0,0 +1,147 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Pages + - link "New page" [ref=e127] [cursor=pointer]: + - /url: http://shop.test/admin/pages/create + - img [ref=e128] + - generic [ref=e130]: New page + - generic [ref=e131]: + - generic: + - img + - textbox "Search pages..." [ref=e132] + - table [ref=e137]: + - rowgroup [ref=e138]: + - row "Title Handle Status Updated" [ref=e139]: + - columnheader "Title" [ref=e140]: + - generic [ref=e141]: Title + - columnheader "Handle" [ref=e142]: + - generic [ref=e143]: Handle + - columnheader "Status" [ref=e144]: + - generic [ref=e145]: Status + - columnheader "Updated" [ref=e146]: + - generic [ref=e147]: Updated + - columnheader [ref=e148] + - rowgroup [ref=e149]: + - row "About Us about-us published 4 minutes ago Edit Delete" [ref=e150]: + - cell "About Us" [ref=e151]: + - link "About Us" [ref=e152] [cursor=pointer]: + - /url: http://shop.test/admin/pages/1/edit + - cell "about-us" [ref=e153] + - cell "published" [ref=e154]: + - generic [ref=e155]: published + - cell "4 minutes ago" [ref=e156] + - cell "Edit Delete" [ref=e157]: + - generic [ref=e158]: + - link "Edit" [ref=e159] [cursor=pointer]: + - /url: http://shop.test/admin/pages/1/edit + - button "Delete" [ref=e160]: + - img [ref=e162] + - generic [ref=e165]: Delete + - row "Contact contact published 4 minutes ago Edit Delete" [ref=e166]: + - cell "Contact" [ref=e167]: + - link "Contact" [ref=e168] [cursor=pointer]: + - /url: http://shop.test/admin/pages/2/edit + - cell "contact" [ref=e169] + - cell "published" [ref=e170]: + - generic [ref=e171]: published + - cell "4 minutes ago" [ref=e172] + - cell "Edit Delete" [ref=e173]: + - generic [ref=e174]: + - link "Edit" [ref=e175] [cursor=pointer]: + - /url: http://shop.test/admin/pages/2/edit + - button "Delete" [ref=e176]: + - img [ref=e178] + - generic [ref=e181]: Delete + - row "FAQ faq published 4 minutes ago Edit Delete" [ref=e182]: + - cell "FAQ" [ref=e183]: + - link "FAQ" [ref=e184] [cursor=pointer]: + - /url: http://shop.test/admin/pages/3/edit + - cell "faq" [ref=e185] + - cell "published" [ref=e186]: + - generic [ref=e187]: published + - cell "4 minutes ago" [ref=e188] + - cell "Edit Delete" [ref=e189]: + - generic [ref=e190]: + - link "Edit" [ref=e191] [cursor=pointer]: + - /url: http://shop.test/admin/pages/3/edit + - button "Delete" [ref=e192]: + - img [ref=e194] + - generic [ref=e197]: Delete \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-24-08-408Z.yml b/.playwright-mcp/page-2026-04-12T20-24-08-408Z.yml new file mode 100644 index 00000000..71fba4a2 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-24-08-408Z.yml @@ -0,0 +1,104 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Analytics + - generic [ref=e127]: + - generic [ref=e128]: + - generic [ref=e129]: Start date + - textbox "Start date" [ref=e131]: 2026-03-14 + - generic [ref=e133]: + - generic [ref=e134]: End date + - textbox "End date" [ref=e136]: 2026-04-12 + - generic [ref=e138]: + - generic [ref=e139]: + - paragraph [ref=e140]: Revenue + - paragraph [ref=e141]: 0.00 EUR + - generic [ref=e142]: + - paragraph [ref=e143]: Orders + - paragraph [ref=e144]: "0" + - generic [ref=e145]: + - paragraph [ref=e146]: AOV + - paragraph [ref=e147]: 0.00 EUR + - generic [ref=e148]: + - paragraph [ref=e149]: Visits + - paragraph [ref=e150]: "0" + - generic [ref=e151]: + - generic [ref=e153]: Daily breakdown + - generic [ref=e154]: No data for this range. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-24-13-117Z.yml b/.playwright-mcp/page-2026-04-12T20-24-13-117Z.yml new file mode 100644 index 00000000..ea6c60ed --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-24-13-117Z.yml @@ -0,0 +1,58 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - button "Search" [ref=e6]: + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: "#" + - img [ref=e10] + - button "Cart" [ref=e12]: + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - navigation "Breadcrumb" [ref=e18]: + - link "Collections" [ref=e19] [cursor=pointer]: + - /url: http://shop.test/collections + - text: / Featured + - heading "Featured" [level=1] [ref=e20] + - generic [ref=e21]: + - paragraph [ref=e22]: 4 products + - generic [ref=e23]: + - generic [ref=e24]: Sort + - combobox "Sort" [ref=e25]: + - option "Newest" [selected] + - option "Title, A to Z" + - option "Featured" + - generic [ref=e26]: + - link "Tote Bag EUR 14.99" [ref=e27] [cursor=pointer]: + - /url: /products/tote-bag + - generic [ref=e29]: + - heading "Tote Bag" [level=3] [ref=e30] + - paragraph [ref=e31]: EUR 14.99 + - link "Cap EUR 24.99" [ref=e32] [cursor=pointer]: + - /url: /products/cap + - generic [ref=e34]: + - heading "Cap" [level=3] [ref=e35] + - paragraph [ref=e36]: EUR 24.99 + - link "Hoodie EUR 49.99" [ref=e37] [cursor=pointer]: + - /url: /products/hoodie + - generic [ref=e39]: + - heading "Hoodie" [level=3] [ref=e40] + - paragraph [ref=e41]: EUR 49.99 + - link "Classic Tee EUR 19.99" [ref=e42] [cursor=pointer]: + - /url: /products/classic-tee + - generic [ref=e44]: + - heading "Classic Tee" [level=3] [ref=e45] + - paragraph [ref=e46]: EUR 19.99 + - contentinfo [ref=e47]: + - generic [ref=e49]: + - navigation "Footer navigation" + - paragraph [ref=e50]: (c) Shop + - button "Open cart" [ref=e53]: + - img [ref=e54] + - generic [ref=e56]: Cart + - generic [ref=e57]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-28-09-973Z.yml b/.playwright-mcp/page-2026-04-12T20-28-09-973Z.yml new file mode 100644 index 00000000..08d41c6b --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-28-09-973Z.yml @@ -0,0 +1,147 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Dashboard + - combobox [ref=e127]: + - option "Last 7 days" + - option "Last 30 days" [selected] + - option "Last 90 days" + - generic [ref=e128]: + - generic [ref=e129]: + - generic [ref=e130]: Total sales + - generic [ref=e131]: "103.92" + - generic [ref=e132]: + - generic [ref=e133]: Orders + - generic [ref=e134]: "4" + - generic [ref=e135]: + - generic [ref=e136]: Average order value + - generic [ref=e137]: "25.98" + - generic [ref=e138]: + - generic [ref=e139]: Conversion rate + - generic [ref=e140]: N/A + - generic [ref=e141]: + - generic [ref=e142]: Sales over time + - generic [ref=e143]: Charts coming soon + - generic [ref=e144]: + - generic [ref=e145]: Recent orders + - table [ref=e149]: + - rowgroup [ref=e150]: + - row "Order Customer Total Status" [ref=e151]: + - columnheader "Order" [ref=e152]: + - generic [ref=e153]: Order + - columnheader "Customer" [ref=e154]: + - generic [ref=e155]: Customer + - columnheader "Total" [ref=e156]: + - generic [ref=e157]: Total + - columnheader "Status" [ref=e158]: + - generic [ref=e159]: Status + - rowgroup [ref=e160]: + - row "#1 review@shop.test 25.98 EUR paid" [ref=e161]: + - cell "#1" [ref=e162]: + - link "#1" [ref=e163] [cursor=pointer]: + - /url: http://shop.test/admin/orders/4 + - cell "review@shop.test" [ref=e164] + - cell "25.98 EUR" [ref=e165] + - cell "paid" [ref=e166]: + - generic [ref=e167]: paid + - row "D-1001 alice@shop.test 30.98 EUR pending" [ref=e168]: + - cell "D-1001" [ref=e169]: + - link "D-1001" [ref=e170] [cursor=pointer]: + - /url: http://shop.test/admin/orders/1 + - cell "alice@shop.test" [ref=e171] + - cell "30.98 EUR" [ref=e172] + - cell "pending" [ref=e173]: + - generic [ref=e174]: pending + - row "D-1002 bob@shop.test 20.98 EUR paid" [ref=e175]: + - cell "D-1002" [ref=e176]: + - link "D-1002" [ref=e177] [cursor=pointer]: + - /url: http://shop.test/admin/orders/2 + - cell "bob@shop.test" [ref=e178] + - cell "20.98 EUR" [ref=e179] + - cell "paid" [ref=e180]: + - generic [ref=e181]: paid + - row "D-1003 carol@shop.test 25.98 EUR refunded" [ref=e182]: + - cell "D-1003" [ref=e183]: + - link "D-1003" [ref=e184] [cursor=pointer]: + - /url: http://shop.test/admin/orders/3 + - cell "carol@shop.test" [ref=e185] + - cell "25.98 EUR" [ref=e186] + - cell "refunded" [ref=e187]: + - generic [ref=e188]: refunded \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-28-14-370Z.yml b/.playwright-mcp/page-2026-04-12T20-28-14-370Z.yml new file mode 100644 index 00000000..db0e408e --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-28-14-370Z.yml @@ -0,0 +1,84 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account/login + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - paragraph [ref=e18]: New season + - heading "Thoughtfully made, honestly priced." [level=1] [ref=e19] + - paragraph [ref=e20]: A curated collection of timeless goods designed to last. Explore our latest arrivals and find something you will love. + - generic [ref=e21]: + - link "Shop the collection" [ref=e22] [cursor=pointer]: + - /url: "#featured" + - link "What is new" [ref=e23] [cursor=pointer]: + - /url: "#recent" + - generic [ref=e24]: + - generic [ref=e26]: + - heading "Featured collections" [level=2] [ref=e27] + - paragraph [ref=e28]: Hand-picked edits for every occasion. + - generic [ref=e29]: + - link "Featured Shop now" [ref=e30] [cursor=pointer]: + - /url: /collections/featured + - generic [ref=e31]: + - heading "Featured" [level=3] [ref=e32] + - generic [ref=e33]: Shop now + - link "Sale Shop now" [ref=e34] [cursor=pointer]: + - /url: /collections/sale + - generic [ref=e35]: + - heading "Sale" [level=3] [ref=e36] + - generic [ref=e37]: Shop now + - generic [ref=e38]: + - generic [ref=e40]: + - heading "New arrivals" [level=2] [ref=e41] + - paragraph [ref=e42]: Fresh goods, just in. + - generic [ref=e43]: + - link "Cap EUR 24.99" [ref=e44] [cursor=pointer]: + - /url: /products/cap + - generic [ref=e46]: + - heading "Cap" [level=3] [ref=e47] + - paragraph [ref=e48]: EUR 24.99 + - link "Tote Bag EUR 14.99" [ref=e49] [cursor=pointer]: + - /url: /products/tote-bag + - generic [ref=e51]: + - heading "Tote Bag" [level=3] [ref=e52] + - paragraph [ref=e53]: EUR 14.99 + - link "Classic Tee EUR 19.99" [ref=e54] [cursor=pointer]: + - /url: /products/classic-tee + - generic [ref=e56]: + - heading "Classic Tee" [level=3] [ref=e57] + - paragraph [ref=e58]: EUR 19.99 + - link "Hoodie EUR 49.99" [ref=e59] [cursor=pointer]: + - /url: /products/hoodie + - generic [ref=e61]: + - heading "Hoodie" [level=3] [ref=e62] + - paragraph [ref=e63]: EUR 49.99 + - link "Sneakers EUR 79.99" [ref=e64] [cursor=pointer]: + - /url: /products/sneakers + - generic [ref=e66]: + - heading "Sneakers" [level=3] [ref=e67] + - paragraph [ref=e68]: EUR 79.99 + - link "Mug EUR 9.99" [ref=e69] [cursor=pointer]: + - /url: /products/mug + - generic [ref=e71]: + - heading "Mug" [level=3] [ref=e72] + - paragraph [ref=e73]: EUR 9.99 + - contentinfo [ref=e74]: + - generic [ref=e76]: + - navigation "Footer navigation" + - paragraph [ref=e77]: (c) Shop + - button "Open cart" [ref=e80]: + - img [ref=e81] + - generic [ref=e83]: Cart + - generic [ref=e84]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-28-27-083Z.yml b/.playwright-mcp/page-2026-04-12T20-28-27-083Z.yml new file mode 100644 index 00000000..8bc2ff42 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-28-27-083Z.yml @@ -0,0 +1,43 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account/login + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Brand + - heading "Hoodie" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 49.99 + - group "Variant" [ref=e25]: + - generic [ref=e26]: Variant + - generic [ref=e27]: + - button "HOOD-L" [ref=e28] + - button "HOOD-M" [ref=e29] + - generic [ref=e30]: + - generic [ref=e31]: Quantity + - generic [ref=e32]: + - button "Decrease quantity" [ref=e33]: "-" + - generic [ref=e34]: "1" + - button "Increase quantity" [ref=e35]: + + - button "Add to cart" [ref=e36]: + - generic [ref=e37]: Add to cart + - paragraph [ref=e39]: Hoodie description. + - contentinfo [ref=e40]: + - generic [ref=e42]: + - navigation "Footer navigation" + - paragraph [ref=e43]: (c) Shop + - button "Open cart" [ref=e46]: + - img [ref=e47] + - generic [ref=e49]: Cart + - generic [ref=e50]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-28-32-540Z.yml b/.playwright-mcp/page-2026-04-12T20-28-32-540Z.yml new file mode 100644 index 00000000..85d46518 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-28-32-540Z.yml @@ -0,0 +1,43 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account/login + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - heading "Sign in" [level=1] [ref=e18] + - paragraph [ref=e19]: Access your orders and saved addresses. + - generic [ref=e20]: + - generic [ref=e21]: + - generic [ref=e22]: Email + - textbox "Email" [ref=e23] + - generic [ref=e24]: + - generic [ref=e25]: Password + - textbox "Password" [ref=e26] + - generic [ref=e27]: + - checkbox "Remember me" [ref=e28] + - generic [ref=e29]: Remember me + - button "Sign in" [ref=e30] + - paragraph [ref=e31]: + - text: New customer? + - link "Create an account" [ref=e32] [cursor=pointer]: + - /url: http://shop.test/account/register + - contentinfo [ref=e33]: + - generic [ref=e35]: + - navigation "Footer navigation" + - paragraph [ref=e36]: (c) Shop + - button "Open cart" [ref=e39]: + - img [ref=e40] + - generic [ref=e42]: Cart + - generic [ref=e43]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-28-59-050Z.yml b/.playwright-mcp/page-2026-04-12T20-28-59-050Z.yml new file mode 100644 index 00000000..60ff04c6 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-28-59-050Z.yml @@ -0,0 +1,55 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - generic [ref=e18]: + - paragraph [ref=e19]: Account + - heading "Hi, Alice Example" [level=1] [ref=e20] + - paragraph [ref=e21]: alice@shop.test + - button "Sign out" [ref=e23] + - navigation "Account sections" [ref=e24]: + - link "Orders Review your order history." [ref=e25] [cursor=pointer]: + - /url: http://shop.test/account/orders + - heading "Orders" [level=2] [ref=e26] + - paragraph [ref=e27]: Review your order history. + - link "Addresses Manage saved shipping addresses." [ref=e28] [cursor=pointer]: + - /url: http://shop.test/account/addresses + - heading "Addresses" [level=2] [ref=e29] + - paragraph [ref=e30]: Manage saved shipping addresses. + - link "Continue shopping Browse the latest arrivals." [ref=e31] [cursor=pointer]: + - /url: http://shop.test/storefront + - heading "Continue shopping" [level=2] [ref=e32] + - paragraph [ref=e33]: Browse the latest arrivals. + - generic [ref=e34]: + - heading "Recent orders" [level=2] [ref=e35] + - list [ref=e36]: + - listitem [ref=e37]: + - generic [ref=e38]: + - paragraph [ref=e39]: D-1001 + - paragraph [ref=e40]: Apr 11, 2026 + - generic [ref=e41]: + - generic [ref=e42]: EUR 30.98 + - link "View" [ref=e43] [cursor=pointer]: + - /url: http://shop.test/account/orders/D-1001 + - contentinfo [ref=e44]: + - generic [ref=e46]: + - navigation "Footer navigation" + - paragraph [ref=e47]: (c) Shop + - button "Open cart" [ref=e50]: + - img [ref=e51] + - generic [ref=e53]: Cart + - generic [ref=e54]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-29-07-015Z.yml b/.playwright-mcp/page-2026-04-12T20-29-07-015Z.yml new file mode 100644 index 00000000..f507eb3b --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-29-07-015Z.yml @@ -0,0 +1,46 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - link "Back to account" [ref=e18] [cursor=pointer]: + - /url: http://shop.test/account + - heading "Orders" [level=1] [ref=e19] + - table [ref=e21]: + - rowgroup [ref=e22]: + - row "Order Date Total Status" [ref=e23]: + - columnheader "Order" [ref=e24] + - columnheader "Date" [ref=e25] + - columnheader "Total" [ref=e26] + - columnheader "Status" [ref=e27] + - columnheader [ref=e28] + - rowgroup [ref=e29]: + - row "D-1001 Apr 11, 2026 EUR 30.98 pending View" [ref=e30]: + - cell "D-1001" [ref=e31] + - cell "Apr 11, 2026" [ref=e32] + - cell "EUR 30.98" [ref=e33] + - cell "pending" [ref=e34] + - cell "View" [ref=e35]: + - link "View" [ref=e36] [cursor=pointer]: + - /url: http://shop.test/account/orders/D-1001 + - contentinfo [ref=e37]: + - generic [ref=e39]: + - navigation "Footer navigation" + - paragraph [ref=e40]: (c) Shop + - button "Open cart" [ref=e43]: + - img [ref=e44] + - generic [ref=e46]: Cart + - generic [ref=e47]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-29-18-970Z.yml b/.playwright-mcp/page-2026-04-12T20-29-18-970Z.yml new file mode 100644 index 00000000..2b825ce1 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-29-18-970Z.yml @@ -0,0 +1,156 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: Orders + - generic [ref=e126]: + - generic [ref=e127]: + - generic: + - img + - textbox "Search orders..." [ref=e128] + - combobox [ref=e130]: + - option "All payments" [selected] + - option "Pending" + - option "Paid" + - option "Refunded" + - option "Partially refunded" + - combobox [ref=e131]: + - option "All fulfillment" [selected] + - option "Unfulfilled" + - option "Partial" + - option "Fulfilled" + - table [ref=e135]: + - rowgroup [ref=e136]: + - row "Order Customer Total Payment Fulfillment Date" [ref=e137]: + - columnheader "Order" [ref=e138]: + - generic [ref=e139]: Order + - columnheader "Customer" [ref=e140]: + - generic [ref=e141]: Customer + - columnheader "Total" [ref=e142]: + - generic [ref=e143]: Total + - columnheader "Payment" [ref=e144]: + - generic [ref=e145]: Payment + - columnheader "Fulfillment" [ref=e146]: + - generic [ref=e147]: Fulfillment + - columnheader "Date" [ref=e148]: + - generic [ref=e149]: Date + - rowgroup [ref=e150]: + - row "#1 review@shop.test 25.98 EUR paid unfulfilled Apr 12, 2026" [ref=e151]: + - cell "#1" [ref=e152]: + - link "#1" [ref=e153] [cursor=pointer]: + - /url: http://shop.test/admin/orders/4 + - cell "review@shop.test" [ref=e154] + - cell "25.98 EUR" [ref=e155] + - cell "paid" [ref=e156]: + - generic [ref=e157]: paid + - cell "unfulfilled" [ref=e158]: + - generic [ref=e159]: unfulfilled + - cell "Apr 12, 2026" [ref=e160] + - row "D-1001 alice@shop.test 30.98 EUR pending unfulfilled Apr 11, 2026" [ref=e161]: + - cell "D-1001" [ref=e162]: + - link "D-1001" [ref=e163] [cursor=pointer]: + - /url: http://shop.test/admin/orders/1 + - cell "alice@shop.test" [ref=e164] + - cell "30.98 EUR" [ref=e165] + - cell "pending" [ref=e166]: + - generic [ref=e167]: pending + - cell "unfulfilled" [ref=e168]: + - generic [ref=e169]: unfulfilled + - cell "Apr 11, 2026" [ref=e170] + - row "D-1002 bob@shop.test 20.98 EUR paid fulfilled Apr 10, 2026" [ref=e171]: + - cell "D-1002" [ref=e172]: + - link "D-1002" [ref=e173] [cursor=pointer]: + - /url: http://shop.test/admin/orders/2 + - cell "bob@shop.test" [ref=e174] + - cell "20.98 EUR" [ref=e175] + - cell "paid" [ref=e176]: + - generic [ref=e177]: paid + - cell "fulfilled" [ref=e178]: + - generic [ref=e179]: fulfilled + - cell "Apr 10, 2026" [ref=e180] + - row "D-1003 carol@shop.test 25.98 EUR refunded fulfilled Apr 09, 2026" [ref=e181]: + - cell "D-1003" [ref=e182]: + - link "D-1003" [ref=e183] [cursor=pointer]: + - /url: http://shop.test/admin/orders/3 + - cell "carol@shop.test" [ref=e184] + - cell "25.98 EUR" [ref=e185] + - cell "refunded" [ref=e186]: + - generic [ref=e187]: refunded + - cell "fulfilled" [ref=e188]: + - generic [ref=e189]: fulfilled + - cell "Apr 09, 2026" [ref=e190] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-29-29-752Z.yml b/.playwright-mcp/page-2026-04-12T20-29-29-752Z.yml new file mode 100644 index 00000000..7269713b --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-29-29-752Z.yml @@ -0,0 +1,133 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e126]: + - generic [ref=e127]: Order D-1001 + - generic [ref=e128]: + - generic [ref=e129]: pending + - generic [ref=e130]: unfulfilled + - generic [ref=e131]: Apr 11, 2026 20:19 + - generic [ref=e132]: + - generic [ref=e133]: + - generic [ref=e134]: + - generic [ref=e135]: Items + - table [ref=e138]: + - rowgroup [ref=e139]: + - row "Product SKU Qty Total" [ref=e140]: + - columnheader "Product" [ref=e141]: + - generic [ref=e142]: Product + - columnheader "SKU" [ref=e143]: + - generic [ref=e144]: SKU + - columnheader "Qty" [ref=e145]: + - generic [ref=e146]: Qty + - columnheader "Total" [ref=e147]: + - generic [ref=e148]: Total + - rowgroup [ref=e149]: + - row "Cap CAP-001 1 24.99 EUR" [ref=e150]: + - cell "Cap" [ref=e151] + - cell "CAP-001" [ref=e152] + - cell "1" [ref=e153] + - cell "24.99 EUR" [ref=e154] + - generic [ref=e155]: + - generic [ref=e156]: + - generic [ref=e157]: Subtotal + - generic [ref=e158]: "24.99" + - generic [ref=e159]: + - generic [ref=e160]: Shipping + - generic [ref=e161]: "5.99" + - generic [ref=e162]: + - generic [ref=e163]: Tax + - generic [ref=e164]: "0.00" + - generic [ref=e165]: + - generic [ref=e166]: Total + - generic [ref=e167]: "30.98" + - generic [ref=e168]: + - generic [ref=e169]: Fulfillments + - paragraph [ref=e170]: No fulfillments yet. + - generic [ref=e171]: + - generic [ref=e172]: + - generic [ref=e173]: Customer + - generic [ref=e174]: + - generic [ref=e175]: Alice Example + - generic [ref=e176]: alice@shop.test + - generic [ref=e177]: + - generic [ref=e178]: Payment + - generic [ref=e179]: + - generic [ref=e180]: "Method: credit_card" + - generic [ref=e181]: "Status: pending" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-32-45-824Z.yml b/.playwright-mcp/page-2026-04-12T20-32-45-824Z.yml new file mode 100644 index 00000000..4cb5ed6e --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-32-45-824Z.yml @@ -0,0 +1,84 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - paragraph [ref=e18]: New season + - heading "Thoughtfully made, honestly priced." [level=1] [ref=e19] + - paragraph [ref=e20]: A curated collection of timeless goods designed to last. Explore our latest arrivals and find something you will love. + - generic [ref=e21]: + - link "Shop the collection" [ref=e22] [cursor=pointer]: + - /url: "#featured" + - link "What is new" [ref=e23] [cursor=pointer]: + - /url: "#recent" + - generic [ref=e24]: + - generic [ref=e26]: + - heading "Featured collections" [level=2] [ref=e27] + - paragraph [ref=e28]: Hand-picked edits for every occasion. + - generic [ref=e29]: + - link "Featured Shop now" [ref=e30] [cursor=pointer]: + - /url: /collections/featured + - generic [ref=e31]: + - heading "Featured" [level=3] [ref=e32] + - generic [ref=e33]: Shop now + - link "Sale Shop now" [ref=e34] [cursor=pointer]: + - /url: /collections/sale + - generic [ref=e35]: + - heading "Sale" [level=3] [ref=e36] + - generic [ref=e37]: Shop now + - generic [ref=e38]: + - generic [ref=e40]: + - heading "New arrivals" [level=2] [ref=e41] + - paragraph [ref=e42]: Fresh goods, just in. + - generic [ref=e43]: + - link "Cap EUR 24.99" [ref=e44] [cursor=pointer]: + - /url: /products/cap + - generic [ref=e46]: + - heading "Cap" [level=3] [ref=e47] + - paragraph [ref=e48]: EUR 24.99 + - link "Tote Bag EUR 14.99" [ref=e49] [cursor=pointer]: + - /url: /products/tote-bag + - generic [ref=e51]: + - heading "Tote Bag" [level=3] [ref=e52] + - paragraph [ref=e53]: EUR 14.99 + - link "Classic Tee EUR 19.99" [ref=e54] [cursor=pointer]: + - /url: /products/classic-tee + - generic [ref=e56]: + - heading "Classic Tee" [level=3] [ref=e57] + - paragraph [ref=e58]: EUR 19.99 + - link "Hoodie EUR 49.99" [ref=e59] [cursor=pointer]: + - /url: /products/hoodie + - generic [ref=e61]: + - heading "Hoodie" [level=3] [ref=e62] + - paragraph [ref=e63]: EUR 49.99 + - link "Sneakers EUR 79.99" [ref=e64] [cursor=pointer]: + - /url: /products/sneakers + - generic [ref=e66]: + - heading "Sneakers" [level=3] [ref=e67] + - paragraph [ref=e68]: EUR 79.99 + - link "Mug EUR 9.99" [ref=e69] [cursor=pointer]: + - /url: /products/mug + - generic [ref=e71]: + - heading "Mug" [level=3] [ref=e72] + - paragraph [ref=e73]: EUR 9.99 + - contentinfo [ref=e74]: + - generic [ref=e76]: + - navigation "Footer navigation" + - paragraph [ref=e77]: (c) Shop + - button "Open cart" [ref=e80]: + - img [ref=e81] + - generic [ref=e83]: Cart + - generic [ref=e84]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-32-58-626Z.yml b/.playwright-mcp/page-2026-04-12T20-32-58-626Z.yml new file mode 100644 index 00000000..0228d8a7 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-32-58-626Z.yml @@ -0,0 +1,44 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Brand + - heading "Sneakers" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 79.99 + - group "Variant" [ref=e25]: + - generic [ref=e26]: Variant + - generic [ref=e27]: + - button "SNK-43" [ref=e28] + - button "SNK-44" [ref=e29] + - button "SNK-42" [ref=e30] + - generic [ref=e31]: + - generic [ref=e32]: Quantity + - generic [ref=e33]: + - button "Decrease quantity" [ref=e34]: "-" + - generic [ref=e35]: "1" + - button "Increase quantity" [ref=e36]: + + - button "Add to cart" [ref=e37]: + - generic [ref=e38]: Add to cart + - paragraph [ref=e40]: Sneakers description. + - contentinfo [ref=e41]: + - generic [ref=e43]: + - navigation "Footer navigation" + - paragraph [ref=e44]: (c) Shop + - button "Open cart" [ref=e47]: + - img [ref=e48] + - generic [ref=e50]: Cart + - generic [ref=e51]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-33-11-977Z.yml b/.playwright-mcp/page-2026-04-12T20-33-11-977Z.yml new file mode 100644 index 00000000..4abac94d --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-33-11-977Z.yml @@ -0,0 +1,139 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Dashboard + - combobox [ref=e127]: + - option "Last 7 days" + - option "Last 30 days" [selected] + - option "Last 90 days" + - generic [ref=e128]: + - generic [ref=e129]: + - generic [ref=e130]: Total sales + - generic [ref=e131]: "77.94" + - generic [ref=e132]: + - generic [ref=e133]: Orders + - generic [ref=e134]: "3" + - generic [ref=e135]: + - generic [ref=e136]: Average order value + - generic [ref=e137]: "25.98" + - generic [ref=e138]: + - generic [ref=e139]: Conversion rate + - generic [ref=e140]: N/A + - generic [ref=e141]: + - generic [ref=e142]: Sales over time + - generic [ref=e143]: Charts coming soon + - generic [ref=e144]: + - generic [ref=e145]: Recent orders + - table [ref=e149]: + - rowgroup [ref=e150]: + - row "Order Customer Total Status" [ref=e151]: + - columnheader "Order" [ref=e152]: + - generic [ref=e153]: Order + - columnheader "Customer" [ref=e154]: + - generic [ref=e155]: Customer + - columnheader "Total" [ref=e156]: + - generic [ref=e157]: Total + - columnheader "Status" [ref=e158]: + - generic [ref=e159]: Status + - rowgroup [ref=e160]: + - row "D-1001 alice@shop.test 30.98 EUR pending" [ref=e161]: + - cell "D-1001" [ref=e162]: + - link "D-1001" [ref=e163] [cursor=pointer]: + - /url: http://shop.test/admin/orders/1 + - cell "alice@shop.test" [ref=e164] + - cell "30.98 EUR" [ref=e165] + - cell "pending" [ref=e166]: + - generic [ref=e167]: pending + - row "D-1002 bob@shop.test 20.98 EUR paid" [ref=e168]: + - cell "D-1002" [ref=e169]: + - link "D-1002" [ref=e170] [cursor=pointer]: + - /url: http://shop.test/admin/orders/2 + - cell "bob@shop.test" [ref=e171] + - cell "20.98 EUR" [ref=e172] + - cell "paid" [ref=e173]: + - generic [ref=e174]: paid + - row "D-1003 carol@shop.test 25.98 EUR refunded" [ref=e175]: + - cell "D-1003" [ref=e176]: + - link "D-1003" [ref=e177] [cursor=pointer]: + - /url: http://shop.test/admin/orders/3 + - cell "carol@shop.test" [ref=e178] + - cell "25.98 EUR" [ref=e179] + - cell "refunded" [ref=e180]: + - generic [ref=e181]: refunded \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-33-43-842Z.yml b/.playwright-mcp/page-2026-04-12T20-33-43-842Z.yml new file mode 100644 index 00000000..c9c07aee --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-33-43-842Z.yml @@ -0,0 +1,174 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Products + - link "New product" [ref=e127] [cursor=pointer]: + - /url: http://shop.test/admin/products/create + - img [ref=e128] + - generic [ref=e130]: New product + - generic [ref=e131]: + - generic [ref=e132]: + - generic: + - img + - textbox "Search products..." [ref=e133] + - combobox [ref=e135]: + - option "All statuses" [selected] + - option "Draft" + - option "Active" + - option "Archived" + - table [ref=e139]: + - rowgroup [ref=e140]: + - row "Title Status Vendor Variants" [ref=e141]: + - columnheader "Title" [ref=e142]: + - generic [ref=e143]: Title + - columnheader "Status" [ref=e144]: + - generic [ref=e145]: Status + - columnheader "Vendor" [ref=e146]: + - generic [ref=e147]: Vendor + - columnheader "Variants" [ref=e148]: + - generic [ref=e149]: Variants + - columnheader [ref=e150] + - rowgroup [ref=e151]: + - row "Cap active Demo Brand 1 Edit" [ref=e152]: + - cell "Cap" [ref=e153]: + - link "Cap" [ref=e154] [cursor=pointer]: + - /url: http://shop.test/admin/products/3/edit + - cell "active" [ref=e155]: + - generic [ref=e156]: active + - cell "Demo Brand" [ref=e157] + - cell "1" [ref=e158] + - cell "Edit" [ref=e159]: + - link "Edit" [ref=e160] [cursor=pointer]: + - /url: http://shop.test/admin/products/3/edit + - row "Tote Bag active Demo Brand 1 Edit" [ref=e161]: + - cell "Tote Bag" [ref=e162]: + - link "Tote Bag" [ref=e163] [cursor=pointer]: + - /url: http://shop.test/admin/products/4/edit + - cell "active" [ref=e164]: + - generic [ref=e165]: active + - cell "Demo Brand" [ref=e166] + - cell "1" [ref=e167] + - cell "Edit" [ref=e168]: + - link "Edit" [ref=e169] [cursor=pointer]: + - /url: http://shop.test/admin/products/4/edit + - row "Classic Tee active Demo Brand 3 Edit" [ref=e170]: + - cell "Classic Tee" [ref=e171]: + - link "Classic Tee" [ref=e172] [cursor=pointer]: + - /url: http://shop.test/admin/products/1/edit + - cell "active" [ref=e173]: + - generic [ref=e174]: active + - cell "Demo Brand" [ref=e175] + - cell "3" [ref=e176] + - cell "Edit" [ref=e177]: + - link "Edit" [ref=e178] [cursor=pointer]: + - /url: http://shop.test/admin/products/1/edit + - row "Hoodie active Demo Brand 2 Edit" [ref=e179]: + - cell "Hoodie" [ref=e180]: + - link "Hoodie" [ref=e181] [cursor=pointer]: + - /url: http://shop.test/admin/products/2/edit + - cell "active" [ref=e182]: + - generic [ref=e183]: active + - cell "Demo Brand" [ref=e184] + - cell "2" [ref=e185] + - cell "Edit" [ref=e186]: + - link "Edit" [ref=e187] [cursor=pointer]: + - /url: http://shop.test/admin/products/2/edit + - row "Sneakers active Demo Brand 3 Edit" [ref=e188]: + - cell "Sneakers" [ref=e189]: + - link "Sneakers" [ref=e190] [cursor=pointer]: + - /url: http://shop.test/admin/products/5/edit + - cell "active" [ref=e191]: + - generic [ref=e192]: active + - cell "Demo Brand" [ref=e193] + - cell "3" [ref=e194] + - cell "Edit" [ref=e195]: + - link "Edit" [ref=e196] [cursor=pointer]: + - /url: http://shop.test/admin/products/5/edit + - row "Mug active Demo Brand 1 Edit" [ref=e197]: + - cell "Mug" [ref=e198]: + - link "Mug" [ref=e199] [cursor=pointer]: + - /url: http://shop.test/admin/products/6/edit + - cell "active" [ref=e200]: + - generic [ref=e201]: active + - cell "Demo Brand" [ref=e202] + - cell "1" [ref=e203] + - cell "Edit" [ref=e204]: + - link "Edit" [ref=e205] [cursor=pointer]: + - /url: http://shop.test/admin/products/6/edit \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-33-53-778Z.yml b/.playwright-mcp/page-2026-04-12T20-33-53-778Z.yml new file mode 100644 index 00000000..32b520b6 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-33-53-778Z.yml @@ -0,0 +1,137 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: + - generic [ref=e127]: Order D-1002 + - generic [ref=e128]: + - generic [ref=e129]: paid + - generic [ref=e130]: fulfilled + - generic [ref=e131]: Apr 10, 2026 20:32 + - button "Refund" [ref=e133]: + - img [ref=e135] + - generic [ref=e138]: Refund + - generic [ref=e139]: + - generic [ref=e140]: + - generic [ref=e141]: + - generic [ref=e142]: Items + - table [ref=e145]: + - rowgroup [ref=e146]: + - row "Product SKU Qty Total" [ref=e147]: + - columnheader "Product" [ref=e148]: + - generic [ref=e149]: Product + - columnheader "SKU" [ref=e150]: + - generic [ref=e151]: SKU + - columnheader "Qty" [ref=e152]: + - generic [ref=e153]: Qty + - columnheader "Total" [ref=e154]: + - generic [ref=e155]: Total + - rowgroup [ref=e156]: + - row "Tote Bag TOTE-001 1 14.99 EUR" [ref=e157]: + - cell "Tote Bag" [ref=e158] + - cell "TOTE-001" [ref=e159] + - cell "1" [ref=e160] + - cell "14.99 EUR" [ref=e161] + - generic [ref=e162]: + - generic [ref=e163]: + - generic [ref=e164]: Subtotal + - generic [ref=e165]: "14.99" + - generic [ref=e166]: + - generic [ref=e167]: Shipping + - generic [ref=e168]: "5.99" + - generic [ref=e169]: + - generic [ref=e170]: Tax + - generic [ref=e171]: "0.00" + - generic [ref=e172]: + - generic [ref=e173]: Total + - generic [ref=e174]: "20.98" + - generic [ref=e175]: + - generic [ref=e176]: Fulfillments + - paragraph [ref=e177]: No fulfillments yet. + - generic [ref=e178]: + - generic [ref=e179]: + - generic [ref=e180]: Customer + - generic [ref=e181]: + - generic [ref=e182]: Bob Example + - generic [ref=e183]: bob@shop.test + - generic [ref=e184]: + - generic [ref=e185]: Payment + - generic [ref=e186]: + - generic [ref=e187]: "Method: credit_card" + - generic [ref=e188]: "Status: paid" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-34-07-387Z.yml b/.playwright-mcp/page-2026-04-12T20-34-07-387Z.yml new file mode 100644 index 00000000..4f27a8dd --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-34-07-387Z.yml @@ -0,0 +1,123 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: Shipping + - generic [ref=e127]: + - link "Back" [ref=e128] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - button "New zone" [ref=e129]: + - img [ref=e131] + - img [ref=e134] + - generic [ref=e136]: New zone + - generic [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - generic [ref=e141]: Europe + - generic [ref=e142]: + - generic [ref=e143]: DE + - generic [ref=e144]: AT + - generic [ref=e145]: CH + - generic [ref=e146]: + - button "Add rate" [ref=e147]: + - img [ref=e149] + - img [ref=e152] + - generic [ref=e154]: Add rate + - button "Delete" [ref=e155]: + - img [ref=e157] + - generic [ref=e160]: Delete + - table [ref=e164]: + - rowgroup [ref=e165]: + - row "Name Type Amount" [ref=e166]: + - columnheader "Name" [ref=e167]: + - generic [ref=e168]: Name + - columnheader "Type" [ref=e169]: + - generic [ref=e170]: Type + - columnheader "Amount" [ref=e171]: + - generic [ref=e172]: Amount + - columnheader [ref=e173] + - rowgroup [ref=e174]: + - row "Standard flat 5.99 Remove" [ref=e175]: + - cell "Standard" [ref=e176] + - cell "flat" [ref=e177] + - cell "5.99" [ref=e178] + - cell "Remove" [ref=e179]: + - button "Remove" [ref=e180]: + - img [ref=e182] + - generic [ref=e185]: Remove \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-34-19-216Z.yml b/.playwright-mcp/page-2026-04-12T20-34-19-216Z.yml new file mode 100644 index 00000000..cfde9d4d --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-34-19-216Z.yml @@ -0,0 +1,38 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Brand + - heading "Mug" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 9.99 + - generic [ref=e25]: + - generic [ref=e26]: Quantity + - generic [ref=e27]: + - button "Decrease quantity" [ref=e28]: "-" + - generic [ref=e29]: "1" + - button "Increase quantity" [ref=e30]: + + - button "Add to cart" [ref=e31]: + - generic [ref=e32]: Add to cart + - paragraph [ref=e34]: Mug description. + - contentinfo [ref=e35]: + - generic [ref=e37]: + - navigation "Footer navigation" + - paragraph [ref=e38]: (c) Shop + - button "Open cart" [ref=e41]: + - img [ref=e42] + - generic [ref=e44]: Cart + - generic [ref=e45]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-34-31-378Z.yml b/.playwright-mcp/page-2026-04-12T20-34-31-378Z.yml new file mode 100644 index 00000000..cfde9d4d --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-34-31-378Z.yml @@ -0,0 +1,38 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Brand + - heading "Mug" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 9.99 + - generic [ref=e25]: + - generic [ref=e26]: Quantity + - generic [ref=e27]: + - button "Decrease quantity" [ref=e28]: "-" + - generic [ref=e29]: "1" + - button "Increase quantity" [ref=e30]: + + - button "Add to cart" [ref=e31]: + - generic [ref=e32]: Add to cart + - paragraph [ref=e34]: Mug description. + - contentinfo [ref=e35]: + - generic [ref=e37]: + - navigation "Footer navigation" + - paragraph [ref=e38]: (c) Shop + - button "Open cart" [ref=e41]: + - img [ref=e42] + - generic [ref=e44]: Cart + - generic [ref=e45]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-34-35-711Z.yml b/.playwright-mcp/page-2026-04-12T20-34-35-711Z.yml new file mode 100644 index 00000000..db5fb83e --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-34-35-711Z.yml @@ -0,0 +1,32 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - heading "Your cart" [level=1] [ref=e18] + - paragraph [ref=e19]: Review the items you are about to purchase. + - generic [ref=e20]: + - paragraph [ref=e21]: Your cart is empty. + - link "Browse collections" [ref=e22] [cursor=pointer]: + - /url: http://shop.test/collections + - contentinfo [ref=e23]: + - generic [ref=e25]: + - navigation "Footer navigation" + - paragraph [ref=e26]: (c) Shop + - button "Open cart" [ref=e29]: + - img [ref=e30] + - generic [ref=e32]: Cart + - generic [ref=e33]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-34-55-308Z.yml b/.playwright-mcp/page-2026-04-12T20-34-55-308Z.yml new file mode 100644 index 00000000..cfde9d4d --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-34-55-308Z.yml @@ -0,0 +1,38 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Brand + - heading "Mug" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 9.99 + - generic [ref=e25]: + - generic [ref=e26]: Quantity + - generic [ref=e27]: + - button "Decrease quantity" [ref=e28]: "-" + - generic [ref=e29]: "1" + - button "Increase quantity" [ref=e30]: + + - button "Add to cart" [ref=e31]: + - generic [ref=e32]: Add to cart + - paragraph [ref=e34]: Mug description. + - contentinfo [ref=e35]: + - generic [ref=e37]: + - navigation "Footer navigation" + - paragraph [ref=e38]: (c) Shop + - button "Open cart" [ref=e41]: + - img [ref=e42] + - generic [ref=e44]: Cart + - generic [ref=e45]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-35-09-693Z.yml b/.playwright-mcp/page-2026-04-12T20-35-09-693Z.yml new file mode 100644 index 00000000..cfde9d4d --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-35-09-693Z.yml @@ -0,0 +1,38 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e20]: + - generic [ref=e21]: + - paragraph [ref=e22]: Demo Brand + - heading "Mug" [level=1] [ref=e23] + - paragraph [ref=e24]: EUR 9.99 + - generic [ref=e25]: + - generic [ref=e26]: Quantity + - generic [ref=e27]: + - button "Decrease quantity" [ref=e28]: "-" + - generic [ref=e29]: "1" + - button "Increase quantity" [ref=e30]: + + - button "Add to cart" [ref=e31]: + - generic [ref=e32]: Add to cart + - paragraph [ref=e34]: Mug description. + - contentinfo [ref=e35]: + - generic [ref=e37]: + - navigation "Footer navigation" + - paragraph [ref=e38]: (c) Shop + - button "Open cart" [ref=e41]: + - img [ref=e42] + - generic [ref=e44]: Cart + - generic [ref=e45]: "0" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-36-14-207Z.yml b/.playwright-mcp/page-2026-04-12T20-36-14-207Z.yml new file mode 100644 index 00000000..d50c8679 --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-36-14-207Z.yml @@ -0,0 +1,95 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e3]: + - link "Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/storefront + - generic [ref=e5]: + - link "Search" [ref=e6] [cursor=pointer]: + - /url: http://shop.test/search + - img [ref=e7] + - link "Account" [ref=e9] [cursor=pointer]: + - /url: http://shop.test/account + - img [ref=e10] + - link "Cart" [ref=e12] [cursor=pointer]: + - /url: http://shop.test/cart + - img [ref=e13] + - main [ref=e15]: + - generic [ref=e16]: + - heading "Checkout" [level=1] [ref=e18] + - list [ref=e19]: + - listitem [ref=e20]: + - generic [ref=e21]: "1" + - generic [ref=e22]: Address + - listitem [ref=e24]: + - generic [ref=e25]: "2" + - generic [ref=e26]: Shipping + - listitem [ref=e28]: + - generic [ref=e29]: "3" + - generic [ref=e30]: Payment + - generic [ref=e31]: + - generic [ref=e33]: + - heading "Contact and shipping address" [level=2] [ref=e34] + - generic [ref=e35]: + - generic [ref=e36]: + - generic [ref=e37]: Email + - textbox "Email" [ref=e38]: alice@shop.test + - generic [ref=e39]: + - generic [ref=e40]: + - generic [ref=e41]: First name + - textbox "First name" [ref=e42] + - generic [ref=e43]: + - generic [ref=e44]: Last name + - textbox "Last name" [ref=e45] + - generic [ref=e46]: + - generic [ref=e47]: Address + - textbox "Address" [ref=e48] + - generic [ref=e49]: + - generic [ref=e50]: Apartment, suite (optional) + - textbox "Apartment, suite (optional)" [ref=e51] + - generic [ref=e52]: + - generic [ref=e53]: + - generic [ref=e54]: City + - textbox "City" [ref=e55] + - generic [ref=e56]: + - generic [ref=e57]: Postal code + - textbox "Postal code" [ref=e58] + - generic [ref=e59]: + - generic [ref=e60]: Country + - combobox "Country" [ref=e61]: + - option "Germany" [selected] + - option "Austria" + - option "Switzerland" + - option "France" + - option "Netherlands" + - option "United States" + - option "United Kingdom" + - generic [ref=e62]: + - checkbox "Billing address is the same as shipping" [checked] [ref=e63] + - generic [ref=e64]: Billing address is the same as shipping + - button "Continue to shipping" [ref=e65] + - complementary [ref=e66]: + - heading "Order summary" [level=2] [ref=e67] + - list [ref=e68]: + - listitem [ref=e69]: + - generic [ref=e71]: + - paragraph [ref=e72]: Mug + - paragraph [ref=e73]: Qty 1 + - paragraph [ref=e74]: EUR 9.99 + - generic [ref=e75]: + - generic [ref=e76]: + - generic [ref=e77]: Subtotal + - generic [ref=e78]: EUR 9.99 + - generic [ref=e79]: + - generic [ref=e80]: Shipping + - generic [ref=e81]: EUR 0.00 + - generic [ref=e82]: + - generic [ref=e83]: Total + - generic [ref=e84]: EUR 9.99 + - contentinfo [ref=e85]: + - generic [ref=e87]: + - navigation "Footer navigation" + - paragraph [ref=e88]: (c) Shop + - button "Open cart" [ref=e91]: + - img [ref=e92] + - generic [ref=e94]: Cart + - generic [ref=e95]: "1" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T20-37-25-632Z.yml b/.playwright-mcp/page-2026-04-12T20-37-25-632Z.yml new file mode 100644 index 00000000..a614240f --- /dev/null +++ b/.playwright-mcp/page-2026-04-12T20-37-25-632Z.yml @@ -0,0 +1,156 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - link "Shop Admin Demo Store" [ref=e4] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e6] + - generic [ref=e8]: + - generic [ref=e9]: Shop Admin + - generic [ref=e10]: Demo Store + - navigation [ref=e11]: + - generic [ref=e12]: + - generic [ref=e14]: Catalog + - generic [ref=e15]: + - link "Dashboard" [ref=e17] [cursor=pointer]: + - /url: http://shop.test/admin + - img [ref=e19] + - generic [ref=e21]: Dashboard + - link "Products" [ref=e23] [cursor=pointer]: + - /url: http://shop.test/admin/products + - img [ref=e25] + - generic [ref=e27]: Products + - link "Collections" [ref=e29] [cursor=pointer]: + - /url: http://shop.test/admin/collections + - img [ref=e31] + - generic [ref=e33]: Collections + - link "Customers" [ref=e35] [cursor=pointer]: + - /url: http://shop.test/admin/customers + - img [ref=e37] + - generic [ref=e39]: Customers + - link "Discounts" [ref=e41] [cursor=pointer]: + - /url: http://shop.test/admin/discounts + - img [ref=e43] + - generic [ref=e46]: Discounts + - generic [ref=e47]: + - generic [ref=e49]: Orders + - link "Orders" [ref=e52] [cursor=pointer]: + - /url: http://shop.test/admin/orders + - img [ref=e54] + - generic [ref=e56]: Orders + - generic [ref=e57]: + - generic [ref=e59]: Content + - generic [ref=e60]: + - link "Pages" [ref=e62] [cursor=pointer]: + - /url: http://shop.test/admin/pages + - img [ref=e64] + - generic [ref=e66]: Pages + - link "Navigation" [ref=e68] [cursor=pointer]: + - /url: http://shop.test/admin/navigation + - img [ref=e70] + - generic [ref=e72]: Navigation + - link "Themes" [ref=e74] [cursor=pointer]: + - /url: http://shop.test/admin/themes + - img [ref=e76] + - generic [ref=e78]: Themes + - generic [ref=e79]: + - generic [ref=e81]: Marketing + - link "Analytics" [ref=e84] [cursor=pointer]: + - /url: http://shop.test/admin/analytics + - img [ref=e86] + - generic [ref=e88]: Analytics + - generic [ref=e89]: + - generic [ref=e91]: Configuration + - generic [ref=e92]: + - link "Settings" [ref=e94] [cursor=pointer]: + - /url: http://shop.test/admin/settings + - img [ref=e96] + - generic [ref=e99]: Settings + - link "Apps" [ref=e101] [cursor=pointer]: + - /url: http://shop.test/admin/apps + - img [ref=e103] + - generic [ref=e105]: Apps + - link "Developers" [ref=e107] [cursor=pointer]: + - /url: http://shop.test/admin/developers + - img [ref=e109] + - generic [ref=e111]: Developers + - button "SA Shop Admin" [ref=e114]: + - generic [ref=e117]: SA + - generic [ref=e118]: Shop Admin + - img [ref=e120] + - generic [ref=e124]: + - generic [ref=e125]: Orders + - generic [ref=e126]: + - generic [ref=e127]: + - generic: + - img + - textbox "Search orders..." [ref=e128] + - combobox [ref=e130]: + - option "All payments" [selected] + - option "Pending" + - option "Paid" + - option "Refunded" + - option "Partially refunded" + - combobox [ref=e131]: + - option "All fulfillment" [selected] + - option "Unfulfilled" + - option "Partial" + - option "Fulfilled" + - table [ref=e135]: + - rowgroup [ref=e136]: + - row "Order Customer Total Payment Fulfillment Date" [ref=e137]: + - columnheader "Order" [ref=e138]: + - generic [ref=e139]: Order + - columnheader "Customer" [ref=e140]: + - generic [ref=e141]: Customer + - columnheader "Total" [ref=e142]: + - generic [ref=e143]: Total + - columnheader "Payment" [ref=e144]: + - generic [ref=e145]: Payment + - columnheader "Fulfillment" [ref=e146]: + - generic [ref=e147]: Fulfillment + - columnheader "Date" [ref=e148]: + - generic [ref=e149]: Date + - rowgroup [ref=e150]: + - row "#1001 final@shop.test 15.98 EUR paid unfulfilled Apr 12, 2026" [ref=e151]: + - cell "#1001" [ref=e152]: + - link "#1001" [ref=e153] [cursor=pointer]: + - /url: http://shop.test/admin/orders/4 + - cell "final@shop.test" [ref=e154] + - cell "15.98 EUR" [ref=e155] + - cell "paid" [ref=e156]: + - generic [ref=e157]: paid + - cell "unfulfilled" [ref=e158]: + - generic [ref=e159]: unfulfilled + - cell "Apr 12, 2026" [ref=e160] + - row "D-1001 alice@shop.test 30.98 EUR pending unfulfilled Apr 11, 2026" [ref=e161]: + - cell "D-1001" [ref=e162]: + - link "D-1001" [ref=e163] [cursor=pointer]: + - /url: http://shop.test/admin/orders/1 + - cell "alice@shop.test" [ref=e164] + - cell "30.98 EUR" [ref=e165] + - cell "pending" [ref=e166]: + - generic [ref=e167]: pending + - cell "unfulfilled" [ref=e168]: + - generic [ref=e169]: unfulfilled + - cell "Apr 11, 2026" [ref=e170] + - row "D-1002 bob@shop.test 20.98 EUR paid fulfilled Apr 10, 2026" [ref=e171]: + - cell "D-1002" [ref=e172]: + - link "D-1002" [ref=e173] [cursor=pointer]: + - /url: http://shop.test/admin/orders/2 + - cell "bob@shop.test" [ref=e174] + - cell "20.98 EUR" [ref=e175] + - cell "paid" [ref=e176]: + - generic [ref=e177]: paid + - cell "fulfilled" [ref=e178]: + - generic [ref=e179]: fulfilled + - cell "Apr 10, 2026" [ref=e180] + - row "D-1003 carol@shop.test 25.98 EUR refunded fulfilled Apr 09, 2026" [ref=e181]: + - cell "D-1003" [ref=e182]: + - link "D-1003" [ref=e183] [cursor=pointer]: + - /url: http://shop.test/admin/orders/3 + - cell "carol@shop.test" [ref=e184] + - cell "25.98 EUR" [ref=e185] + - cell "refunded" [ref=e186]: + - generic [ref=e187]: refunded + - cell "fulfilled" [ref=e188]: + - generic [ref=e189]: fulfilled + - cell "Apr 09, 2026" [ref=e190] \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 7b0f1e95..c29b66df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,432 +29,217 @@ The complete specification is in `specs/`. Start with `specs/09-IMPLEMENTATION-R # Laravel Boost Guidelines -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. ## Foundational Context + This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.4.17 +- php - 8.4 +- laravel/fortify (FORTIFY) - v1 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - livewire/flux (FLUXUI_FREE) - v2 - livewire/livewire (LIVEWIRE) - v4 +- laravel/boost (BOOST) - v2 +- laravel/mcp (MCP) - v0 +- laravel/pail (PAIL) - v1 - laravel/pint (PINT) - v1 +- laravel/sail (SAIL) - v1 - pestphp/pest (PEST) - v4 +- phpunit/phpunit (PHPUNIT) - v12 - tailwindcss (TAILWINDCSS) - v4 +## Skills Activation + +This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. + +- `developing-with-fortify` — Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications. +- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. +- `fluxui-development` — Use this skill for Flux UI development in Livewire applications only. Trigger when working with components, building or customizing Livewire component UIs, creating forms, modals, tables, or other interactive elements. Covers: flux: components (buttons, inputs, modals, forms, tables, date-pickers, kanban, badges, tooltips, etc.), component composition, Tailwind CSS styling, Heroicons/Lucide icon integration, validation patterns, responsive design, and theming. Do not use for non-Livewire frameworks or non-component styling. +- `livewire-development` — Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, wire:sort, or islands, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, drag-and-drop, loading states, migrating from Livewire 3 to 4, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire. +- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code. +- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS. ## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. + +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. - Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. - Check for existing components to reuse before writing a new one. ## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. ## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. + +- Stick to existing directory structure; don't create new base folders without approval. - Do not change the application's dependencies without approval. ## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. ## Documentation Files + - You must only create documentation files if explicitly requested by the user. +## Replies + +- Be concise in your explanations - focus on what's important rather than explaining obvious details. === boost rules === -## Laravel Boost -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. +# Laravel Boost -## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. +## Tools -## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. +- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads. +- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker. +- Use `database-schema` to inspect table structure before writing migrations or models. +- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. +- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries. -## Tinker / Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. +## Searching Documentation (IMPORTANT) -## Reading Browser Logs With the `browser-logs` Tool -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. +- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. +- Pass a `packages` array to scope results when you know which packages are relevant. +- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first. +- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`. -## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. +### Search Syntax -### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. +1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit". +2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order. +3. Combine words and phrases for mixed queries: `middleware "rate limit"`. +4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`. -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms +## Artisan +- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. +- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`. +- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory. +- To check environment variables, read the `.env` file directly. -=== php rules === +## Tinker -## PHP +- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code. +- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'` + - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'` -- Always use curly braces for control structures, even if it has one line. +=== php rules === -### Constructors -- Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. +# PHP -### Type Declarations -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. +- Always use curly braces for control structures, even for single-line bodies. +- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. +- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` +- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. +- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. +- Use array shape type definitions in PHPDoc blocks. - -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} - +=== herd rules === -## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. +# Laravel Herd -## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. +- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. +- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. +=== tests rules === +# Test Enforcement -=== herd rules === +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. -## Laravel Herd +=== fortify/core rules === -- The application is served by Laravel Herd and will be available at: https?://[kebab-case-project-dir].test. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs. -- You must not run any commands to make the site available via HTTP(s). It is _always_ available through Laravel Herd. +# Laravel Fortify +- Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications. +- IMPORTANT: Always use the `search-docs` tool for detailed Laravel Fortify patterns and documentation. +- IMPORTANT: Activate `developing-with-fortify` skill when working with Fortify authentication features. === laravel/core rules === -## Do Things the Laravel Way +# Do Things the Laravel Way -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. +- If you're creating a generic PHP class, use `php artisan make:class`. - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. -### Database -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - ### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. -### APIs & Eloquent Resources -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. -### Controllers & Validation -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. +## APIs & Eloquent Resources -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. -### Authentication & Authorization -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). +## URL Generation -### URL Generation - When generating links to other pages, prefer named routes and the `route()` function. -### Configuration -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. +## Testing -### Testing - When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. - Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +## Vite Error -### Vite Error - If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. +## Deployment + +- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. === laravel/v12 rules === -## Laravel 12 +# Laravel 12 -- Use the `search-docs` tool to get version specific documentation. +- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples. - Since Laravel 11, Laravel has a new streamlined file structure which this project uses. -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. +## Laravel 12 Structure + +- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`. +- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`. - `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. - `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. +- The `app/Console/Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration. +- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration. + +## Database -### Database - When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. +- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. ### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - - -=== fluxui-free/core rules === - -## Flux UI Free - -- This project is using the free edition of Flux UI. It has full access to the free components and variants, but does not have access to the Pro components. -- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted, UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. -- You should use Flux UI components when available. -- Fallback to standard Blade components if Flux is unavailable. -- If available, use Laravel Boost's `search-docs` tool to get the exact documentation and code snippets available for this project. -- Flux UI components look like this: - - - - - - -### Available Components -This is correct as of Boost installation, but there may be additional components within the codebase. - - -avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip - +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. === livewire/core rules === -## Livewire Core -- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. -- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components -- State should live on the server, with the UI reflecting it. -- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. - -## Livewire Best Practices -- Livewire components require a single root element. -- Use `wire:loading` and `wire:dirty` for delightful loading states. -- Add `wire:key` in loops: - - ```blade - @foreach ($items as $item) -
- {{ $item->name }} -
- @endforeach - ``` - -- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: - - - public function mount(User $user) { $this->user = $user; } - public function updatedSearch() { $this->resetPage(); } - - - -## Testing Livewire - - - Livewire::test(Counter::class) - ->assertSet('count', 0) - ->call('increment') - ->assertSet('count', 1) - ->assertSee(1) - ->assertStatus(200); - - - - - $this->get('/posts/create') - ->assertSeeLivewire(CreatePost::class); - +# Livewire +- Livewire allow to build dynamic, reactive interfaces in PHP without writing JavaScript. +- You can use Alpine.js for client-side interactions instead of JavaScript frameworks. +- Keep state server-side so the UI reflects it. Validate and authorize in actions as you would in HTTP requests. === pint/core rules === -## Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. +# Laravel Pint Code Formatter +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. === pest/core rules === ## Pest -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - - - -=== pest/v4 rules === - -## Pest 4 - -- Pest v4 is a huge upgrade to Pest and offers: browser testing, smoke testing, visual regression testing, test sharding, and faster type coverage. -- Browser testing is incredibly powerful and useful for this project. -- Browser tests should live in `tests/Browser/`. -- Use the `search-docs` tool for detailed guidance on utilizing these features. - -### Browser Testing -- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test. -- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test. -- If requested, test on multiple browsers (Chrome, Firefox, Safari). -- If requested, test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints). -- Switch color schemes (light/dark mode) when appropriate. -- Take screenshots or pause tests for debugging when appropriate. - -### Example Tests - - -it('may reset the password', function () { - Notification::fake(); - - $this->actingAs(User::factory()->create()); - - $page = visit('/sign-in'); // Visit on a real browser... - - $page->assertSee('Sign In') - ->assertNoJavascriptErrors() // or ->assertNoConsoleLogs() - ->click('Forgot Password?') - ->fill('email', 'nuno@laravel.com') - ->click('Send Reset Link') - ->assertSee('We have emailed your password reset link!') - - Notification::assertSent(ResetPassword::class); -}); - - - +- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. +- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. +- Do NOT delete tests without approval. - -$pages = visit(['/', '/about', '/contact']); - -$pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); - - - -=== tailwindcss/core rules === - -## Tailwind Core - -- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. -- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) -- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically -- You can use the `search-docs` tool to get exact examples from the official documentation when needed. - -### Spacing -- When listing items, use gap utilities for spacing, don't use margins. - - -
-
Superior
-
Michigan
-
Erie
-
-
- - -### Dark Mode -- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. - - -=== tailwindcss/v4 rules === - -## Tailwind 4 - -- Always use Tailwind CSS v4 - do not use the deprecated utilities. -- `corePlugins` is not supported in Tailwind v4. -- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - - - - -### Replaced Utilities -- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. -- Opacity values are still numeric. - -| Deprecated | Replacement | -|------------+--------------| -| bg-opacity-* | bg-black/* | -| text-opacity-* | text-black/* | -| border-opacity-* | border-black/* | -| divide-opacity-* | divide-black/* | -| ring-opacity-* | ring-black/* | -| placeholder-opacity-* | placeholder-black/* | -| flex-shrink-* | shrink-* | -| flex-grow-* | grow-* | -| overflow-ellipsis | text-ellipsis | -| decoration-slice | box-decoration-slice | -| decoration-clone | box-decoration-clone | - - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. diff --git a/README.md b/README.md new file mode 100644 index 00000000..932a9ad4 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +Your mission is to implement an entire shop system based on the specifications im specs/*. You must do in one go without stopping. You must use team mode! You must test everything via Pest (unit, and functional tests). You must also additional simulate user behaviour using the Playwright MPC and confirm that all acceptance criterias are met. If you find bugs, you must fix them. The result is a perfect shop system. All requirements are perfectly implemented. All acceptance criterias are met, tested and confirmed by you. + +Continuously keep track of the progress in specs/progress.md Commit your progress after every relevant iteration with a meaningful message. + +When implementation is fully done, then make a full review meeting and showcase all features (customer- and admin-side) to me. In case bugs appear, you must fix them all and restart the review meeting. + +Use team-mode (see https://code.claude.com/docs/en/agent-teams), not sub-agents. diff --git a/app/Auth/CustomerUserProvider.php b/app/Auth/CustomerUserProvider.php new file mode 100644 index 00000000..2637fdd9 --- /dev/null +++ b/app/Auth/CustomerUserProvider.php @@ -0,0 +1,157 @@ + $model + */ + public function __construct( + protected Hasher $hasher, + protected string $model, + ) {} + + public function retrieveById($identifier): ?Authenticatable + { + $query = $this->newModelQuery(); + $this->constrainToCurrentStore($query); + + /** @var Authenticatable|null $model */ + $model = $query->find($identifier); + + return $model; + } + + public function retrieveByToken($identifier, $token): ?Authenticatable + { + $model = $this->createModel(); + + $query = $this->newModelQuery() + ->where($model->getAuthIdentifierName(), $identifier); + + $this->constrainToCurrentStore($query); + + /** @var Authenticatable|null $retrieved */ + $retrieved = $query->first(); + + if ($retrieved === null) { + return null; + } + + $rememberToken = $retrieved->getRememberToken(); + + if ($rememberToken !== null && hash_equals($rememberToken, $token)) { + return $retrieved; + } + + return null; + } + + public function updateRememberToken(Authenticatable $user, $token): void + { + $user->setRememberToken($token); + + $timestamps = $user->timestamps; + $user->timestamps = false; + $user->save(); + $user->timestamps = $timestamps; + } + + /** + * @param array $credentials + */ + public function retrieveByCredentials(array $credentials): ?Authenticatable + { + if (empty($credentials) + || (count($credentials) === 1 && Str::contains(array_key_first($credentials), 'password')) + ) { + return null; + } + + $query = $this->newModelQuery(); + $this->constrainToCurrentStore($query); + + foreach ($credentials as $key => $value) { + if (Str::contains($key, 'password')) { + continue; + } + + if (is_array($value) || $value instanceof \Illuminate\Contracts\Support\Arrayable) { + $query->whereIn($key, $value); + } else { + $query->where($key, $value); + } + } + + /** @var Authenticatable|null $result */ + $result = $query->first(); + + return $result; + } + + /** + * @param array $credentials + */ + public function validateCredentials(Authenticatable $user, array $credentials): bool + { + $plain = $credentials['password'] ?? null; + + if (! is_string($plain) || $plain === '') { + return false; + } + + return $this->hasher->check($plain, $user->getAuthPassword()); + } + + /** + * @param array $credentials + */ + public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void + { + if (! $this->hasher->needsRehash($user->getAuthPassword()) && ! $force) { + return; + } + + $plain = $credentials['password'] ?? null; + + if (! is_string($plain) || $plain === '') { + return; + } + + $user->forceFill([ + $user->getAuthPasswordName() => $this->hasher->make($plain), + ])->save(); + } + + protected function createModel(): Model + { + $class = '\\'.ltrim($this->model, '\\'); + + return new $class; + } + + protected function newModelQuery(): \Illuminate\Database\Eloquent\Builder + { + return $this->createModel()->newQuery(); + } + + protected function constrainToCurrentStore(\Illuminate\Database\Eloquent\Builder $query): void + { + if (! app()->bound('current_store')) { + return; + } + + /** @var Store $store */ + $store = app('current_store'); + + $query->where('store_id', $store->id); + } +} diff --git a/app/Contracts/PaymentProvider.php b/app/Contracts/PaymentProvider.php new file mode 100644 index 00000000..bb56fc8d --- /dev/null +++ b/app/Contracts/PaymentProvider.php @@ -0,0 +1,19 @@ + $details + */ + public function charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult; + + public function refund(Payment $payment, int $amount): RefundResult; +} diff --git a/app/Enums/CartStatus.php b/app/Enums/CartStatus.php new file mode 100644 index 00000000..56a92071 --- /dev/null +++ b/app/Enums/CartStatus.php @@ -0,0 +1,10 @@ + $this->resolveFromHost($request), + 'admin' => $this->resolveFromSession($request), + default => null, + }; + + if ($store === null) { + throw new NotFoundHttpException('Store not found.'); + } + + if ($store->status === StoreStatus::Suspended) { + throw new HttpException(503, 'Store is temporarily unavailable.'); + } + + app()->instance('current_store', $store); + + return $next($request); + } + + protected function resolveFromHost(Request $request): ?Store + { + $hostname = $request->getHost(); + + $storeId = Cache::remember( + 'store_domain:'.$hostname, + now()->addMinutes(5), + function () use ($hostname): ?int { + $domain = StoreDomain::query() + ->where('hostname', $hostname) + ->first(); + + return $domain?->store_id; + } + ); + + if ($storeId === null) { + return null; + } + + return Store::query()->find($storeId); + } + + protected function resolveFromSession(Request $request): ?Store + { + $storeId = $request->session()->get('current_store_id'); + + if ($storeId === null) { + return null; + } + + $user = Auth::guard('web')->user(); + + if ($user === null) { + return null; + } + + $store = Store::query()->find($storeId); + + if ($store === null) { + return null; + } + + $hasAccess = $store->users() + ->where('users.id', $user->id) + ->exists(); + + if (! $hasAccess) { + return null; + } + + return $store; + } +} diff --git a/app/Jobs/AggregateAnalytics.php b/app/Jobs/AggregateAnalytics.php new file mode 100644 index 00000000..09ee1440 --- /dev/null +++ b/app/Jobs/AggregateAnalytics.php @@ -0,0 +1,73 @@ +date !== null + ? CarbonImmutable::parse($this->date)->startOfDay() + : CarbonImmutable::yesterday()->startOfDay(); + + $dateString = $date->toDateString(); + $start = $date->startOfDay(); + $end = $date->endOfDay(); + + Store::query()->each(function (Store $store) use ($dateString, $start, $end): void { + $orders = Order::query() + ->where('store_id', $store->id) + ->whereNotNull('placed_at') + ->whereBetween('placed_at', [$start, $end]) + ->get(['id', 'total_amount']); + + $ordersCount = $orders->count(); + $revenue = (int) $orders->sum('total_amount'); + $aov = $ordersCount > 0 ? (int) round($revenue / $ordersCount) : 0; + + $events = AnalyticsEvent::query() + ->where('store_id', $store->id) + ->whereBetween('occurred_at', [$start, $end]) + ->get(['type', 'session_id']); + + $visits = (int) $events + ->where('type', 'page_view') + ->pluck('session_id') + ->filter() + ->unique() + ->count(); + + $addToCart = (int) $events->where('type', 'add_to_cart')->count(); + $checkoutStarted = (int) $events->where('type', 'checkout_started')->count(); + $checkoutCompleted = (int) $events->where('type', 'checkout_completed')->count(); + + DB::table('analytics_daily')->updateOrInsert( + ['store_id' => $store->id, 'date' => $dateString], + [ + 'orders_count' => $ordersCount, + 'revenue_amount' => $revenue, + 'aov_amount' => $aov, + 'visits_count' => $visits, + 'add_to_cart_count' => $addToCart, + 'checkout_started_count' => $checkoutStarted, + 'checkout_completed_count' => $checkoutCompleted, + ] + ); + }); + } +} diff --git a/app/Jobs/CancelUnpaidBankTransferOrders.php b/app/Jobs/CancelUnpaidBankTransferOrders.php new file mode 100644 index 00000000..5de4f7cf --- /dev/null +++ b/app/Jobs/CancelUnpaidBankTransferOrders.php @@ -0,0 +1,28 @@ +withoutGlobalScopes() + ->where('payment_method', PaymentMethod::BankTransfer->value) + ->where('financial_status', FinancialStatus::Pending->value) + ->where('placed_at', '<', now()->subDays(7)) + ->get() + ->each(function (Order $order) use ($orderService): void { + $orderService->cancel($order, 'Bank transfer not received within 7 days.'); + }); + } +} diff --git a/app/Jobs/CleanupAbandonedCarts.php b/app/Jobs/CleanupAbandonedCarts.php new file mode 100644 index 00000000..8027ac7f --- /dev/null +++ b/app/Jobs/CleanupAbandonedCarts.php @@ -0,0 +1,22 @@ +withoutGlobalScopes() + ->where('status', CartStatus::Active->value) + ->where('updated_at', '<', now()->subDays(14)) + ->update(['status' => CartStatus::Abandoned->value]); + } +} diff --git a/app/Jobs/DeliverWebhook.php b/app/Jobs/DeliverWebhook.php new file mode 100644 index 00000000..87b0f785 --- /dev/null +++ b/app/Jobs/DeliverWebhook.php @@ -0,0 +1,103 @@ + + */ + public array $backoff = [60, 300, 1800, 7200, 43200]; + + /** + * @param array $payload + */ + public function __construct( + public readonly WebhookSubscription $subscription, + public readonly string $eventType, + public readonly array $payload, + ) {} + + public function handle(WebhookService $webhookService): void + { + $subscription = $this->subscription->fresh() ?? $this->subscription; + + if ($subscription->status !== 'active') { + return; + } + + $body = json_encode($this->payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''; + $signature = $webhookService->sign($body, (string) $subscription->secret); + $deliveryId = (string) Str::uuid(); + $timestamp = (string) now()->getTimestamp(); + + $delivery = WebhookDelivery::create([ + 'subscription_id' => $subscription->id, + 'event_type' => $this->eventType, + 'payload_json' => $this->payload, + 'attempts' => $this->attempts(), + ]); + + $response = null; + + try { + /** @var Response $response */ + $response = Http::timeout(10) + ->withHeaders([ + 'X-Platform-Signature' => $signature, + 'X-Platform-Event' => $this->eventType, + 'X-Platform-Delivery-Id' => $deliveryId, + 'X-Platform-Timestamp' => $timestamp, + 'Content-Type' => 'application/json', + ]) + ->withBody($body, 'application/json') + ->post((string) $subscription->url); + } catch (Throwable $exception) { + $delivery->update([ + 'response_body' => mb_substr($exception->getMessage(), 0, 2000), + ]); + $this->recordFailure($subscription); + throw $exception; + } + + $delivery->update([ + 'response_status' => $response->status(), + 'response_body' => mb_substr((string) $response->body(), 0, 2000), + 'delivered_at' => now(), + ]); + + if ($response->successful()) { + return; + } + + $this->recordFailure($subscription); + throw new \RuntimeException('Webhook responded with non-2xx status: '.$response->status()); + } + + private function recordFailure(WebhookSubscription $subscription): void + { + $subscription->increment('failed_count'); + $subscription->refresh(); + + if ($subscription->failed_count >= 5 && $subscription->status === 'active') { + $subscription->update(['status' => 'paused']); + } + } +} diff --git a/app/Jobs/ExpireAbandonedCheckouts.php b/app/Jobs/ExpireAbandonedCheckouts.php new file mode 100644 index 00000000..fb8297bb --- /dev/null +++ b/app/Jobs/ExpireAbandonedCheckouts.php @@ -0,0 +1,30 @@ +withoutGlobalScopes() + ->whereNotNull('expires_at') + ->where('expires_at', '<', now()) + ->whereNotIn('status', [ + CheckoutStatus::Completed->value, + CheckoutStatus::Expired->value, + ]) + ->get() + ->each(function (Checkout $checkout) use ($checkoutService): void { + $checkoutService->expire($checkout); + }); + } +} diff --git a/app/Jobs/ProcessMediaUpload.php b/app/Jobs/ProcessMediaUpload.php new file mode 100644 index 00000000..e5a7f910 --- /dev/null +++ b/app/Jobs/ProcessMediaUpload.php @@ -0,0 +1,27 @@ + $this->media->id, + 'storage_key' => $this->media->storage_key, + ]); + + $this->media->status = MediaStatus::Ready; + $this->media->save(); + } +} diff --git a/app/Listeners/DispatchOrderWebhooks.php b/app/Listeners/DispatchOrderWebhooks.php new file mode 100644 index 00000000..14ca3991 --- /dev/null +++ b/app/Listeners/DispatchOrderWebhooks.php @@ -0,0 +1,39 @@ +dispatch('order.created', $event->order); + } + + public function handlePaid(OrderPaid $event): void + { + $this->dispatch('order.paid', $event->order); + } + + public function handleFulfilled(OrderFulfilled $event): void + { + $this->dispatch('order.fulfilled', $event->order); + } + + private function dispatch(string $eventType, Order $order): void + { + $this->webhookService->dispatch($order->store, $eventType, [ + 'order_id' => $order->id, + 'order_number' => $order->order_number, + 'total_amount' => $order->total_amount, + 'currency' => $order->currency, + ]); + } +} diff --git a/app/Livewire/Admin/Analytics/Index.php b/app/Livewire/Admin/Analytics/Index.php new file mode 100644 index 00000000..32686a95 --- /dev/null +++ b/app/Livewire/Admin/Analytics/Index.php @@ -0,0 +1,49 @@ +endDate = now()->toDateString(); + $this->startDate = now()->subDays(29)->toDateString(); + } + + public function render(AnalyticsService $analytics): View + { + /** @var Store $store */ + $store = app('current_store'); + + $metrics = $analytics->getDailyMetrics($store, $this->startDate, $this->endDate); + + $totals = [ + 'revenue' => (int) $metrics->sum('revenue_amount'), + 'orders' => (int) $metrics->sum('orders_count'), + 'visits' => (int) $metrics->sum('visits_count'), + 'checkouts_started' => (int) $metrics->sum('checkout_started_count'), + 'checkouts_completed' => (int) $metrics->sum('checkout_completed_count'), + ]; + + $totals['aov'] = $totals['orders'] > 0 + ? (int) round($totals['revenue'] / $totals['orders']) + : 0; + + return view('livewire.admin.analytics.index', [ + 'metrics' => $metrics, + 'totals' => $totals, + 'currency' => $store->default_currency ?? 'EUR', + ]); + } +} diff --git a/app/Livewire/Admin/Apps/Index.php b/app/Livewire/Admin/Apps/Index.php new file mode 100644 index 00000000..951859af --- /dev/null +++ b/app/Livewire/Admin/Apps/Index.php @@ -0,0 +1,71 @@ +updateOrCreate( + ['store_id' => $store->id, 'app_id' => $appId], + [ + 'status' => 'active', + 'installed_at' => now(), + ] + ); + + session()->flash('status', 'App installed.'); + } + + public function uninstall(int $appId): void + { + /** @var Store $store */ + $store = app('current_store'); + + AppInstallation::query() + ->where('store_id', $store->id) + ->where('app_id', $appId) + ->update(['status' => 'uninstalled']); + + session()->flash('status', 'App uninstalled.'); + } + + public function render(): View + { + /** @var Store $store */ + $store = app('current_store'); + + $installations = AppInstallation::query() + ->where('store_id', $store->id) + ->pluck('status', 'app_id'); + + $apps = App::query() + ->orderBy('name') + ->get() + ->map(function (App $app) use ($installations): App { + $status = $installations->get($app->id); + $app->setAttribute('installation_status', $status); + + return $app; + }); + + $installedApps = $apps->filter(fn (App $app) => $app->getAttribute('installation_status') === 'active')->values(); + $marketplaceApps = $apps->filter(fn (App $app) => $app->getAttribute('installation_status') !== 'active')->values(); + + return view('livewire.admin.apps.index', [ + 'installedApps' => $installedApps, + 'marketplaceApps' => $marketplaceApps, + ]); + } +} diff --git a/app/Livewire/Admin/Auth/Login.php b/app/Livewire/Admin/Auth/Login.php new file mode 100644 index 00000000..22de5081 --- /dev/null +++ b/app/Livewire/Admin/Auth/Login.php @@ -0,0 +1,78 @@ +validate(); + + $throttleKey = $this->throttleKey(); + + if (RateLimiter::tooManyAttempts($throttleKey, 5)) { + $seconds = RateLimiter::availableIn($throttleKey); + + throw ValidationException::withMessages([ + 'email' => __('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => (int) ceil($seconds / 60), + ]), + ]); + } + + if (! Auth::guard('web')->attempt( + ['email' => $this->email, 'password' => $this->password], + $this->remember, + )) { + RateLimiter::hit($throttleKey, 60); + + throw ValidationException::withMessages([ + 'email' => __('auth.failed'), + ]); + } + + RateLimiter::clear($throttleKey); + + Session::regenerate(); + + $user = Auth::guard('web')->user(); + + $firstStore = $user?->stores()->first(); + + if ($firstStore !== null) { + Session::put('current_store_id', $firstStore->id); + } + + $user?->forceFill(['last_login_at' => now()])->save(); + + return redirect()->intended('/admin'); + } + + protected function throttleKey(): string + { + return 'login:'.strtolower($this->email).'|'.request()->ip(); + } + + #[Layout('components.layouts.admin-auth')] + public function render(): \Illuminate\Contracts\View\View + { + return view('livewire.admin.auth.login'); + } +} diff --git a/app/Livewire/Admin/Collections/Form.php b/app/Livewire/Admin/Collections/Form.php new file mode 100644 index 00000000..c2fee0ea --- /dev/null +++ b/app/Livewire/Admin/Collections/Form.php @@ -0,0 +1,132 @@ + */ + public array $productIds = []; + + public string $productSearch = ''; + + public function mount(?Collection $collection = null): void + { + if ($collection !== null && $collection->exists) { + $this->collection = $collection; + $this->mode = 'edit'; + $this->title = (string) $collection->title; + $this->handle = (string) $collection->handle; + $this->description = (string) ($collection->description_html ?? ''); + $this->type = $collection->type->value; + $this->status = $collection->status->value; + $this->productIds = $collection->products()->pluck('products.id')->map(fn ($id) => (int) $id)->all(); + } + } + + public function addProduct(int $productId): void + { + if (! in_array($productId, $this->productIds, true)) { + $this->productIds[] = $productId; + } + } + + public function removeProduct(int $productId): void + { + $this->productIds = array_values(array_filter($this->productIds, fn (int $id): bool => $id !== $productId)); + } + + public function save(): mixed + { + $this->validate(); + + /** @var Store $store */ + $store = app('current_store'); + + if ($this->mode === 'create') { + $handle = $this->handle !== '' + ? HandleGenerator::generate($this->handle, 'collections', $store->id) + : HandleGenerator::generate($this->title, 'collections', $store->id); + + $collection = Collection::create([ + 'store_id' => $store->id, + 'title' => $this->title, + 'handle' => $handle, + 'description_html' => $this->description !== '' ? $this->description : null, + 'type' => $this->type, + 'status' => $this->status, + ]); + } else { + $collection = $this->collection; + $handle = $this->handle !== '' && $this->handle !== $collection->handle + ? HandleGenerator::generate($this->handle, 'collections', $store->id, $collection->id) + : $collection->handle; + + $collection->update([ + 'title' => $this->title, + 'handle' => $handle, + 'description_html' => $this->description !== '' ? $this->description : null, + 'type' => $this->type, + 'status' => $this->status, + ]); + } + + $syncData = []; + foreach ($this->productIds as $position => $id) { + $syncData[$id] = ['position' => $position]; + } + $collection->products()->sync($syncData); + + session()->flash('status', 'Collection saved.'); + + return redirect()->route('admin.collections.index'); + } + + public function render(): View + { + $searchResults = $this->productSearch !== '' + ? Product::query() + ->where('title', 'like', '%'.$this->productSearch.'%') + ->whereNotIn('id', $this->productIds) + ->limit(10) + ->get() + : collect(); + + $assignedProducts = $this->productIds === [] + ? collect() + : Product::query()->whereIn('id', $this->productIds)->get(); + + return view('livewire.admin.collections.form', [ + 'searchResults' => $searchResults, + 'assignedProducts' => $assignedProducts, + ]); + } +} diff --git a/app/Livewire/Admin/Collections/Index.php b/app/Livewire/Admin/Collections/Index.php new file mode 100644 index 00000000..4689e944 --- /dev/null +++ b/app/Livewire/Admin/Collections/Index.php @@ -0,0 +1,39 @@ +resetPage(); + } + + public function render(): View + { + $collections = Collection::query() + ->withCount('products') + ->when($this->search !== '', fn ($q) => $q->where('title', 'like', '%'.$this->search.'%')) + ->latest() + ->paginate($this->perPage); + + return view('livewire.admin.collections.index', [ + 'collections' => $collections, + ]); + } +} diff --git a/app/Livewire/Admin/Customers/Index.php b/app/Livewire/Admin/Customers/Index.php new file mode 100644 index 00000000..f22f6490 --- /dev/null +++ b/app/Livewire/Admin/Customers/Index.php @@ -0,0 +1,45 @@ +resetPage(); + } + + public function render(): View + { + $customers = Customer::query() + ->withCount('orders') + ->withSum('orders as total_spent', 'total_amount') + ->when($this->search !== '', function ($q): void { + $q->where(function ($query): void { + $query->where('email', 'like', '%'.$this->search.'%') + ->orWhere('name', 'like', '%'.$this->search.'%'); + }); + }) + ->latest() + ->paginate($this->perPage); + + return view('livewire.admin.customers.index', [ + 'customers' => $customers, + ]); + } +} diff --git a/app/Livewire/Admin/Customers/Show.php b/app/Livewire/Admin/Customers/Show.php new file mode 100644 index 00000000..72a13848 --- /dev/null +++ b/app/Livewire/Admin/Customers/Show.php @@ -0,0 +1,34 @@ +customer = $customer->load(['addresses', 'orders' => fn ($q) => $q->latest()->limit(20)]); + } + + public function render(): View + { + $orders = $this->customer->orders; + + $stats = [ + 'orders_count' => $orders->count(), + 'total_spent' => (int) $orders->sum('total_amount'), + 'average' => $orders->count() > 0 ? (int) round($orders->sum('total_amount') / $orders->count()) : 0, + ]; + + return view('livewire.admin.customers.show', [ + 'stats' => $stats, + ]); + } +} diff --git a/app/Livewire/Admin/Dashboard.php b/app/Livewire/Admin/Dashboard.php new file mode 100644 index 00000000..aa474e8b --- /dev/null +++ b/app/Livewire/Admin/Dashboard.php @@ -0,0 +1,65 @@ +periodStart(); + + $query = Order::query()->where('placed_at', '>=', $since); + + $totalSales = (int) $query->clone()->sum('total_amount'); + $ordersCount = (int) $query->clone()->count(); + $aov = $ordersCount > 0 ? (int) round($totalSales / $ordersCount) : 0; + + return [ + 'total_sales' => $totalSales, + 'orders_count' => $ordersCount, + 'aov' => $aov, + 'conversion_rate' => null, + ]; + } + + /** + * @return \Illuminate\Support\Collection + */ + #[Computed] + public function recentOrders(): \Illuminate\Support\Collection + { + return Order::query() + ->with('customer') + ->latest('placed_at') + ->limit(10) + ->get(); + } + + protected function periodStart(): DateTimeInterface + { + return match ($this->period) { + '7d' => now()->subDays(7), + '90d' => now()->subDays(90), + default => now()->subDays(30), + }; + } + + public function render(): View + { + return view('livewire.admin.dashboard'); + } +} diff --git a/app/Livewire/Admin/Developers/Index.php b/app/Livewire/Admin/Developers/Index.php new file mode 100644 index 00000000..7f54adbe --- /dev/null +++ b/app/Livewire/Admin/Developers/Index.php @@ -0,0 +1,100 @@ +validateOnly('newTokenName'); + + /** @var User $user */ + $user = Auth::user(); + + $token = $user->createToken($this->newTokenName); + $this->plaintextToken = $token->plainTextToken; + $this->newTokenName = ''; + + session()->flash('status', 'API token created. Copy it now; it will not be shown again.'); + } + + public function revokeToken(int $tokenId): void + { + /** @var User $user */ + $user = Auth::user(); + + $user->tokens()->where('id', $tokenId)->delete(); + + session()->flash('status', 'Token revoked.'); + } + + public function createWebhook(): void + { + $this->validate([ + 'webhookEventType' => 'required|string|max:255', + 'webhookUrl' => 'required|string|max:2048', + ]); + + /** @var Store $store */ + $store = app('current_store'); + + WebhookSubscription::create([ + 'store_id' => $store->id, + 'event_type' => $this->webhookEventType, + 'url' => $this->webhookUrl, + 'secret' => Str::random(40), + 'status' => 'active', + 'failed_count' => 0, + ]); + + $this->reset('webhookEventType', 'webhookUrl'); + session()->flash('status', 'Webhook subscription created.'); + } + + public function deleteWebhook(int $webhookId): void + { + $webhook = WebhookSubscription::query()->findOrFail($webhookId); + $webhook->delete(); + + session()->flash('status', 'Webhook deleted.'); + } + + public function render(): View + { + /** @var User $user */ + $user = Auth::user(); + + $tokens = $user->tokens()->orderByDesc('created_at')->get(); + + $webhooks = WebhookSubscription::query() + ->orderByDesc('created_at') + ->get(); + + return view('livewire.admin.developers.index', [ + 'tokens' => $tokens, + 'webhooks' => $webhooks, + ]); + } +} diff --git a/app/Livewire/Admin/Discounts/Form.php b/app/Livewire/Admin/Discounts/Form.php new file mode 100644 index 00000000..cc7bfa1e --- /dev/null +++ b/app/Livewire/Admin/Discounts/Form.php @@ -0,0 +1,109 @@ +exists) { + $this->discount = $discount; + $this->mode = 'edit'; + $this->type = $discount->type->value; + $this->code = (string) ($discount->code ?? ''); + $this->valueType = $discount->value_type->value; + $this->valueAmount = (int) $discount->value_amount; + $this->startsAt = $discount->starts_at?->format('Y-m-d\TH:i'); + $this->endsAt = $discount->ends_at?->format('Y-m-d\TH:i'); + $this->usageLimit = $discount->usage_limit; + $this->status = $discount->status->value; + $this->minimumPurchase = $discount->rules_json['minimum_purchase'] ?? null; + } + } + + public function save(): mixed + { + $this->validate(); + + if ($this->type === 'code' && $this->code === '') { + $this->addError('code', 'Code is required for code-based discounts.'); + + return null; + } + + /** @var Store $store */ + $store = app('current_store'); + + $rules = []; + if ($this->minimumPurchase !== null && $this->minimumPurchase > 0) { + $rules['minimum_purchase'] = $this->minimumPurchase; + } + + $data = [ + 'store_id' => $store->id, + 'type' => $this->type, + 'code' => $this->type === 'code' ? $this->code : null, + 'value_type' => $this->valueType, + 'value_amount' => $this->valueAmount, + 'starts_at' => $this->startsAt, + 'ends_at' => $this->endsAt, + 'usage_limit' => $this->usageLimit, + 'rules_json' => $rules !== [] ? $rules : null, + 'status' => $this->status, + ]; + + if ($this->mode === 'create') { + Discount::create($data); + } else { + $this->discount->update($data); + } + + session()->flash('status', 'Discount saved.'); + + return redirect()->route('admin.discounts.index'); + } + + public function render(): View + { + return view('livewire.admin.discounts.form'); + } +} diff --git a/app/Livewire/Admin/Discounts/Index.php b/app/Livewire/Admin/Discounts/Index.php new file mode 100644 index 00000000..e3b22c20 --- /dev/null +++ b/app/Livewire/Admin/Discounts/Index.php @@ -0,0 +1,47 @@ +resetPage(); + } + + public function updatingTypeFilter(): void + { + $this->resetPage(); + } + + public function render(): View + { + $discounts = Discount::query() + ->when($this->statusFilter !== '', fn ($q) => $q->where('status', $this->statusFilter)) + ->when($this->typeFilter !== '', fn ($q) => $q->where('type', $this->typeFilter)) + ->latest() + ->paginate($this->perPage); + + return view('livewire.admin.discounts.index', [ + 'discounts' => $discounts, + ]); + } +} diff --git a/app/Livewire/Admin/Navigation/Index.php b/app/Livewire/Admin/Navigation/Index.php new file mode 100644 index 00000000..b4215967 --- /dev/null +++ b/app/Livewire/Admin/Navigation/Index.php @@ -0,0 +1,144 @@ +validateOnly('newMenuTitle'); + + /** @var Store $store */ + $store = app('current_store'); + + NavigationMenu::create([ + 'store_id' => $store->id, + 'title' => $this->newMenuTitle, + 'handle' => Str::slug($this->newMenuTitle), + ]); + + $this->reset('newMenuTitle'); + session()->flash('status', 'Menu created.'); + } + + public function deleteMenu(int $menuId): void + { + $menu = NavigationMenu::query()->findOrFail($menuId); + $menu->delete(); + session()->flash('status', 'Menu deleted.'); + } + + public function openItemModal(int $menuId): void + { + $this->activeMenuId = $menuId; + $this->reset('newItemType', 'newItemLabel', 'newItemUrl', 'newItemResourceId'); + $this->newItemType = 'link'; + $this->showItemModal = true; + } + + public function closeItemModal(): void + { + $this->showItemModal = false; + $this->activeMenuId = null; + } + + public function addItem(): void + { + $this->validate([ + 'newItemType' => 'required|string|in:link,page,collection,product', + 'newItemLabel' => 'required|string|max:255', + 'newItemUrl' => 'nullable|string|max:2048', + 'newItemResourceId' => 'nullable|integer', + ]); + + if ($this->activeMenuId === null) { + return; + } + + $menu = NavigationMenu::query()->findOrFail($this->activeMenuId); + + $position = (int) ($menu->items()->max('position') ?? -1) + 1; + + NavigationItem::create([ + 'menu_id' => $menu->id, + 'type' => $this->newItemType, + 'label' => $this->newItemLabel, + 'url' => $this->newItemType === 'link' ? ($this->newItemUrl !== '' ? $this->newItemUrl : null) : null, + 'resource_id' => $this->newItemType !== 'link' ? $this->newItemResourceId : null, + 'position' => $position, + ]); + + $this->closeItemModal(); + session()->flash('status', 'Item added.'); + } + + public function deleteItem(int $itemId): void + { + $item = NavigationItem::query()->findOrFail($itemId); + $item->delete(); + session()->flash('status', 'Item deleted.'); + } + + public function moveItem(int $itemId, string $direction): void + { + $item = NavigationItem::query()->findOrFail($itemId); + + $sibling = NavigationItem::query() + ->where('menu_id', $item->menu_id) + ->when( + $direction === 'up', + fn ($q) => $q->where('position', '<', $item->position)->orderByDesc('position'), + fn ($q) => $q->where('position', '>', $item->position)->orderBy('position'), + ) + ->first(); + + if ($sibling === null) { + return; + } + + $itemPosition = $item->position; + $item->update(['position' => $sibling->position]); + $sibling->update(['position' => $itemPosition]); + } + + public function render(): View + { + $menus = NavigationMenu::query() + ->with('items') + ->orderBy('title') + ->get(); + + return view('livewire.admin.navigation.index', [ + 'menus' => $menus, + ]); + } +} diff --git a/app/Livewire/Admin/Orders/Index.php b/app/Livewire/Admin/Orders/Index.php new file mode 100644 index 00000000..6b04b280 --- /dev/null +++ b/app/Livewire/Admin/Orders/Index.php @@ -0,0 +1,71 @@ +resetPage(); + } + + public function updatingStatusFilter(): void + { + $this->resetPage(); + } + + public function updatingFinancialFilter(): void + { + $this->resetPage(); + } + + public function updatingFulfillmentFilter(): void + { + $this->resetPage(); + } + + public function render(): View + { + $orders = Order::query() + ->with('customer') + ->when($this->search !== '', function ($q): void { + $q->where(function ($query): void { + $query->where('order_number', 'like', '%'.$this->search.'%') + ->orWhere('email', 'like', '%'.$this->search.'%'); + }); + }) + ->when($this->statusFilter !== '', fn ($q) => $q->where('status', $this->statusFilter)) + ->when($this->financialFilter !== '', fn ($q) => $q->where('financial_status', $this->financialFilter)) + ->when($this->fulfillmentFilter !== '', fn ($q) => $q->where('fulfillment_status', $this->fulfillmentFilter)) + ->latest('placed_at') + ->paginate($this->perPage); + + return view('livewire.admin.orders.index', [ + 'orders' => $orders, + ]); + } +} diff --git a/app/Livewire/Admin/Orders/Show.php b/app/Livewire/Admin/Orders/Show.php new file mode 100644 index 00000000..e51d55f0 --- /dev/null +++ b/app/Livewire/Admin/Orders/Show.php @@ -0,0 +1,153 @@ + */ + public array $fulfillLines = []; + + public string $trackingCompany = ''; + + public string $trackingNumber = ''; + + public string $trackingUrl = ''; + + public int $refundAmount = 0; + + public string $refundReason = ''; + + public bool $refundRestock = false; + + public string $cancelReason = ''; + + public function mount(Order $order): void + { + $this->order = $order->load(['lines', 'customer', 'payments', 'refunds', 'fulfillments.lines']); + $this->refundAmount = (int) $order->refundableAmount(); + + foreach ($this->order->lines as $line) { + $this->fulfillLines[$line->id] = (int) $line->quantity; + } + } + + public function openFulfillModal(): void + { + $this->showFulfillModal = true; + } + + public function openRefundModal(): void + { + $this->showRefundModal = true; + } + + public function createFulfillment(FulfillmentService $service): void + { + try { + $lines = array_filter(array_map('intval', $this->fulfillLines), fn (int $qty): bool => $qty > 0); + + if ($lines === []) { + $this->addError('fulfill', 'Select at least one line.'); + + return; + } + + $service->create($this->order, $lines, [ + 'company' => $this->trackingCompany !== '' ? $this->trackingCompany : null, + 'number' => $this->trackingNumber !== '' ? $this->trackingNumber : null, + 'url' => $this->trackingUrl !== '' ? $this->trackingUrl : null, + ]); + + $this->showFulfillModal = false; + $this->order->refresh()->load(['lines', 'fulfillments.lines']); + session()->flash('status', 'Fulfillment created.'); + } catch (\Throwable $exception) { + $this->addError('fulfill', $exception->getMessage()); + } + } + + public function markShipped(int $fulfillmentId, FulfillmentService $service): void + { + $fulfillment = $this->order->fulfillments()->findOrFail($fulfillmentId); + $service->markAsShipped($fulfillment); + $this->order->refresh()->load('fulfillments.lines'); + } + + public function markDelivered(int $fulfillmentId, FulfillmentService $service): void + { + $fulfillment = $this->order->fulfillments()->findOrFail($fulfillmentId); + $service->markAsDelivered($fulfillment); + $this->order->refresh()->load('fulfillments.lines'); + } + + public function createRefund(RefundService $service): void + { + $payment = $this->order->payments()->latest('id')->first(); + + if ($payment === null) { + $this->addError('refund', 'No payment to refund.'); + + return; + } + + try { + $service->create( + $this->order, + $payment, + $this->refundAmount, + $this->refundReason !== '' ? $this->refundReason : null, + $this->refundRestock, + ); + + $this->showRefundModal = false; + $this->order->refresh()->load(['lines', 'payments', 'refunds']); + session()->flash('status', 'Refund processed.'); + } catch (InvalidArgumentException $exception) { + $this->addError('refund', $exception->getMessage()); + } + } + + public function confirmBankTransfer(OrderService $service): void + { + try { + $service->confirmBankTransferPayment($this->order); + $this->order->refresh(); + session()->flash('status', 'Payment confirmed.'); + } catch (DomainException $exception) { + $this->addError('order', $exception->getMessage()); + } + } + + public function cancelOrder(OrderService $service): void + { + try { + $service->cancel($this->order, $this->cancelReason !== '' ? $this->cancelReason : null); + $this->order->refresh(); + session()->flash('status', 'Order cancelled.'); + } catch (DomainException $exception) { + $this->addError('order', $exception->getMessage()); + } + } + + public function render(): View + { + return view('livewire.admin.orders.show'); + } +} diff --git a/app/Livewire/Admin/Pages/Form.php b/app/Livewire/Admin/Pages/Form.php new file mode 100644 index 00000000..54e46731 --- /dev/null +++ b/app/Livewire/Admin/Pages/Form.php @@ -0,0 +1,99 @@ +exists) { + $this->page = $page; + $this->mode = 'edit'; + $this->title = (string) $page->title; + $this->handle = (string) $page->handle; + $this->bodyHtml = (string) ($page->body_html ?? ''); + $this->status = $page->status->value; + $this->publishedAt = $page->published_at?->format('Y-m-d'); + } + } + + public function save(): mixed + { + $this->validate(); + + /** @var Store $store */ + $store = app('current_store'); + + $publishedAt = null; + if ($this->status === PageStatus::Published->value) { + $publishedAt = $this->publishedAt !== null && $this->publishedAt !== '' + ? $this->publishedAt + : now(); + } + + if ($this->mode === 'create') { + $handle = $this->handle !== '' + ? HandleGenerator::generate($this->handle, 'pages', $store->id) + : HandleGenerator::generate($this->title, 'pages', $store->id); + + Page::create([ + 'store_id' => $store->id, + 'title' => $this->title, + 'handle' => $handle, + 'body_html' => $this->bodyHtml !== '' ? $this->bodyHtml : null, + 'status' => $this->status, + 'published_at' => $publishedAt, + ]); + } else { + $handle = $this->handle !== '' && $this->handle !== $this->page->handle + ? HandleGenerator::generate($this->handle, 'pages', $store->id, $this->page->id) + : $this->page->handle; + + $this->page->update([ + 'title' => $this->title, + 'handle' => $handle, + 'body_html' => $this->bodyHtml !== '' ? $this->bodyHtml : null, + 'status' => $this->status, + 'published_at' => $publishedAt, + ]); + } + + session()->flash('status', 'Page saved.'); + + return redirect()->route('admin.pages.index'); + } + + public function render(): View + { + return view('livewire.admin.pages.form'); + } +} diff --git a/app/Livewire/Admin/Pages/Index.php b/app/Livewire/Admin/Pages/Index.php new file mode 100644 index 00000000..ab265e25 --- /dev/null +++ b/app/Livewire/Admin/Pages/Index.php @@ -0,0 +1,46 @@ +resetPage(); + } + + public function delete(int $pageId): void + { + $page = Page::query()->findOrFail($pageId); + $page->delete(); + + session()->flash('status', 'Page deleted.'); + } + + public function render(): View + { + $pages = Page::query() + ->when($this->search !== '', fn ($q) => $q->where('title', 'like', '%'.$this->search.'%')) + ->latest('updated_at') + ->paginate($this->perPage); + + return view('livewire.admin.pages.index', [ + 'pages' => $pages, + ]); + } +} diff --git a/app/Livewire/Admin/Products/Form.php b/app/Livewire/Admin/Products/Form.php new file mode 100644 index 00000000..7a634e70 --- /dev/null +++ b/app/Livewire/Admin/Products/Form.php @@ -0,0 +1,129 @@ +exists) { + $this->product = $product; + $this->mode = 'edit'; + $this->title = (string) $product->title; + $this->handle = (string) $product->handle; + $this->description = (string) ($product->description_html ?? ''); + $this->vendor = (string) ($product->vendor ?? ''); + $this->productType = (string) ($product->product_type ?? ''); + $this->status = $product->status->value; + $this->tagsInput = implode(', ', $product->tags ?? []); + + $defaultVariant = $product->variants()->where('is_default', true)->first() + ?? $product->variants()->first(); + + if ($defaultVariant !== null) { + $this->priceAmount = (int) $defaultVariant->price_amount; + $this->sku = (string) ($defaultVariant->sku ?? ''); + } + } + } + + public function save(ProductService $service): mixed + { + $this->validate(); + + /** @var Store $store */ + $store = app('current_store'); + + $tags = array_values(array_filter(array_map('trim', explode(',', $this->tagsInput)))); + + $data = [ + 'title' => $this->title, + 'handle' => $this->handle !== '' ? $this->handle : null, + 'description_html' => $this->description !== '' ? $this->description : null, + 'vendor' => $this->vendor !== '' ? $this->vendor : null, + 'product_type' => $this->productType !== '' ? $this->productType : null, + 'status' => $this->status, + 'tags' => $tags, + ]; + + if ($this->mode === 'create') { + $product = $service->create($store, $data); + + $variant = $product->variants()->where('is_default', true)->first(); + if ($variant !== null) { + $variant->update([ + 'price_amount' => $this->priceAmount, + 'sku' => $this->sku !== '' ? $this->sku : null, + ]); + } + } else { + $product = $service->update($this->product, $data); + $product->update(['status' => $this->status]); + + $variant = $product->variants()->where('is_default', true)->first() + ?? $product->variants()->first(); + + if ($variant !== null) { + $variant->update([ + 'price_amount' => $this->priceAmount, + 'sku' => $this->sku !== '' ? $this->sku : null, + ]); + } + } + + session()->flash('status', 'Product saved.'); + + return redirect()->route('admin.products.index'); + } + + public function render(): View + { + return view('livewire.admin.products.form', [ + 'statuses' => ProductStatus::cases(), + ]); + } +} diff --git a/app/Livewire/Admin/Products/Index.php b/app/Livewire/Admin/Products/Index.php new file mode 100644 index 00000000..bdf680c9 --- /dev/null +++ b/app/Livewire/Admin/Products/Index.php @@ -0,0 +1,80 @@ + */ + public array $selectedIds = []; + + public function updatingSearch(): void + { + $this->resetPage(); + } + + public function updatingStatusFilter(): void + { + $this->resetPage(); + } + + public function bulkArchive(): void + { + if ($this->selectedIds === []) { + return; + } + + Product::query() + ->whereIn('id', $this->selectedIds) + ->update(['status' => ProductStatus::Archived->value]); + + $this->selectedIds = []; + } + + public function bulkDelete(): void + { + if ($this->selectedIds === []) { + return; + } + + Product::query() + ->whereIn('id', $this->selectedIds) + ->where('status', ProductStatus::Draft->value) + ->delete(); + + $this->selectedIds = []; + } + + public function render(): View + { + $products = Product::query() + ->with(['variants' => fn ($q) => $q->where('is_default', true)]) + ->withCount('variants') + ->when($this->search !== '', fn ($q) => $q->where('title', 'like', '%'.$this->search.'%')) + ->when($this->statusFilter !== '', fn ($q) => $q->where('status', $this->statusFilter)) + ->latest() + ->paginate($this->perPage); + + return view('livewire.admin.products.index', [ + 'products' => $products, + ]); + } +} diff --git a/app/Livewire/Admin/Settings/Index.php b/app/Livewire/Admin/Settings/Index.php new file mode 100644 index 00000000..4c3887a7 --- /dev/null +++ b/app/Livewire/Admin/Settings/Index.php @@ -0,0 +1,58 @@ +name = (string) $store->name; + $this->defaultCurrency = (string) $store->default_currency; + $this->defaultLocale = (string) $store->default_locale; + $this->timezone = (string) $store->timezone; + } + + public function save(): void + { + $this->validate(); + + /** @var Store $store */ + $store = app('current_store'); + + $store->update([ + 'name' => $this->name, + 'default_currency' => strtoupper($this->defaultCurrency), + 'default_locale' => $this->defaultLocale, + 'timezone' => $this->timezone, + ]); + + session()->flash('status', 'Settings saved.'); + } + + public function render(): View + { + return view('livewire.admin.settings.index'); + } +} diff --git a/app/Livewire/Admin/Settings/Shipping.php b/app/Livewire/Admin/Settings/Shipping.php new file mode 100644 index 00000000..469f80b8 --- /dev/null +++ b/app/Livewire/Admin/Settings/Shipping.php @@ -0,0 +1,127 @@ +reset('zoneName', 'zoneCountries'); + $this->showZoneModal = true; + } + + public function createZone(): void + { + $this->validate([ + 'zoneName' => 'required|string|max:255', + 'zoneCountries' => 'nullable|string', + ]); + + /** @var Store $store */ + $store = app('current_store'); + + $countries = array_values(array_filter(array_map( + fn (string $code): string => strtoupper(trim($code)), + explode(',', $this->zoneCountries) + ))); + + ShippingZone::create([ + 'store_id' => $store->id, + 'name' => $this->zoneName, + 'countries_json' => $countries, + 'regions_json' => [], + ]); + + $this->showZoneModal = false; + session()->flash('status', 'Zone created.'); + } + + public function deleteZone(int $zoneId): void + { + $zone = ShippingZone::query()->findOrFail($zoneId); + $zone->delete(); + session()->flash('status', 'Zone deleted.'); + } + + public function openRateModal(int $zoneId): void + { + $this->activeZoneId = $zoneId; + $this->reset('rateName', 'rateType', 'rateAmount'); + $this->rateType = 'flat'; + $this->showRateModal = true; + } + + public function createRate(): void + { + $this->validate([ + 'rateName' => 'required|string|max:255', + 'rateType' => 'required|string|in:flat,weight,price,carrier', + 'rateAmount' => 'nullable|integer|min:0', + ]); + + if ($this->activeZoneId === null) { + return; + } + + ShippingRate::create([ + 'zone_id' => $this->activeZoneId, + 'name' => $this->rateName, + 'type' => $this->rateType, + 'config_json' => ['amount' => $this->rateAmount], + 'is_active' => true, + ]); + + $this->showRateModal = false; + $this->activeZoneId = null; + session()->flash('status', 'Rate added.'); + } + + public function deleteRate(int $rateId): void + { + $rate = ShippingRate::query()->findOrFail($rateId); + $rate->delete(); + session()->flash('status', 'Rate deleted.'); + } + + public function render(): View + { + $zones = ShippingZone::query() + ->with('rates') + ->orderBy('name') + ->get(); + + return view('livewire.admin.settings.shipping', [ + 'zones' => $zones, + ]); + } +} diff --git a/app/Livewire/Admin/Settings/Taxes.php b/app/Livewire/Admin/Settings/Taxes.php new file mode 100644 index 00000000..9c871ead --- /dev/null +++ b/app/Livewire/Admin/Settings/Taxes.php @@ -0,0 +1,66 @@ +firstOrNew(['store_id' => $store->id]); + + $this->mode = $settings->mode?->value ?? 'manual'; + $this->pricesIncludeTax = (bool) $settings->prices_include_tax; + $config = (array) ($settings->config_json ?? []); + $this->taxName = (string) ($config['name'] ?? ''); + $this->rateBasisPoints = (int) ($config['rate_basis_points'] ?? 0); + } + + public function save(): void + { + $this->validate(); + + /** @var Store $store */ + $store = app('current_store'); + + TaxSettings::query()->updateOrCreate( + ['store_id' => $store->id], + [ + 'mode' => $this->mode, + 'prices_include_tax' => $this->pricesIncludeTax, + 'config_json' => [ + 'name' => $this->taxName, + 'rate_basis_points' => $this->rateBasisPoints, + ], + ] + ); + + session()->flash('status', 'Tax settings saved.'); + } + + public function render(): View + { + return view('livewire.admin.settings.taxes'); + } +} diff --git a/app/Livewire/Admin/Themes/Index.php b/app/Livewire/Admin/Themes/Index.php new file mode 100644 index 00000000..51c2b17e --- /dev/null +++ b/app/Livewire/Admin/Themes/Index.php @@ -0,0 +1,78 @@ +where('store_id', $store->id) + ->where('status', ThemeStatus::Published->value) + ->update(['status' => ThemeStatus::Draft->value, 'published_at' => null]); + + Theme::query() + ->where('store_id', $store->id) + ->where('id', $themeId) + ->update(['status' => ThemeStatus::Published->value, 'published_at' => now()]); + }); + + session()->flash('status', 'Theme published.'); + } + + public function duplicate(int $themeId): void + { + /** @var Store $store */ + $store = app('current_store'); + + $source = Theme::query()->where('store_id', $store->id)->findOrFail($themeId); + + Theme::create([ + 'store_id' => $store->id, + 'name' => $source->name.' Copy', + 'version' => $source->version, + 'status' => ThemeStatus::Draft->value, + ]); + + session()->flash('status', 'Theme duplicated.'); + } + + public function delete(int $themeId): void + { + $theme = Theme::query()->findOrFail($themeId); + + if ($theme->status === ThemeStatus::Published) { + session()->flash('status', 'Cannot delete the published theme.'); + + return; + } + + $theme->delete(); + session()->flash('status', 'Theme deleted.'); + } + + public function render(): View + { + $themes = Theme::query() + ->orderByDesc('status') + ->orderBy('name') + ->get(); + + return view('livewire.admin.themes.index', [ + 'themes' => $themes, + ]); + } +} diff --git a/app/Livewire/Storefront/Account/Addresses/Index.php b/app/Livewire/Storefront/Account/Addresses/Index.php new file mode 100644 index 00000000..5f46cdd7 --- /dev/null +++ b/app/Livewire/Storefront/Account/Addresses/Index.php @@ -0,0 +1,122 @@ +ensureCurrentStore(); + } + + public function addAddress(): void + { + $this->validate([ + 'label' => 'required|string|max:60', + 'firstName' => 'required|string|max:120', + 'lastName' => 'required|string|max:120', + 'line1' => 'required|string|max:255', + 'city' => 'required|string|max:120', + 'postalCode' => 'required|string|max:30', + 'country' => 'required|string|size:2', + ]); + + /** @var Customer $customer */ + $customer = Auth::guard('customer')->user(); + + if ($this->isDefault) { + CustomerAddress::query() + ->where('customer_id', $customer->id) + ->update(['is_default' => false]); + } + + CustomerAddress::create([ + 'customer_id' => $customer->id, + 'label' => $this->label, + 'is_default' => $this->isDefault, + 'address_json' => [ + 'first_name' => $this->firstName, + 'last_name' => $this->lastName, + 'address1' => $this->line1, + 'address2' => $this->line2, + 'city' => $this->city, + 'postal_code' => $this->postalCode, + 'country' => $this->country, + ], + ]); + + $this->reset(['label', 'firstName', 'lastName', 'line1', 'line2', 'city', 'postalCode', 'isDefault']); + $this->label = 'Home'; + $this->country = 'DE'; + } + + public function makeDefault(int $addressId): void + { + /** @var Customer $customer */ + $customer = Auth::guard('customer')->user(); + + CustomerAddress::query() + ->where('customer_id', $customer->id) + ->update(['is_default' => false]); + + CustomerAddress::query() + ->where('customer_id', $customer->id) + ->where('id', $addressId) + ->update(['is_default' => true]); + } + + public function deleteAddress(int $addressId): void + { + /** @var Customer $customer */ + $customer = Auth::guard('customer')->user(); + + CustomerAddress::query() + ->where('customer_id', $customer->id) + ->where('id', $addressId) + ->delete(); + } + + public function render(): View + { + /** @var Customer $customer */ + $customer = Auth::guard('customer')->user(); + + $addresses = CustomerAddress::query() + ->where('customer_id', $customer->id) + ->orderByDesc('is_default') + ->get(); + + return view('livewire.storefront.account.addresses.index', [ + 'addresses' => $addresses, + ]); + } +} diff --git a/app/Livewire/Storefront/Account/Auth/Login.php b/app/Livewire/Storefront/Account/Auth/Login.php new file mode 100644 index 00000000..9d96a26c --- /dev/null +++ b/app/Livewire/Storefront/Account/Auth/Login.php @@ -0,0 +1,63 @@ +ensureCurrentStore(); + + if (Auth::guard('customer')->check()) { + $this->redirect(route('storefront.account.dashboard'), navigate: false); + } + } + + public function login(): void + { + $this->validate([ + 'email' => 'required|email', + 'password' => 'required|string', + ]); + + $store = $this->ensureCurrentStore(); + + $customer = Customer::query() + ->where('store_id', $store->id) + ->where('email', $this->email) + ->first(); + + if ($customer === null || ! Hash::check($this->password, (string) $customer->password_hash)) { + $this->addError('email', 'These credentials do not match our records.'); + + return; + } + + Auth::guard('customer')->login($customer, $this->remember); + session()->regenerate(); + + $this->redirect(route('storefront.account.dashboard'), navigate: false); + } + + public function render(): View + { + return view('livewire.storefront.account.auth.login'); + } +} diff --git a/app/Livewire/Storefront/Account/Auth/Register.php b/app/Livewire/Storefront/Account/Auth/Register.php new file mode 100644 index 00000000..b1604393 --- /dev/null +++ b/app/Livewire/Storefront/Account/Auth/Register.php @@ -0,0 +1,70 @@ +ensureCurrentStore(); + + if (Auth::guard('customer')->check()) { + $this->redirect(route('storefront.account.dashboard'), navigate: false); + } + } + + public function register(): void + { + $store = $this->ensureCurrentStore(); + + $this->validate([ + 'name' => 'required|string|max:120', + 'email' => [ + 'required', + 'email', + Rule::unique('customers', 'email')->where(fn ($query) => $query->where('store_id', $store->id)), + ], + 'password' => 'required|string|min:8|confirmed', + ]); + + /** @var Customer $customer */ + $customer = Customer::create([ + 'store_id' => $store->id, + 'name' => $this->name, + 'email' => $this->email, + 'password_hash' => $this->password, + 'marketing_opt_in' => $this->marketing_opt_in, + ]); + + Auth::guard('customer')->login($customer); + session()->regenerate(); + + $this->redirect(route('storefront.account.dashboard'), navigate: false); + } + + public function render(): View + { + return view('livewire.storefront.account.auth.register'); + } +} diff --git a/app/Livewire/Storefront/Account/Dashboard.php b/app/Livewire/Storefront/Account/Dashboard.php new file mode 100644 index 00000000..e14a43f0 --- /dev/null +++ b/app/Livewire/Storefront/Account/Dashboard.php @@ -0,0 +1,39 @@ +ensureCurrentStore(); + } + + public function render(): View + { + /** @var Customer $customer */ + $customer = Auth::guard('customer')->user(); + + $recentOrders = Order::query() + ->where('customer_id', $customer->id) + ->latest('id') + ->limit(5) + ->get(); + + return view('livewire.storefront.account.dashboard', [ + 'customer' => $customer, + 'recentOrders' => $recentOrders, + ]); + } +} diff --git a/app/Livewire/Storefront/Account/Orders/Index.php b/app/Livewire/Storefront/Account/Orders/Index.php new file mode 100644 index 00000000..848647cd --- /dev/null +++ b/app/Livewire/Storefront/Account/Orders/Index.php @@ -0,0 +1,38 @@ +ensureCurrentStore(); + } + + public function render(): View + { + /** @var Customer $customer */ + $customer = Auth::guard('customer')->user(); + + $orders = Order::query() + ->where('customer_id', $customer->id) + ->latest('id') + ->paginate(15); + + return view('livewire.storefront.account.orders.index', [ + 'orders' => $orders, + ]); + } +} diff --git a/app/Livewire/Storefront/Account/Orders/Show.php b/app/Livewire/Storefront/Account/Orders/Show.php new file mode 100644 index 00000000..3cd42442 --- /dev/null +++ b/app/Livewire/Storefront/Account/Orders/Show.php @@ -0,0 +1,40 @@ +ensureCurrentStore(); + + /** @var Customer $customer */ + $customer = Auth::guard('customer')->user(); + + $this->order = Order::query() + ->where('customer_id', $customer->id) + ->where('order_number', '#'.$orderNumber) + ->with('lines') + ->firstOrFail(); + } + + public function render(): View + { + return view('livewire.storefront.account.orders.show', [ + 'order' => $this->order, + ]); + } +} diff --git a/app/Livewire/Storefront/Cart/Show.php b/app/Livewire/Storefront/Cart/Show.php new file mode 100644 index 00000000..90c09bfa --- /dev/null +++ b/app/Livewire/Storefront/Cart/Show.php @@ -0,0 +1,79 @@ +ensureCurrentStore(); + } + + public function updateQty(int $lineId, int $qty): void + { + $cart = CartSession::current(); + if ($cart === null) { + return; + } + + $qty = max(1, $qty); + + try { + app(CartService::class)->updateLineQuantity($cart, $lineId, $qty); + } catch (RuntimeException $exception) { + $this->addError('cart', $exception->getMessage()); + } + + $this->dispatch('cart-updated'); + } + + public function removeLine(int $lineId): void + { + $cart = CartSession::current(); + if ($cart === null) { + return; + } + + app(CartService::class)->removeLine($cart, $lineId); + $this->dispatch('cart-updated'); + } + + public function applyDiscount(): void + { + session(['discount_code' => $this->discountCode]); + $this->dispatch('cart-updated'); + } + + public function render(): View + { + $this->ensureCurrentStore(); + + $cart = CartSession::current(); + $cart?->load('lines.variant.product'); + + $subtotal = 0; + if ($cart !== null) { + foreach ($cart->lines as $line) { + $subtotal += (int) $line->line_subtotal_amount; + } + } + + return view('livewire.storefront.cart.show', [ + 'cart' => $cart, + 'subtotal' => $subtotal, + ]); + } +} diff --git a/app/Livewire/Storefront/CartDrawer.php b/app/Livewire/Storefront/CartDrawer.php new file mode 100644 index 00000000..a11445fd --- /dev/null +++ b/app/Livewire/Storefront/CartDrawer.php @@ -0,0 +1,48 @@ +ensureCurrentStore(); + } + + #[On('cart-updated')] + public function refreshCart(): void + { + // Triggers re-render when cart changes elsewhere. + } + + public function render(): View + { + $this->ensureCurrentStore(); + + $cart = CartSession::current(); + $cart?->load('lines.variant.product'); + + $count = 0; + $subtotal = 0; + if ($cart !== null) { + foreach ($cart->lines as $line) { + $count += (int) $line->quantity; + $subtotal += (int) $line->line_subtotal_amount; + } + } + + return view('livewire.storefront.cart-drawer', [ + 'cart' => $cart, + 'count' => $count, + 'subtotal' => $subtotal, + ]); + } +} diff --git a/app/Livewire/Storefront/Checkout/Confirmation.php b/app/Livewire/Storefront/Checkout/Confirmation.php new file mode 100644 index 00000000..030b97d7 --- /dev/null +++ b/app/Livewire/Storefront/Checkout/Confirmation.php @@ -0,0 +1,34 @@ +ensureCurrentStore(); + + $this->order = Order::query() + ->where('order_number', '#'.$order_number) + ->with('lines') + ->firstOrFail(); + } + + public function render(): View + { + return view('livewire.storefront.checkout.confirmation', [ + 'order' => $this->order, + ]); + } +} diff --git a/app/Livewire/Storefront/Checkout/Show.php b/app/Livewire/Storefront/Checkout/Show.php new file mode 100644 index 00000000..8d03d62a --- /dev/null +++ b/app/Livewire/Storefront/Checkout/Show.php @@ -0,0 +1,259 @@ + */ + public array $shippingAddress = [ + 'first_name' => '', + 'last_name' => '', + 'line1' => '', + 'line2' => '', + 'city' => '', + 'postal_code' => '', + 'country' => 'DE', + 'province_code' => null, + ]; + + /** @var array */ + public array $billingAddress = [ + 'first_name' => '', + 'last_name' => '', + 'line1' => '', + 'line2' => '', + 'city' => '', + 'postal_code' => '', + 'country' => 'DE', + 'province_code' => null, + ]; + + public bool $billingSameAsShipping = true; + + public ?int $shippingMethodId = null; + + public string $paymentMethod = 'credit_card'; + + public string $cardNumber = '4242424242424242'; + + public string $cardExpiry = '12/30'; + + public string $cardCvc = '123'; + + public int $step = 1; + + public function mount(): void + { + $this->ensureCurrentStore(); + + $cart = CartSession::current(); + if ($cart === null || $cart->lines()->count() === 0) { + $this->redirect(route('storefront.cart.show'), navigate: false); + + return; + } + + $customer = Auth::guard('customer')->user(); + if ($customer !== null && $this->email === '') { + $this->email = (string) $customer->email; + } + } + + public function continueToShipping(): void + { + $this->validate([ + 'email' => 'required|email', + 'shippingAddress.first_name' => 'required|string|max:100', + 'shippingAddress.last_name' => 'required|string|max:100', + 'shippingAddress.line1' => 'required|string|max:255', + 'shippingAddress.city' => 'required|string|max:120', + 'shippingAddress.postal_code' => 'required|string|max:30', + 'shippingAddress.country' => 'required|string|size:2', + ]); + + $billing = $this->billingSameAsShipping ? $this->shippingAddress : $this->billingAddress; + + $checkout = $this->getOrCreateCheckout(); + app(CheckoutService::class)->setAddress($checkout, [ + 'email' => $this->email, + 'shipping_address' => $this->shippingAddressPayload(), + 'billing_address' => $this->addressToPayload($billing), + ]); + + $this->step = 2; + } + + public function continueToPayment(): void + { + $this->validate([ + 'shippingMethodId' => 'required|integer', + ]); + + $checkout = $this->getOrCreateCheckout(); + app(CheckoutService::class)->setShippingMethod($checkout, (int) $this->shippingMethodId); + + $this->step = 3; + } + + public function backToAddress(): void + { + $this->step = 1; + } + + public function backToShipping(): void + { + $this->step = 2; + } + + public function placeOrder(): void + { + $this->validate([ + 'paymentMethod' => 'required|in:credit_card,paypal,bank_transfer', + ]); + + $checkout = $this->getOrCreateCheckout(); + $service = app(CheckoutService::class); + + $service->selectPaymentMethod($checkout, $this->paymentMethod); + $checkout->refresh(); + + try { + $service->complete($checkout, [ + 'card_number' => $this->cardNumber, + 'card_expiry' => $this->cardExpiry, + 'card_cvc' => $this->cardCvc, + ]); + } catch (PaymentFailedException $exception) { + $this->addError('payment', $exception->getMessage()); + + return; + } + + $storeId = (int) $checkout->store_id; + $order = Order::withoutGlobalScopes() + ->where('store_id', $storeId) + ->latest('id') + ->first(); + + CartSession::clear(); + + if ($order !== null) { + $this->redirect( + route('storefront.checkout.confirmation', ['order_number' => ltrim((string) $order->order_number, '#')]), + navigate: false + ); + } + } + + public function render(): View + { + $store = $this->ensureCurrentStore(); + + $cart = CartSession::current(); + $cart?->load('lines.variant.product'); + + $shippingRates = collect(); + if ($this->step >= 2 && $cart !== null) { + $shippingRates = app(ShippingCalculator::class)->getAvailableRates( + $store, + $this->shippingAddressPayload() + ); + } + + $totals = $this->computeTotals($cart, $shippingRates); + + return view('livewire.storefront.checkout.show', [ + 'cart' => $cart, + 'shippingRates' => $shippingRates, + 'totals' => $totals, + ]); + } + + private function getOrCreateCheckout(): Checkout + { + $cart = CartSession::current(); + + if ($cart === null) { + $this->redirect(route('storefront.cart.show'), navigate: false); + abort(404); + } + + $existing = $cart->checkouts()->latest('id')->first(); + if ($existing !== null) { + return $existing; + } + + return app(CheckoutService::class)->start($cart); + } + + /** + * @return array + */ + private function shippingAddressPayload(): array + { + return $this->addressToPayload($this->shippingAddress); + } + + /** + * @param array $address + * @return array + */ + private function addressToPayload(array $address): array + { + return [ + 'first_name' => $address['first_name'] ?? '', + 'last_name' => $address['last_name'] ?? '', + 'address1' => $address['line1'] ?? '', + 'address2' => $address['line2'] ?? '', + 'city' => $address['city'] ?? '', + 'postal_code' => $address['postal_code'] ?? '', + 'country' => $address['country'] ?? '', + 'province_code' => $address['province_code'] ?? null, + ]; + } + + /** + * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Collection $rates + * @return array + */ + private function computeTotals(?\App\Models\Cart $cart, $rates): array + { + $subtotal = 0; + if ($cart !== null) { + foreach ($cart->lines as $line) { + $subtotal += (int) $line->line_subtotal_amount; + } + } + + $shipping = 0; + if ($this->shippingMethodId !== null && $cart !== null) { + $rate = $rates->firstWhere('id', $this->shippingMethodId); + if ($rate !== null) { + $shipping = app(ShippingCalculator::class)->calculate($rate, $cart); + } + } + + return [ + 'subtotal' => $subtotal, + 'shipping' => $shipping, + 'total' => $subtotal + $shipping, + ]; + } +} diff --git a/app/Livewire/Storefront/Collections/Index.php b/app/Livewire/Storefront/Collections/Index.php new file mode 100644 index 00000000..5d8bd15c --- /dev/null +++ b/app/Livewire/Storefront/Collections/Index.php @@ -0,0 +1,34 @@ +ensureCurrentStore(); + } + + public function render(): View + { + $collections = CollectionModel::query() + ->where('status', CollectionStatus::Active->value) + ->withCount('products') + ->orderBy('title') + ->get(); + + return view('livewire.storefront.collections.index', [ + 'collections' => $collections, + ]); + } +} diff --git a/app/Livewire/Storefront/Collections/Show.php b/app/Livewire/Storefront/Collections/Show.php new file mode 100644 index 00000000..a04f4d04 --- /dev/null +++ b/app/Livewire/Storefront/Collections/Show.php @@ -0,0 +1,59 @@ +ensureCurrentStore(); + $this->handle = $handle; + } + + public function updatedSort(): void + { + $this->resetPage(); + } + + public function render(): View + { + $collection = CollectionModel::query() + ->where('handle', $this->handle) + ->firstOrFail(); + + $query = $collection->products() + ->where('products.status', ProductStatus::Active->value); + + if ($this->sort === 'title_asc') { + $query->orderBy('products.title'); + } elseif ($this->sort === 'newest') { + $query->orderByDesc('products.id'); + } else { + $query->orderBy('collection_products.position'); + } + + $products = $query->with('variants')->paginate(12); + + return view('livewire.storefront.collections.show', [ + 'collection' => $collection, + 'products' => $products, + ]); + } +} diff --git a/app/Livewire/Storefront/Concerns/EnsuresStore.php b/app/Livewire/Storefront/Concerns/EnsuresStore.php new file mode 100644 index 00000000..807c0186 --- /dev/null +++ b/app/Livewire/Storefront/Concerns/EnsuresStore.php @@ -0,0 +1,22 @@ +bound('current_store')) { + /** @var Store $store */ + $store = Store::first() ?? Store::factory()->create(); + app()->instance('current_store', $store); + } + + /** @var Store $current */ + $current = app('current_store'); + + return $current; + } +} diff --git a/app/Livewire/Storefront/Home.php b/app/Livewire/Storefront/Home.php new file mode 100644 index 00000000..4e05b625 --- /dev/null +++ b/app/Livewire/Storefront/Home.php @@ -0,0 +1,65 @@ +bound('current_store')) { + $store = Store::first(); + + if ($store !== null) { + app()->instance('current_store', $store); + } + } + } + + public function render(): View + { + return view('livewire.storefront.home', [ + 'featuredCollections' => $this->featuredCollections(), + 'recentProducts' => $this->recentProducts(), + ]); + } + + /** + * @return SupportCollection + */ + protected function featuredCollections(): SupportCollection + { + if (! class_exists(Collection::class)) { + return collect(); + } + + return Collection::query() + ->latest() + ->limit(3) + ->get(); + } + + /** + * @return SupportCollection + */ + protected function recentProducts(): SupportCollection + { + if (! class_exists(Product::class)) { + return collect(); + } + + return Product::query() + ->with('variants') + ->latest() + ->limit(8) + ->get(); + } +} diff --git a/app/Livewire/Storefront/Pages/Show.php b/app/Livewire/Storefront/Pages/Show.php new file mode 100644 index 00000000..8bcd50da --- /dev/null +++ b/app/Livewire/Storefront/Pages/Show.php @@ -0,0 +1,36 @@ +ensureCurrentStore(); + $this->handle = $handle; + } + + public function render(): View + { + $page = Page::query() + ->where('handle', $this->handle) + ->where('status', PageStatus::Published->value) + ->firstOrFail(); + + return view('livewire.storefront.pages.show', [ + 'page' => $page, + ]); + } +} diff --git a/app/Livewire/Storefront/Products/Show.php b/app/Livewire/Storefront/Products/Show.php new file mode 100644 index 00000000..10e51738 --- /dev/null +++ b/app/Livewire/Storefront/Products/Show.php @@ -0,0 +1,93 @@ +ensureCurrentStore(); + $this->handle = $handle; + } + + public function incrementQuantity(): void + { + $this->quantity = min(99, $this->quantity + 1); + } + + public function decrementQuantity(): void + { + $this->quantity = max(1, $this->quantity - 1); + } + + public function selectVariant(int $variantId): void + { + $this->selectedVariantId = $variantId; + } + + public function addToCart(): void + { + if ($this->selectedVariantId === null) { + $this->addError('cart', 'Please select a variant.'); + + return; + } + + $store = $this->ensureCurrentStore(); + $cart = CartSession::getOrCreate($store); + + try { + app(CartService::class)->addLine($cart, $this->selectedVariantId, $this->quantity); + } catch (RuntimeException $exception) { + $this->addError('cart', $exception->getMessage()); + + return; + } + + $this->dispatch('cart-updated'); + session()->flash('cart-success', 'Added to cart'); + } + + public function render(): View + { + $product = Product::query() + ->where('handle', $this->handle) + ->with(['variants', 'media']) + ->firstOrFail(); + + $activeVariants = $product->variants->filter( + fn ($variant): bool => $variant->status === VariantStatus::Active + )->values(); + + if ($this->selectedVariantId === null && $activeVariants->isNotEmpty()) { + $this->selectedVariantId = (int) $activeVariants->first()->id; + } + + $selectedVariant = $activeVariants->firstWhere('id', $this->selectedVariantId); + + return view('livewire.storefront.products.show', [ + 'product' => $product, + 'activeVariants' => $activeVariants, + 'selectedVariant' => $selectedVariant, + ]); + } +} diff --git a/app/Livewire/Storefront/Search/Index.php b/app/Livewire/Storefront/Search/Index.php new file mode 100644 index 00000000..c437bf97 --- /dev/null +++ b/app/Livewire/Storefront/Search/Index.php @@ -0,0 +1,44 @@ +ensureCurrentStore(); + } + + public function updatedQ(): void + { + $this->resetPage(); + } + + public function render(): View + { + $store = $this->ensureCurrentStore(); + + $products = trim($this->q) !== '' + ? app(SearchService::class)->search($store, $this->q, [], 12) + : Product::query()->whereRaw('1 = 0')->paginate(12); + + return view('livewire.storefront.search.index', [ + 'products' => $products, + ]); + } +} diff --git a/app/Models/AnalyticsDaily.php b/app/Models/AnalyticsDaily.php new file mode 100644 index 00000000..aac423a2 --- /dev/null +++ b/app/Models/AnalyticsDaily.php @@ -0,0 +1,68 @@ + */ + use BelongsToStore, HasFactory; + + protected $table = 'analytics_daily'; + + public $incrementing = false; + + public $timestamps = false; + + /** + * @var list + */ + protected $primaryKey = ['store_id', 'date']; + + protected $fillable = [ + 'store_id', + 'date', + 'orders_count', + 'revenue_amount', + 'aov_amount', + 'visits_count', + 'add_to_cart_count', + 'checkout_started_count', + 'checkout_completed_count', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'date' => 'date', + ]; + } + + /** + * @param Builder $query + * @return Builder + */ + protected function setKeysForSaveQuery($query) + { + $query->where('store_id', $this->getAttribute('store_id')) + ->where('date', $this->getAttribute('date')); + + return $query; + } + + /** + * @param Builder $query + * @return Builder + */ + protected function setKeysForSelectQuery($query) + { + return $this->setKeysForSaveQuery($query); + } +} diff --git a/app/Models/AnalyticsEvent.php b/app/Models/AnalyticsEvent.php new file mode 100644 index 00000000..6e3e6379 --- /dev/null +++ b/app/Models/AnalyticsEvent.php @@ -0,0 +1,38 @@ + */ + use BelongsToStore, HasFactory; + + protected $table = 'analytics_events'; + + public const UPDATED_AT = null; + + protected $fillable = [ + 'store_id', + 'type', + 'session_id', + 'customer_id', + 'properties_json', + 'client_event_id', + 'occurred_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'properties_json' => 'array', + 'occurred_at' => 'datetime', + ]; + } +} diff --git a/app/Models/App.php b/app/Models/App.php new file mode 100644 index 00000000..04f28901 --- /dev/null +++ b/app/Models/App.php @@ -0,0 +1,41 @@ + */ + use HasFactory; + + protected $table = 'apps'; + + protected $fillable = [ + 'name', + 'slug', + 'description', + 'scopes_json', + 'type', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'scopes_json' => 'array', + ]; + } + + /** + * @return HasMany + */ + public function installations(): HasMany + { + return $this->hasMany(AppInstallation::class); + } +} diff --git a/app/Models/AppInstallation.php b/app/Models/AppInstallation.php new file mode 100644 index 00000000..1dbb12e6 --- /dev/null +++ b/app/Models/AppInstallation.php @@ -0,0 +1,56 @@ + */ + use BelongsToStore, HasFactory; + + protected $table = 'app_installations'; + + public const UPDATED_AT = 'updated_at'; + + public const CREATED_AT = null; + + protected $fillable = [ + 'store_id', + 'app_id', + 'status', + 'settings_json', + 'installed_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'settings_json' => 'array', + 'installed_at' => 'datetime', + ]; + } + + /** + * @return BelongsTo + */ + public function app(): BelongsTo + { + return $this->belongsTo(App::class); + } + + /** + * @return HasMany + */ + public function webhookSubscriptions(): HasMany + { + return $this->hasMany(WebhookSubscription::class); + } +} diff --git a/app/Models/Cart.php b/app/Models/Cart.php new file mode 100644 index 00000000..7848cf29 --- /dev/null +++ b/app/Models/Cart.php @@ -0,0 +1,55 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'customer_id', + 'session_id', + 'currency', + 'cart_version', + 'status', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => CartStatus::class, + ]; + } + + /** + * @return HasMany + */ + public function lines(): HasMany + { + return $this->hasMany(CartLine::class); + } + + /** + * @return HasMany + */ + public function checkouts(): HasMany + { + return $this->hasMany(Checkout::class); + } + + public function incrementVersion(): void + { + $this->cart_version = (int) $this->cart_version + 1; + } +} diff --git a/app/Models/CartLine.php b/app/Models/CartLine.php new file mode 100644 index 00000000..67815b9b --- /dev/null +++ b/app/Models/CartLine.php @@ -0,0 +1,41 @@ + */ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'cart_id', + 'variant_id', + 'quantity', + 'unit_price_amount', + 'line_subtotal_amount', + 'line_discount_amount', + 'line_total_amount', + ]; + + /** + * @return BelongsTo + */ + public function cart(): BelongsTo + { + return $this->belongsTo(Cart::class); + } + + /** + * @return BelongsTo + */ + public function variant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class, 'variant_id'); + } +} diff --git a/app/Models/Checkout.php b/app/Models/Checkout.php new file mode 100644 index 00000000..a0e62ad0 --- /dev/null +++ b/app/Models/Checkout.php @@ -0,0 +1,54 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'cart_id', + 'customer_id', + 'status', + 'payment_method', + 'email', + 'shipping_address_json', + 'billing_address_json', + 'shipping_method_id', + 'discount_code', + 'tax_provider_snapshot_json', + 'totals_json', + 'expires_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => CheckoutStatus::class, + 'shipping_address_json' => 'array', + 'billing_address_json' => 'array', + 'tax_provider_snapshot_json' => 'array', + 'totals_json' => 'array', + 'expires_at' => 'datetime', + ]; + } + + /** + * @return BelongsTo + */ + public function cart(): BelongsTo + { + return $this->belongsTo(Cart::class); + } +} diff --git a/app/Models/Collection.php b/app/Models/Collection.php new file mode 100644 index 00000000..a35c5e08 --- /dev/null +++ b/app/Models/Collection.php @@ -0,0 +1,45 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'title', + 'handle', + 'description_html', + 'type', + 'status', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'type' => CollectionType::class, + 'status' => CollectionStatus::class, + ]; + } + + /** + * @return BelongsToMany + */ + public function products(): BelongsToMany + { + return $this->belongsToMany(Product::class, 'collection_products') + ->withPivot('position'); + } +} diff --git a/app/Models/Concerns/BelongsToStore.php b/app/Models/Concerns/BelongsToStore.php new file mode 100644 index 00000000..d5806731 --- /dev/null +++ b/app/Models/Concerns/BelongsToStore.php @@ -0,0 +1,31 @@ +store_id === null && app()->bound('current_store')) { + /** @var Store $store */ + $store = app('current_store'); + $model->store_id = $store->id; + } + }); + } + + /** + * @return BelongsTo + */ + public function store(): BelongsTo + { + return $this->belongsTo(Store::class); + } +} diff --git a/app/Models/Customer.php b/app/Models/Customer.php new file mode 100644 index 00000000..aafa051f --- /dev/null +++ b/app/Models/Customer.php @@ -0,0 +1,80 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'email', + 'password_hash', + 'name', + 'marketing_opt_in', + ]; + + /** + * @var array + */ + protected $hidden = [ + 'password_hash', + 'remember_token', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'marketing_opt_in' => 'boolean', + 'password_hash' => 'hashed', + ]; + } + + public function getAuthPassword(): string + { + return (string) $this->password_hash; + } + + public function getAuthPasswordName(): string + { + return 'password_hash'; + } + + public function getAuthIdentifierName(): string + { + return 'id'; + } + + /** + * @return HasMany + */ + public function addresses(): HasMany + { + return $this->hasMany(CustomerAddress::class); + } + + /** + * @return HasMany + */ + public function orders(): HasMany + { + return $this->hasMany(Order::class); + } + + /** + * @return HasMany + */ + public function carts(): HasMany + { + return $this->hasMany(Cart::class); + } +} diff --git a/app/Models/CustomerAddress.php b/app/Models/CustomerAddress.php new file mode 100644 index 00000000..604332aa --- /dev/null +++ b/app/Models/CustomerAddress.php @@ -0,0 +1,41 @@ + */ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'customer_id', + 'label', + 'address_json', + 'is_default', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'address_json' => 'array', + 'is_default' => 'boolean', + ]; + } + + /** + * @return BelongsTo + */ + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } +} diff --git a/app/Models/Discount.php b/app/Models/Discount.php new file mode 100644 index 00000000..7f54cc45 --- /dev/null +++ b/app/Models/Discount.php @@ -0,0 +1,68 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'type', + 'code', + 'value_type', + 'value_amount', + 'starts_at', + 'ends_at', + 'usage_limit', + 'usage_count', + 'rules_json', + 'status', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'type' => DiscountType::class, + 'value_type' => DiscountValueType::class, + 'status' => DiscountStatus::class, + 'starts_at' => 'datetime', + 'ends_at' => 'datetime', + 'rules_json' => 'array', + ]; + } + + public function isCurrentlyActive(): bool + { + if ($this->status !== DiscountStatus::Active) { + return false; + } + + $now = now(); + + if ($this->starts_at !== null && $this->starts_at->isFuture()) { + return false; + } + + if ($this->ends_at !== null && $this->ends_at->isPast()) { + return false; + } + + if ($this->usage_limit !== null && (int) $this->usage_count >= (int) $this->usage_limit) { + return false; + } + + return true; + } +} diff --git a/app/Models/Fulfillment.php b/app/Models/Fulfillment.php new file mode 100644 index 00000000..a3129da5 --- /dev/null +++ b/app/Models/Fulfillment.php @@ -0,0 +1,55 @@ + */ + use HasFactory; + + const UPDATED_AT = null; + + protected $fillable = [ + 'order_id', + 'status', + 'tracking_company', + 'tracking_number', + 'tracking_url', + 'shipped_at', + 'delivered_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => FulfillmentShipmentStatus::class, + 'shipped_at' => 'datetime', + 'delivered_at' => 'datetime', + ]; + } + + /** + * @return BelongsTo + */ + public function order(): BelongsTo + { + return $this->belongsTo(Order::class); + } + + /** + * @return HasMany + */ + public function lines(): HasMany + { + return $this->hasMany(FulfillmentLine::class); + } +} diff --git a/app/Models/FulfillmentLine.php b/app/Models/FulfillmentLine.php new file mode 100644 index 00000000..87ca4426 --- /dev/null +++ b/app/Models/FulfillmentLine.php @@ -0,0 +1,37 @@ + */ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'fulfillment_id', + 'order_line_id', + 'quantity', + ]; + + /** + * @return BelongsTo + */ + public function fulfillment(): BelongsTo + { + return $this->belongsTo(Fulfillment::class); + } + + /** + * @return BelongsTo + */ + public function orderLine(): BelongsTo + { + return $this->belongsTo(OrderLine::class); + } +} diff --git a/app/Models/InventoryItem.php b/app/Models/InventoryItem.php new file mode 100644 index 00000000..da8560b7 --- /dev/null +++ b/app/Models/InventoryItem.php @@ -0,0 +1,48 @@ + */ + use BelongsToStore, HasFactory; + + const CREATED_AT = null; + + protected $fillable = [ + 'store_id', + 'variant_id', + 'quantity_on_hand', + 'quantity_reserved', + 'policy', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'policy' => InventoryPolicy::class, + ]; + } + + /** + * @return BelongsTo + */ + public function variant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class, 'variant_id'); + } + + public function quantityAvailable(): int + { + return (int) $this->quantity_on_hand - (int) $this->quantity_reserved; + } +} diff --git a/app/Models/NavigationItem.php b/app/Models/NavigationItem.php new file mode 100644 index 00000000..be19c28c --- /dev/null +++ b/app/Models/NavigationItem.php @@ -0,0 +1,55 @@ + */ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'menu_id', + 'type', + 'label', + 'url', + 'resource_id', + 'position', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'type' => NavigationItemType::class, + 'resource_id' => 'integer', + 'position' => 'integer', + ]; + } + + /** + * @return BelongsTo + */ + public function menu(): BelongsTo + { + return $this->belongsTo(NavigationMenu::class, 'menu_id'); + } + + public function resolveUrl(): string + { + return match ($this->type) { + NavigationItemType::Link => (string) ($this->url ?? '#'), + NavigationItemType::Page => '/pages/'.$this->resource_id, + NavigationItemType::Collection => '/collections/'.$this->resource_id, + NavigationItemType::Product => '/products/'.$this->resource_id, + }; + } +} diff --git a/app/Models/NavigationMenu.php b/app/Models/NavigationMenu.php new file mode 100644 index 00000000..105096a2 --- /dev/null +++ b/app/Models/NavigationMenu.php @@ -0,0 +1,28 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'handle', + 'title', + ]; + + /** + * @return HasMany + */ + public function items(): HasMany + { + return $this->hasMany(NavigationItem::class, 'menu_id')->orderBy('position'); + } +} diff --git a/app/Models/Order.php b/app/Models/Order.php new file mode 100644 index 00000000..d54d5849 --- /dev/null +++ b/app/Models/Order.php @@ -0,0 +1,123 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_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', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'payment_method' => PaymentMethod::class, + 'status' => OrderStatus::class, + 'financial_status' => FinancialStatus::class, + 'fulfillment_status' => FulfillmentStatus::class, + 'billing_address_json' => 'array', + 'shipping_address_json' => 'array', + 'placed_at' => 'datetime', + ]; + } + + /** + * @return BelongsTo + */ + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + /** + * @return HasMany + */ + public function lines(): HasMany + { + return $this->hasMany(OrderLine::class); + } + + /** + * @return HasMany + */ + public function payments(): HasMany + { + return $this->hasMany(Payment::class); + } + + /** + * @return HasMany + */ + public function refunds(): HasMany + { + return $this->hasMany(Refund::class); + } + + /** + * @return HasMany + */ + public function fulfillments(): HasMany + { + return $this->hasMany(Fulfillment::class); + } + + public function requiresShipping(): bool + { + if ((int) $this->shipping_amount > 0) { + return true; + } + + foreach ($this->lines as $line) { + if ($line->variant !== null && (bool) $line->variant->requires_shipping) { + return true; + } + } + + return false; + } + + public function refundedTotal(): int + { + return (int) $this->refunds() + ->where('status', RefundStatus::Processed->value) + ->sum('amount'); + } + + public function refundableAmount(): int + { + return max(0, (int) $this->total_amount - $this->refundedTotal()); + } +} diff --git a/app/Models/OrderLine.php b/app/Models/OrderLine.php new file mode 100644 index 00000000..b06a18d8 --- /dev/null +++ b/app/Models/OrderLine.php @@ -0,0 +1,72 @@ + */ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'order_id', + 'product_id', + 'variant_id', + 'title_snapshot', + 'sku_snapshot', + 'quantity', + 'unit_price_amount', + 'total_amount', + 'tax_lines_json', + 'discount_allocations_json', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'tax_lines_json' => 'array', + 'discount_allocations_json' => 'array', + ]; + } + + /** + * @return BelongsTo + */ + public function order(): BelongsTo + { + return $this->belongsTo(Order::class); + } + + /** + * @return BelongsTo + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + /** + * @return BelongsTo + */ + public function variant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class, 'variant_id'); + } + + /** + * @return HasMany + */ + public function fulfillmentLines(): HasMany + { + return $this->hasMany(FulfillmentLine::class); + } +} diff --git a/app/Models/Organization.php b/app/Models/Organization.php new file mode 100644 index 00000000..ed94f758 --- /dev/null +++ b/app/Models/Organization.php @@ -0,0 +1,26 @@ + */ + use HasFactory; + + protected $fillable = [ + 'name', + 'billing_email', + ]; + + /** + * @return HasMany + */ + public function stores(): HasMany + { + return $this->hasMany(Store::class); + } +} diff --git a/app/Models/Page.php b/app/Models/Page.php new file mode 100644 index 00000000..060539a3 --- /dev/null +++ b/app/Models/Page.php @@ -0,0 +1,34 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'title', + 'handle', + 'body_html', + 'status', + 'published_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => PageStatus::class, + 'published_at' => 'datetime', + ]; + } +} diff --git a/app/Models/Payment.php b/app/Models/Payment.php new file mode 100644 index 00000000..a306852a --- /dev/null +++ b/app/Models/Payment.php @@ -0,0 +1,57 @@ + */ + use HasFactory; + + const UPDATED_AT = null; + + protected $fillable = [ + 'order_id', + 'provider', + 'method', + 'provider_payment_id', + 'status', + 'amount', + 'currency', + 'raw_json_encrypted', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'method' => PaymentMethod::class, + 'status' => PaymentStatus::class, + 'raw_json_encrypted' => 'encrypted', + ]; + } + + /** + * @return BelongsTo + */ + public function order(): BelongsTo + { + return $this->belongsTo(Order::class); + } + + /** + * @return HasMany + */ + public function refunds(): HasMany + { + return $this->hasMany(Refund::class); + } +} diff --git a/app/Models/Product.php b/app/Models/Product.php new file mode 100644 index 00000000..8fb060ee --- /dev/null +++ b/app/Models/Product.php @@ -0,0 +1,73 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'title', + 'handle', + 'status', + 'description_html', + 'vendor', + 'product_type', + 'tags', + 'published_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => ProductStatus::class, + 'tags' => 'array', + 'published_at' => 'datetime', + ]; + } + + /** + * @return HasMany + */ + public function variants(): HasMany + { + return $this->hasMany(ProductVariant::class); + } + + /** + * @return HasMany + */ + public function options(): HasMany + { + return $this->hasMany(ProductOption::class); + } + + /** + * @return HasMany + */ + public function media(): HasMany + { + return $this->hasMany(ProductMedia::class); + } + + /** + * @return BelongsToMany + */ + public function collections(): BelongsToMany + { + return $this->belongsToMany(Collection::class, 'collection_products') + ->withPivot('position'); + } +} diff --git a/app/Models/ProductMedia.php b/app/Models/ProductMedia.php new file mode 100644 index 00000000..2b5fa32e --- /dev/null +++ b/app/Models/ProductMedia.php @@ -0,0 +1,51 @@ + */ + use HasFactory; + + protected $table = 'product_media'; + + const UPDATED_AT = null; + + protected $fillable = [ + 'product_id', + 'type', + 'storage_key', + 'alt_text', + 'width', + 'height', + 'mime_type', + 'byte_size', + 'position', + 'status', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'type' => MediaType::class, + 'status' => MediaStatus::class, + ]; + } + + /** + * @return BelongsTo + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } +} diff --git a/app/Models/ProductOption.php b/app/Models/ProductOption.php new file mode 100644 index 00000000..de8f9f5b --- /dev/null +++ b/app/Models/ProductOption.php @@ -0,0 +1,38 @@ + */ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'product_id', + 'name', + 'position', + ]; + + /** + * @return BelongsTo + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + /** + * @return HasMany + */ + public function values(): HasMany + { + return $this->hasMany(ProductOptionValue::class); + } +} diff --git a/app/Models/ProductOptionValue.php b/app/Models/ProductOptionValue.php new file mode 100644 index 00000000..b2f0c8b3 --- /dev/null +++ b/app/Models/ProductOptionValue.php @@ -0,0 +1,29 @@ + */ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'product_option_id', + 'value', + 'position', + ]; + + /** + * @return BelongsTo + */ + public function option(): BelongsTo + { + return $this->belongsTo(ProductOption::class, 'product_option_id'); + } +} diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php new file mode 100644 index 00000000..8255aa81 --- /dev/null +++ b/app/Models/ProductVariant.php @@ -0,0 +1,71 @@ + */ + use HasFactory; + + protected $fillable = [ + 'product_id', + 'sku', + 'barcode', + 'price_amount', + 'compare_at_amount', + 'currency', + 'weight_g', + 'requires_shipping', + 'is_default', + 'position', + 'status', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => VariantStatus::class, + 'requires_shipping' => 'bool', + 'is_default' => 'bool', + ]; + } + + /** + * @return BelongsTo + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + /** + * @return HasOne + */ + public function inventoryItem(): HasOne + { + return $this->hasOne(InventoryItem::class, 'variant_id'); + } + + /** + * @return BelongsToMany + */ + public function optionValues(): BelongsToMany + { + return $this->belongsToMany( + ProductOptionValue::class, + 'variant_option_values', + 'variant_id', + 'product_option_value_id' + ); + } +} diff --git a/app/Models/Refund.php b/app/Models/Refund.php new file mode 100644 index 00000000..dde9d850 --- /dev/null +++ b/app/Models/Refund.php @@ -0,0 +1,51 @@ + */ + use HasFactory; + + const UPDATED_AT = null; + + protected $fillable = [ + 'order_id', + 'payment_id', + 'amount', + 'reason', + 'status', + 'provider_refund_id', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => RefundStatus::class, + ]; + } + + /** + * @return BelongsTo + */ + public function order(): BelongsTo + { + return $this->belongsTo(Order::class); + } + + /** + * @return BelongsTo + */ + public function payment(): BelongsTo + { + return $this->belongsTo(Payment::class); + } +} diff --git a/app/Models/Scopes/StoreScope.php b/app/Models/Scopes/StoreScope.php new file mode 100644 index 00000000..ad4711b2 --- /dev/null +++ b/app/Models/Scopes/StoreScope.php @@ -0,0 +1,23 @@ +bound('current_store')) { + return; + } + + /** @var Store $store */ + $store = app('current_store'); + + $builder->where($model->getTable().'.store_id', $store->id); + } +} diff --git a/app/Models/ShippingRate.php b/app/Models/ShippingRate.php new file mode 100644 index 00000000..91f713e6 --- /dev/null +++ b/app/Models/ShippingRate.php @@ -0,0 +1,42 @@ + */ + use HasFactory; + + protected $fillable = [ + 'zone_id', + 'name', + 'type', + 'config_json', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'type' => ShippingRateType::class, + 'config_json' => 'array', + 'is_active' => 'bool', + ]; + } + + /** + * @return BelongsTo + */ + public function zone(): BelongsTo + { + return $this->belongsTo(ShippingZone::class, 'zone_id'); + } +} diff --git a/app/Models/ShippingZone.php b/app/Models/ShippingZone.php new file mode 100644 index 00000000..b659464a --- /dev/null +++ b/app/Models/ShippingZone.php @@ -0,0 +1,40 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'name', + 'countries_json', + 'regions_json', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'countries_json' => 'array', + 'regions_json' => 'array', + ]; + } + + /** + * @return HasMany + */ + public function rates(): HasMany + { + return $this->hasMany(ShippingRate::class, 'zone_id'); + } +} diff --git a/app/Models/Store.php b/app/Models/Store.php new file mode 100644 index 00000000..ed4a7bd0 --- /dev/null +++ b/app/Models/Store.php @@ -0,0 +1,71 @@ + */ + use HasFactory; + + protected $fillable = [ + 'organization_id', + 'name', + 'handle', + 'status', + 'default_currency', + 'default_locale', + 'timezone', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => StoreStatus::class, + ]; + } + + /** + * @return BelongsTo + */ + public function organization(): BelongsTo + { + return $this->belongsTo(Organization::class); + } + + /** + * @return HasMany + */ + public function domains(): HasMany + { + return $this->hasMany(StoreDomain::class); + } + + /** + * @return BelongsToMany + */ + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'store_users') + ->using(StoreUser::class) + ->withPivot('role'); + } + + /** + * @return HasOne + */ + public function settings(): HasOne + { + return $this->hasOne(StoreSettings::class); + } +} diff --git a/app/Models/StoreDomain.php b/app/Models/StoreDomain.php new file mode 100644 index 00000000..a62a4d70 --- /dev/null +++ b/app/Models/StoreDomain.php @@ -0,0 +1,43 @@ + */ + use HasFactory; + + public const UPDATED_AT = null; + + protected $fillable = [ + 'store_id', + 'hostname', + 'type', + 'is_primary', + 'tls_mode', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'type' => StoreDomainType::class, + 'is_primary' => 'boolean', + ]; + } + + /** + * @return BelongsTo + */ + public function store(): BelongsTo + { + return $this->belongsTo(Store::class); + } +} diff --git a/app/Models/StoreSettings.php b/app/Models/StoreSettings.php new file mode 100644 index 00000000..c7a07dba --- /dev/null +++ b/app/Models/StoreSettings.php @@ -0,0 +1,44 @@ + */ + use HasFactory; + + protected $table = 'store_settings'; + + protected $primaryKey = 'store_id'; + + public $incrementing = false; + + public const CREATED_AT = null; + + protected $fillable = [ + 'store_id', + 'settings_json', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'settings_json' => 'array', + ]; + } + + /** + * @return BelongsTo + */ + public function store(): BelongsTo + { + return $this->belongsTo(Store::class); + } +} diff --git a/app/Models/StoreUser.php b/app/Models/StoreUser.php new file mode 100644 index 00000000..8c5e63f6 --- /dev/null +++ b/app/Models/StoreUser.php @@ -0,0 +1,36 @@ +attributes['created_at'])) { + $pivot->setAttribute('created_at', $pivot->freshTimestamp()); + } + }); + } + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'role' => StoreUserRole::class, + ]; + } +} diff --git a/app/Models/TaxSettings.php b/app/Models/TaxSettings.php new file mode 100644 index 00000000..7fa8441c --- /dev/null +++ b/app/Models/TaxSettings.php @@ -0,0 +1,52 @@ + */ + use HasFactory; + + protected $table = 'tax_settings'; + + protected $primaryKey = 'store_id'; + + public $incrementing = false; + + protected $keyType = 'int'; + + const CREATED_AT = null; + + protected $fillable = [ + 'store_id', + 'mode', + 'provider', + 'prices_include_tax', + 'config_json', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'mode' => TaxMode::class, + 'prices_include_tax' => 'bool', + 'config_json' => 'array', + ]; + } + + /** + * @return BelongsTo + */ + public function store(): BelongsTo + { + return $this->belongsTo(Store::class); + } +} diff --git a/app/Models/Theme.php b/app/Models/Theme.php new file mode 100644 index 00000000..a30e6a6d --- /dev/null +++ b/app/Models/Theme.php @@ -0,0 +1,51 @@ + */ + use BelongsToStore, HasFactory; + + protected $fillable = [ + 'store_id', + 'name', + 'version', + 'status', + 'published_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => ThemeStatus::class, + 'published_at' => 'datetime', + ]; + } + + /** + * @return HasMany + */ + public function files(): HasMany + { + return $this->hasMany(ThemeFile::class); + } + + /** + * @return HasOne + */ + public function settings(): HasOne + { + return $this->hasOne(ThemeSettings::class); + } +} diff --git a/app/Models/ThemeFile.php b/app/Models/ThemeFile.php new file mode 100644 index 00000000..e5b1ce97 --- /dev/null +++ b/app/Models/ThemeFile.php @@ -0,0 +1,41 @@ + */ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'theme_id', + 'path', + 'storage_key', + 'sha256', + 'byte_size', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'byte_size' => 'integer', + ]; + } + + /** + * @return BelongsTo + */ + public function theme(): BelongsTo + { + return $this->belongsTo(Theme::class); + } +} diff --git a/app/Models/ThemeSettings.php b/app/Models/ThemeSettings.php new file mode 100644 index 00000000..d0093377 --- /dev/null +++ b/app/Models/ThemeSettings.php @@ -0,0 +1,46 @@ + */ + use HasFactory; + + protected $table = 'theme_settings'; + + protected $primaryKey = 'theme_id'; + + public $incrementing = false; + + protected $keyType = 'int'; + + const CREATED_AT = null; + + protected $fillable = [ + 'theme_id', + 'settings_json', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'settings_json' => 'array', + ]; + } + + /** + * @return BelongsTo + */ + public function theme(): BelongsTo + { + return $this->belongsTo(Theme::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 214bea4e..8639975f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,17 +2,20 @@ namespace App\Models; +use App\Enums\StoreUserRole; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; use Laravel\Fortify\TwoFactorAuthenticatable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, TwoFactorAuthenticatable; + use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; /** * The attributes that are mass assignable. @@ -23,6 +26,8 @@ class User extends Authenticatable 'name', 'email', 'password', + 'status', + 'last_login_at', ]; /** @@ -46,7 +51,9 @@ protected function casts(): array { return [ 'email_verified_at' => 'datetime', + 'last_login_at' => 'datetime', 'password' => 'hashed', + 'status' => 'string', ]; } @@ -61,4 +68,35 @@ public function initials(): string ->map(fn ($word) => Str::substr($word, 0, 1)) ->implode(''); } + + /** + * @return BelongsToMany + */ + public function stores(): BelongsToMany + { + return $this->belongsToMany(Store::class, 'store_users') + ->using(StoreUser::class) + ->withPivot('role'); + } + + public function roleForStore(Store $store): ?StoreUserRole + { + /** @var Store|null $match */ + $match = $this->stores()->where('stores.id', $store->id)->first(); + + if ($match === null) { + return null; + } + + /** @var StoreUser $pivot */ + $pivot = $match->pivot; + + $role = $pivot->role; + + if ($role instanceof StoreUserRole) { + return $role; + } + + return $role === null ? null : StoreUserRole::from($role); + } } diff --git a/app/Models/WebhookDelivery.php b/app/Models/WebhookDelivery.php new file mode 100644 index 00000000..9664b700 --- /dev/null +++ b/app/Models/WebhookDelivery.php @@ -0,0 +1,46 @@ + */ + use HasFactory; + + protected $table = 'webhook_deliveries'; + + public const UPDATED_AT = null; + + protected $fillable = [ + 'subscription_id', + 'event_type', + 'payload_json', + 'response_status', + 'response_body', + 'attempts', + 'delivered_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'payload_json' => 'array', + 'delivered_at' => 'datetime', + ]; + } + + /** + * @return BelongsTo + */ + public function subscription(): BelongsTo + { + return $this->belongsTo(WebhookSubscription::class, 'subscription_id'); + } +} diff --git a/app/Models/WebhookSubscription.php b/app/Models/WebhookSubscription.php new file mode 100644 index 00000000..6ff8493e --- /dev/null +++ b/app/Models/WebhookSubscription.php @@ -0,0 +1,43 @@ + */ + use BelongsToStore, HasFactory; + + protected $table = 'webhook_subscriptions'; + + protected $fillable = [ + 'store_id', + 'app_installation_id', + 'event_type', + 'url', + 'secret', + 'status', + 'failed_count', + ]; + + /** + * @return BelongsTo + */ + public function appInstallation(): BelongsTo + { + return $this->belongsTo(AppInstallation::class); + } + + /** + * @return HasMany + */ + public function deliveries(): HasMany + { + return $this->hasMany(WebhookDelivery::class, 'subscription_id'); + } +} diff --git a/app/Observers/ProductObserver.php b/app/Observers/ProductObserver.php new file mode 100644 index 00000000..7f73064b --- /dev/null +++ b/app/Observers/ProductObserver.php @@ -0,0 +1,26 @@ +searchService->syncProduct($product); + } + + public function updated(Product $product): void + { + $this->searchService->syncProduct($product); + } + + public function deleted(Product $product): void + { + $this->searchService->removeProduct($product->id); + } +} diff --git a/app/Policies/Concerns/ChecksStoreRole.php b/app/Policies/Concerns/ChecksStoreRole.php new file mode 100644 index 00000000..ba3052bc --- /dev/null +++ b/app/Policies/Concerns/ChecksStoreRole.php @@ -0,0 +1,29 @@ +bound('current_store')) { + return null; + } + + /** @var Store $store */ + $store = app('current_store'); + + return $user->roleForStore($store); + } + + protected function hasMinRole(User $user, StoreUserRole ...$roles): bool + { + $role = $this->getUserRole($user); + + return $role !== null && in_array($role, $roles, true); + } +} diff --git a/app/Policies/StorePolicy.php b/app/Policies/StorePolicy.php new file mode 100644 index 00000000..9dcfafa9 --- /dev/null +++ b/app/Policies/StorePolicy.php @@ -0,0 +1,30 @@ +roleForStore($store) !== null; + } + + public function update(User $user, Store $store): bool + { + $role = $user->roleForStore($store); + + return in_array($role, [StoreUserRole::Owner, StoreUserRole::Admin], true); + } + + public function delete(User $user, Store $store): bool + { + return $user->roleForStore($store) === StoreUserRole::Owner; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8a29e6f5..bd551751 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,9 +2,24 @@ namespace App\Providers; +use App\Auth\CustomerUserProvider; +use App\Contracts\PaymentProvider; +use App\Events\OrderCreated; +use App\Events\OrderFulfilled; +use App\Events\OrderPaid; +use App\Listeners\DispatchOrderWebhooks; +use App\Models\Product; +use App\Observers\ProductObserver; +use App\Services\Payments\MockPaymentProvider; +use App\Services\ThemeSettingsService; use Carbon\CarbonImmutable; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rules\Password; @@ -15,7 +30,8 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(ThemeSettingsService::class); + $this->app->bind(PaymentProvider::class, MockPaymentProvider::class); } /** @@ -24,6 +40,36 @@ public function register(): void public function boot(): void { $this->configureDefaults(); + $this->configureRateLimiters(); + $this->configureCustomerAuthProvider(); + $this->configureWebhookListeners(); + + Product::observe(ProductObserver::class); + } + + protected function configureWebhookListeners(): void + { + Event::listen(OrderCreated::class, [DispatchOrderWebhooks::class, 'handleCreated']); + Event::listen(OrderPaid::class, [DispatchOrderWebhooks::class, 'handlePaid']); + Event::listen(OrderFulfilled::class, [DispatchOrderWebhooks::class, 'handleFulfilled']); + } + + protected function configureRateLimiters(): void + { + RateLimiter::for('login', fn (Request $request): Limit => Limit::perMinute(5)->by((string) $request->ip())); + } + + /** + * Customer model is created in Phase 6. Guard/provider configured in advance. + */ + protected function configureCustomerAuthProvider(): void + { + Auth::provider('customer', function ($app, array $config): CustomerUserProvider { + /** @var class-string<\Illuminate\Database\Eloquent\Model> $model */ + $model = $config['model']; + + return new CustomerUserProvider($app['hash'], $model); + }); } /** diff --git a/app/Services/AnalyticsService.php b/app/Services/AnalyticsService.php new file mode 100644 index 00000000..d8cb4289 --- /dev/null +++ b/app/Services/AnalyticsService.php @@ -0,0 +1,39 @@ + $properties + */ + public function track(Store $store, string $type, array $properties = [], ?string $sessionId = null, ?int $customerId = null): AnalyticsEvent + { + return AnalyticsEvent::create([ + 'store_id' => $store->id, + 'type' => $type, + 'session_id' => $sessionId, + 'customer_id' => $customerId, + 'properties_json' => $properties, + 'client_event_id' => $properties['client_event_id'] ?? null, + 'occurred_at' => now(), + ]); + } + + /** + * @return Collection + */ + public function getDailyMetrics(Store $store, string $startDate, string $endDate): Collection + { + return AnalyticsDaily::query() + ->where('store_id', $store->id) + ->whereBetween('date', [$startDate, $endDate]) + ->orderBy('date') + ->get(); + } +} diff --git a/app/Services/CartService.php b/app/Services/CartService.php new file mode 100644 index 00000000..5c66ab3c --- /dev/null +++ b/app/Services/CartService.php @@ -0,0 +1,179 @@ +store_id = $store->id; + $cart->customer_id = $customer?->id; + $cart->session_id = $sessionId; + $cart->currency = (string) ($store->default_currency ?? 'USD'); + $cart->cart_version = 1; + $cart->status = CartStatus::Active->value; + $cart->save(); + + return $cart; + } + + public function getOrCreateForSession(Store $store, ?object $customer = null, ?string $sessionId = null): Cart + { + $query = Cart::query() + ->where('store_id', $store->id) + ->where('status', CartStatus::Active->value); + + if ($customer !== null) { + $existing = (clone $query)->where('customer_id', $customer->id)->first(); + if ($existing !== null) { + return $existing; + } + } + + if ($sessionId !== null) { + $existing = (clone $query)->where('session_id', $sessionId)->first(); + if ($existing !== null) { + return $existing; + } + } + + return $this->create($store, $customer, $sessionId); + } + + public function addLine(Cart $cart, int $variantId, int $quantity): CartLine + { + if ($quantity < 1) { + throw new InvalidArgumentException('Quantity must be at least 1.'); + } + + $variant = ProductVariant::query() + ->with(['product', 'inventoryItem']) + ->find($variantId); + + if ($variant === null) { + throw new RuntimeException("Variant {$variantId} not found."); + } + + if ($variant->status !== VariantStatus::Active) { + throw new RuntimeException("Variant {$variantId} is not active."); + } + + $product = $variant->product; + if ($product === null || $product->status !== ProductStatus::Active) { + throw new RuntimeException("Product for variant {$variantId} is not active."); + } + + $existing = $cart->lines()->where('variant_id', $variantId)->first(); + $targetQty = ($existing?->quantity ?? 0) + $quantity; + + $inventoryItem = $variant->inventoryItem; + if ($inventoryItem !== null && ! $this->inventoryService->checkAvailability($inventoryItem, $targetQty)) { + throw new RuntimeException("Insufficient inventory for variant {$variantId}."); + } + + if ($existing !== null) { + $existing->quantity = $targetQty; + $existing->line_subtotal_amount = (int) $existing->unit_price_amount * $targetQty; + $existing->line_total_amount = $existing->line_subtotal_amount - (int) $existing->line_discount_amount; + $existing->save(); + $this->touchVersion($cart); + + return $existing; + } + + $unitPrice = (int) $variant->price_amount; + $line = new CartLine; + $line->cart_id = $cart->id; + $line->variant_id = $variantId; + $line->quantity = $quantity; + $line->unit_price_amount = $unitPrice; + $line->line_subtotal_amount = $unitPrice * $quantity; + $line->line_discount_amount = 0; + $line->line_total_amount = $unitPrice * $quantity; + $line->save(); + + $this->touchVersion($cart); + + return $line; + } + + public function updateLineQuantity(Cart $cart, int $lineId, int $quantity): CartLine + { + if ($quantity < 1) { + throw new InvalidArgumentException('Quantity must be at least 1.'); + } + + $line = $cart->lines()->findOrFail($lineId); + + $variant = ProductVariant::query()->with('inventoryItem')->findOrFail($line->variant_id); + $inventoryItem = $variant->inventoryItem; + if ($inventoryItem !== null && ! $this->inventoryService->checkAvailability($inventoryItem, $quantity)) { + throw new RuntimeException("Insufficient inventory for variant {$line->variant_id}."); + } + + $line->quantity = $quantity; + $line->line_subtotal_amount = (int) $line->unit_price_amount * $quantity; + $line->line_total_amount = $line->line_subtotal_amount - (int) $line->line_discount_amount; + $line->save(); + + $this->touchVersion($cart); + + return $line; + } + + public function removeLine(Cart $cart, int $lineId): void + { + $line = $cart->lines()->findOrFail($lineId); + $line->delete(); + + $this->touchVersion($cart); + } + + public function mergeOnLogin(Cart $guest, Cart $customer): Cart + { + foreach ($guest->lines()->get() as $guestLine) { + $existing = $customer->lines()->where('variant_id', $guestLine->variant_id)->first(); + + if ($existing !== null) { + $newQty = (int) $existing->quantity + (int) $guestLine->quantity; + $existing->quantity = $newQty; + $existing->line_subtotal_amount = (int) $existing->unit_price_amount * $newQty; + $existing->line_total_amount = $existing->line_subtotal_amount - (int) $existing->line_discount_amount; + $existing->save(); + } else { + $copy = $guestLine->replicate(); + $copy->cart_id = $customer->id; + $copy->save(); + } + } + + $guest->lines()->delete(); + $guest->status = CartStatus::Abandoned->value; + $guest->save(); + + $this->touchVersion($customer); + + return $customer; + } + + private function touchVersion(Cart $cart): void + { + $cart->incrementVersion(); + $cart->save(); + } +} diff --git a/app/Services/CheckoutService.php b/app/Services/CheckoutService.php new file mode 100644 index 00000000..468c6fbc --- /dev/null +++ b/app/Services/CheckoutService.php @@ -0,0 +1,255 @@ +store_id = $cart->store_id; + $checkout->cart_id = $cart->id; + $checkout->customer_id = $cart->customer_id; + $checkout->status = CheckoutStatus::Started->value; + $checkout->save(); + + return $checkout; + }); + } + + /** + * @param array $data + */ + public function setAddress(Checkout $checkout, array $data): Checkout + { + $this->assertTransitionAllowed($checkout, [CheckoutStatus::Started, CheckoutStatus::Addressed]); + + $checkout->email = $data['email'] ?? $checkout->email; + $shipping = $data['shipping_address'] ?? []; + $billing = $data['billing_address'] ?? $shipping; + + $checkout->shipping_address_json = $shipping; + $checkout->billing_address_json = $billing; + $checkout->status = CheckoutStatus::Addressed->value; + $checkout->save(); + + return $this->recalculate($checkout); + } + + public function setShippingMethod(Checkout $checkout, int $shippingRateId): Checkout + { + $this->assertTransitionAllowed($checkout, [ + CheckoutStatus::Addressed, + CheckoutStatus::ShippingSelected, + ]); + + $rate = ShippingRate::query()->findOrFail($shippingRateId); + + $checkout->shipping_method_id = $rate->id; + $checkout->status = CheckoutStatus::ShippingSelected->value; + $checkout->save(); + + return $this->recalculate($checkout); + } + + public function selectPaymentMethod(Checkout $checkout, string $method): Checkout + { + $allowedStates = [CheckoutStatus::ShippingSelected, CheckoutStatus::PaymentSelected]; + + $cart = $checkout->cart()->with('lines.variant')->first(); + $requiresShipping = $cart !== null && $cart->lines->contains( + fn ($line): bool => $line->variant !== null && (bool) $line->variant->requires_shipping + ); + + if (! $requiresShipping) { + $allowedStates[] = CheckoutStatus::Addressed; + } + + $this->assertTransitionAllowed($checkout, $allowedStates); + + $allowed = ['credit_card', 'paypal', 'bank_transfer']; + if (! in_array($method, $allowed, true)) { + throw new DomainException("Invalid payment method: {$method}"); + } + + return DB::transaction(function () use ($checkout, $method): Checkout { + $checkout->payment_method = $method; + $checkout->status = CheckoutStatus::PaymentSelected->value; + $checkout->expires_at = now()->addHours(24); + $checkout->save(); + + $cart = $checkout->cart()->with('lines.variant.inventoryItem')->first(); + if ($cart !== null) { + foreach ($cart->lines as $line) { + $inventoryItem = $line->variant?->inventoryItem; + if ($inventoryItem !== null) { + $this->inventoryService->reserve($inventoryItem, (int) $line->quantity); + } + } + } + + return $checkout; + }); + } + + public function applyDiscount(Checkout $checkout, string $code): Checkout + { + $checkout->discount_code = $code; + $checkout->save(); + + return $this->recalculate($checkout); + } + + public function recalculate(Checkout $checkout): Checkout + { + $result = $this->pricingEngine->calculate($checkout); + $checkout->totals_json = $result->toArray(); + $checkout->save(); + + return $checkout; + } + + public function expire(Checkout $checkout): void + { + DB::transaction(function () use ($checkout): void { + if ($checkout->status === CheckoutStatus::PaymentSelected) { + $cart = $checkout->cart()->with('lines.variant.inventoryItem')->first(); + if ($cart !== null) { + foreach ($cart->lines as $line) { + $inventoryItem = $line->variant?->inventoryItem; + if ($inventoryItem !== null) { + $this->inventoryService->release($inventoryItem, (int) $line->quantity); + } + } + } + } + + $checkout->status = CheckoutStatus::Expired->value; + $checkout->save(); + }); + } + + /** + * @param array $details + */ + public function complete(Checkout $checkout, array $details = []): Checkout + { + $this->assertTransitionAllowed($checkout, [CheckoutStatus::PaymentSelected]); + + $methodString = $checkout->payment_method; + if ($methodString === null) { + throw new DomainException('Checkout has no payment method selected.'); + } + $method = PaymentMethod::from((string) $methodString); + + $result = $this->paymentProvider->charge($checkout, $method, $details); + + if ($result->failed()) { + $this->releaseReservedInventory($checkout); + throw new PaymentFailedException($result->errorMessage ?? 'Payment failed.'); + } + + return DB::transaction(function () use ($checkout, $method, $result): Checkout { + $order = $this->orderService->createFromCheckout($checkout); + + /** @var Payment $payment */ + $payment = Payment::create([ + 'order_id' => $order->id, + 'provider' => 'mock', + 'method' => $method->value, + 'provider_payment_id' => $result->providerPaymentId, + 'status' => $result->status->value, + 'amount' => $result->amount, + 'currency' => $result->currency, + 'raw_json_encrypted' => null, + ]); + + if ($result->successful()) { + $order->update([ + 'financial_status' => FinancialStatus::Paid->value, + 'status' => OrderStatus::Paid->value, + ]); + + $this->commitReservedInventory($order); + + OrderPaid::dispatch($order->fresh() ?? $order); + } elseif ($result->pending()) { + $payment->update(['status' => PaymentStatus::Pending->value]); + } + + return $checkout->fresh() ?? $checkout; + }); + } + + private function releaseReservedInventory(Checkout $checkout): void + { + $cart = $checkout->cart()->with('lines.variant.inventoryItem')->first(); + if ($cart === null) { + return; + } + + foreach ($cart->lines as $line) { + $inventoryItem = $line->variant?->inventoryItem; + if ($inventoryItem !== null) { + $this->inventoryService->release($inventoryItem, (int) $line->quantity); + } + } + } + + private function commitReservedInventory(Order $order): void + { + $order->loadMissing('lines'); + foreach ($order->lines as $line) { + if ($line->variant_id === null) { + continue; + } + $item = InventoryItem::withoutGlobalScopes() + ->where('variant_id', $line->variant_id) + ->first(); + if ($item !== null) { + $this->inventoryService->commit($item, (int) $line->quantity); + } + } + } + + /** + * @param array $allowed + */ + private function assertTransitionAllowed(Checkout $checkout, array $allowed): void + { + $current = $checkout->status instanceof CheckoutStatus + ? $checkout->status + : CheckoutStatus::from((string) $checkout->status); + + if (! in_array($current, $allowed, true)) { + throw new DomainException( + "Invalid checkout transition from {$current->value}." + ); + } + } +} diff --git a/app/Services/DiscountService.php b/app/Services/DiscountService.php new file mode 100644 index 00000000..541b2c09 --- /dev/null +++ b/app/Services/DiscountService.php @@ -0,0 +1,117 @@ +where('store_id', $store->id) + ->whereRaw('LOWER(code) = ?', [strtolower(trim($code))]) + ->first(); + + if ($discount === null) { + throw InvalidDiscountException::notFound(); + } + + if ($discount->status === DiscountStatus::Disabled) { + throw InvalidDiscountException::disabled(); + } + + if ($discount->status !== DiscountStatus::Active) { + throw InvalidDiscountException::expired(); + } + + $now = now(); + + if ($discount->starts_at !== null && $discount->starts_at->greaterThan($now)) { + throw InvalidDiscountException::notYetActive(); + } + + if ($discount->ends_at !== null && $discount->ends_at->lessThan($now)) { + throw InvalidDiscountException::expired(); + } + + if ($discount->usage_limit !== null && (int) $discount->usage_count >= (int) $discount->usage_limit) { + throw InvalidDiscountException::usageLimitReached(); + } + + $rules = $discount->rules_json ?? []; + $minPurchase = $rules['min_purchase_amount'] ?? null; + + if ($minPurchase !== null) { + $lines = $cart->relationLoaded('lines') ? $cart->lines : $cart->lines()->get(); + $subtotal = (int) $lines->sum('line_subtotal_amount'); + + if ($subtotal < (int) $minPurchase) { + throw InvalidDiscountException::minimumNotMet(); + } + } + + return $discount; + } + + /** + * @param Collection|\Illuminate\Database\Eloquent\Collection $lines + */ + public function calculate(Discount $discount, int $subtotal, Collection|\Illuminate\Database\Eloquent\Collection $lines): DiscountResult + { + if ($discount->value_type === DiscountValueType::FreeShipping) { + return new DiscountResult(amount: 0, allocations: [], freeShipping: true); + } + + if ($subtotal <= 0 || $lines->isEmpty()) { + return new DiscountResult(amount: 0, allocations: []); + } + + $totalDiscount = match ($discount->value_type) { + DiscountValueType::Percent => (int) floor($subtotal * (int) $discount->value_amount / 100), + DiscountValueType::Fixed => min((int) $discount->value_amount, $subtotal), + default => 0, + }; + + if ($totalDiscount <= 0) { + return new DiscountResult(amount: 0, allocations: []); + } + + $allocations = []; + $remaining = $totalDiscount; + + $lineList = $lines->values(); + $lastIndex = $lineList->count() - 1; + + foreach ($lineList as $index => $line) { + if ($index === $lastIndex) { + $allocations[(int) $line->id] = $remaining; + break; + } + + $lineSubtotal = (int) $line->line_subtotal_amount; + $lineDiscount = (int) round($totalDiscount * $lineSubtotal / $subtotal); + $allocations[(int) $line->id] = $lineDiscount; + $remaining -= $lineDiscount; + } + + return new DiscountResult(amount: $totalDiscount, allocations: $allocations); + } + + public function recordUsage(Discount $discount): void + { + DB::transaction(function () use ($discount): void { + $discount->refresh(); + $discount->usage_count = (int) $discount->usage_count + 1; + $discount->save(); + }); + } +} diff --git a/app/Services/FulfillmentService.php b/app/Services/FulfillmentService.php new file mode 100644 index 00000000..38842a13 --- /dev/null +++ b/app/Services/FulfillmentService.php @@ -0,0 +1,109 @@ + $lines Map of order_line_id => quantity. + * @param array|null $tracking Optional tracking info keyed by company/number/url. + */ + public function create(Order $order, array $lines, ?array $tracking = null): Fulfillment + { + $financialStatus = $order->financial_status instanceof FinancialStatus + ? $order->financial_status + : FinancialStatus::from((string) $order->financial_status); + + if (! in_array($financialStatus, [FinancialStatus::Paid, FinancialStatus::PartiallyRefunded], true)) { + throw new FulfillmentGuardException('Cannot fulfill an unpaid order.'); + } + + return DB::transaction(function () use ($order, $lines, $tracking): Fulfillment { + /** @var Fulfillment $fulfillment */ + $fulfillment = Fulfillment::create([ + 'order_id' => $order->id, + 'status' => FulfillmentShipmentStatus::Pending->value, + 'tracking_company' => $tracking['company'] ?? null, + 'tracking_number' => $tracking['number'] ?? null, + 'tracking_url' => $tracking['url'] ?? null, + ]); + + foreach ($lines as $orderLineId => $quantity) { + FulfillmentLine::create([ + 'fulfillment_id' => $fulfillment->id, + 'order_line_id' => $orderLineId, + 'quantity' => $quantity, + ]); + } + + $this->updateOrderFulfillmentStatus($order); + + return $fulfillment; + }); + } + + /** + * @param array|null $tracking + */ + public function markAsShipped(Fulfillment $fulfillment, ?array $tracking = null): void + { + $updates = [ + 'status' => FulfillmentShipmentStatus::Shipped->value, + 'shipped_at' => now(), + ]; + + if ($tracking !== null) { + $updates['tracking_company'] = $tracking['company'] ?? $fulfillment->tracking_company; + $updates['tracking_number'] = $tracking['number'] ?? $fulfillment->tracking_number; + $updates['tracking_url'] = $tracking['url'] ?? $fulfillment->tracking_url; + } + + $fulfillment->update($updates); + } + + public function markAsDelivered(Fulfillment $fulfillment): void + { + $fulfillment->update([ + 'status' => FulfillmentShipmentStatus::Delivered->value, + 'delivered_at' => now(), + ]); + + FulfillmentDelivered::dispatch($fulfillment); + } + + private function updateOrderFulfillmentStatus(Order $order): void + { + $order->loadMissing('lines'); + + $totalQty = (int) $order->lines->sum('quantity'); + $fulfilledQty = (int) FulfillmentLine::whereIn('order_line_id', $order->lines->pluck('id'))->sum('quantity'); + + if ($totalQty > 0 && $fulfilledQty >= $totalQty) { + $order->update([ + 'fulfillment_status' => FulfillmentStatus::Fulfilled->value, + 'status' => OrderStatus::Fulfilled->value, + ]); + OrderFulfilled::dispatch($order); + + return; + } + + if ($fulfilledQty > 0) { + $order->update([ + 'fulfillment_status' => FulfillmentStatus::Partial->value, + ]); + } + } +} diff --git a/app/Services/InventoryService.php b/app/Services/InventoryService.php new file mode 100644 index 00000000..b532b31a --- /dev/null +++ b/app/Services/InventoryService.php @@ -0,0 +1,67 @@ +quantityAvailable() >= $quantity; + } + + public function reserve(InventoryItem $item, int $quantity): void + { + DB::transaction(function () use ($item, $quantity): void { + $item->refresh(); + + $policy = $item->policy instanceof InventoryPolicy + ? $item->policy + : InventoryPolicy::from((string) $item->policy); + + if ($policy === InventoryPolicy::Deny && $item->quantityAvailable() < $quantity) { + throw new InsufficientInventoryException( + "Insufficient inventory for variant {$item->variant_id}." + ); + } + + $item->quantity_reserved = (int) $item->quantity_reserved + $quantity; + $item->save(); + }); + } + + public function release(InventoryItem $item, int $quantity): void + { + DB::transaction(function () use ($item, $quantity): void { + $item->refresh(); + + $item->quantity_reserved = max(0, (int) $item->quantity_reserved - $quantity); + $item->save(); + }); + } + + public function commit(InventoryItem $item, int $quantity): void + { + DB::transaction(function () use ($item, $quantity): void { + $item->refresh(); + + $item->quantity_on_hand = max(0, (int) $item->quantity_on_hand - $quantity); + $item->quantity_reserved = max(0, (int) $item->quantity_reserved - $quantity); + $item->save(); + }); + } + + public function restock(InventoryItem $item, int $quantity): void + { + DB::transaction(function () use ($item, $quantity): void { + $item->refresh(); + + $item->quantity_on_hand = (int) $item->quantity_on_hand + $quantity; + $item->save(); + }); + } +} diff --git a/app/Services/NavigationService.php b/app/Services/NavigationService.php new file mode 100644 index 00000000..95339a60 --- /dev/null +++ b/app/Services/NavigationService.php @@ -0,0 +1,45 @@ + + */ + public function buildTree(NavigationMenu $menu): array + { + return Cache::remember( + "nav:menu:{$menu->id}", + self::CACHE_TTL, + fn (): array => $menu->items() + ->get() + ->map(fn (NavigationItem $item): array => [ + 'id' => (int) $item->id, + 'type' => $item->type->value, + 'label' => (string) $item->label, + 'url' => $this->resolveUrl($item), + 'position' => (int) $item->position, + ]) + ->all(), + ); + } + + public function resolveUrl(NavigationItem $item): string + { + return $item->resolveUrl(); + } + + public function forgetMenu(NavigationMenu $menu): void + { + Cache::forget("nav:menu:{$menu->id}"); + } +} diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php new file mode 100644 index 00000000..d1438d20 --- /dev/null +++ b/app/Services/OrderService.php @@ -0,0 +1,174 @@ +cart()->with('lines.variant.product')->first(); + /** @var Store $store */ + $store = Store::withoutGlobalScopes()->findOrFail($checkout->store_id); + $totals = $checkout->totals_json ?? []; + + $currency = $cart?->currency ?? ($totals['currency'] ?? 'USD'); + + /** @var Order $order */ + $order = Order::withoutGlobalScopes()->create([ + 'store_id' => $store->id, + 'customer_id' => $checkout->customer_id, + 'order_number' => $this->generateOrderNumber($store), + 'payment_method' => $checkout->payment_method, + 'status' => OrderStatus::Pending->value, + 'financial_status' => FinancialStatus::Pending->value, + 'fulfillment_status' => FulfillmentStatus::Unfulfilled->value, + 'currency' => $currency, + 'subtotal_amount' => (int) ($totals['subtotal'] ?? 0), + 'discount_amount' => (int) ($totals['discount'] ?? 0), + 'shipping_amount' => (int) ($totals['shipping'] ?? 0), + 'tax_amount' => (int) ($totals['tax_total'] ?? 0), + 'total_amount' => (int) ($totals['total'] ?? 0), + 'email' => $checkout->email, + 'billing_address_json' => $checkout->billing_address_json, + 'shipping_address_json' => $checkout->shipping_address_json, + 'placed_at' => now(), + ]); + + if ($cart !== null) { + foreach ($cart->lines as $line) { + OrderLine::create([ + 'order_id' => $order->id, + 'product_id' => $line->variant?->product_id, + 'variant_id' => $line->variant_id, + 'title_snapshot' => $line->variant?->product?->title ?? 'Removed product', + 'sku_snapshot' => $line->variant?->sku, + 'quantity' => (int) $line->quantity, + 'unit_price_amount' => (int) $line->unit_price_amount, + 'total_amount' => (int) $line->line_total_amount, + 'tax_lines_json' => null, + 'discount_allocations_json' => null, + ]); + } + + $cart->update(['status' => 'converted']); + } + + $checkout->update(['status' => 'completed']); + + OrderCreated::dispatch($order); + + return $order; + }); + } + + public function generateOrderNumber(Store $store): string + { + $maxNumeric = (int) Order::withoutGlobalScopes() + ->where('store_id', $store->id) + ->where('order_number', 'like', '#%') + ->whereRaw('CAST(SUBSTR(order_number, 2) AS INTEGER) > 0') + ->max(DB::raw('CAST(SUBSTR(order_number, 2) AS INTEGER)')); + + $next = max(1000, $maxNumeric) + 1; + + return '#'.$next; + } + + public function cancel(Order $order, ?string $reason = null): void + { + $fulfillmentStatus = $order->fulfillment_status instanceof FulfillmentStatus + ? $order->fulfillment_status + : FulfillmentStatus::from((string) $order->fulfillment_status); + + if ($fulfillmentStatus === FulfillmentStatus::Fulfilled) { + throw new DomainException('Cannot cancel a fulfilled order.'); + } + + DB::transaction(function () use ($order): void { + $order->loadMissing('lines'); + + foreach ($order->lines as $line) { + if ($line->variant_id === null) { + continue; + } + + $item = InventoryItem::withoutGlobalScopes() + ->where('variant_id', $line->variant_id) + ->first(); + + if ($item !== null) { + $item->quantity_reserved = max(0, (int) $item->quantity_reserved - (int) $line->quantity); + $item->save(); + } + } + + $order->update(['status' => OrderStatus::Cancelled->value]); + + OrderCancelled::dispatch($order); + }); + } + + public function confirmBankTransferPayment(Order $order): void + { + $paymentMethod = $order->payment_method instanceof PaymentMethod + ? $order->payment_method + : PaymentMethod::from((string) $order->payment_method); + + $financialStatus = $order->financial_status instanceof FinancialStatus + ? $order->financial_status + : FinancialStatus::from((string) $order->financial_status); + + if ($paymentMethod !== PaymentMethod::BankTransfer || $financialStatus !== FinancialStatus::Pending) { + throw new DomainException('Order is not a pending bank transfer.'); + } + + DB::transaction(function () use ($order): void { + $order->update([ + 'financial_status' => FinancialStatus::Paid->value, + 'status' => OrderStatus::Paid->value, + ]); + + $payment = $order->payments()->latest('id')->first(); + if ($payment !== null) { + $payment->update(['status' => PaymentStatus::Captured->value]); + } + + $order->loadMissing('lines'); + + foreach ($order->lines as $line) { + if ($line->variant_id === null) { + continue; + } + + $item = InventoryItem::withoutGlobalScopes() + ->where('variant_id', $line->variant_id) + ->first(); + + if ($item !== null) { + $item->quantity_on_hand = max(0, (int) $item->quantity_on_hand - (int) $line->quantity); + $item->quantity_reserved = max(0, (int) $item->quantity_reserved - (int) $line->quantity); + $item->save(); + } + } + + OrderPaid::dispatch($order); + }); + } +} diff --git a/app/Services/Payments/MockPaymentProvider.php b/app/Services/Payments/MockPaymentProvider.php new file mode 100644 index 00000000..c1717174 --- /dev/null +++ b/app/Services/Payments/MockPaymentProvider.php @@ -0,0 +1,93 @@ + $details + */ + public function charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult + { + $totals = $checkout->totals_json ?? []; + $amount = (int) ($totals['total'] ?? 0); + $currency = (string) ($totals['currency'] ?? 'USD'); + + return match ($method) { + PaymentMethod::CreditCard => $this->chargeCreditCard($details, $amount, $currency), + PaymentMethod::Paypal => new PaymentResult( + status: PaymentStatus::Captured, + providerPaymentId: $this->generateId(), + amount: $amount, + currency: $currency, + ), + PaymentMethod::BankTransfer => new PaymentResult( + status: PaymentStatus::Pending, + providerPaymentId: $this->generateId(), + amount: $amount, + currency: $currency, + ), + }; + } + + public function refund(Payment $payment, int $amount): RefundResult + { + return new RefundResult( + status: RefundStatus::Processed, + providerRefundId: 'mock_ref_'.Str::random(12), + amount: $amount, + ); + } + + /** + * @param array $details + */ + private function chargeCreditCard(array $details, int $amount, string $currency): PaymentResult + { + $cardNumber = preg_replace('/\s+/', '', (string) ($details['card_number'] ?? self::CARD_SUCCESS)); + + return match ($cardNumber) { + self::CARD_DECLINED => new PaymentResult( + status: PaymentStatus::Failed, + providerPaymentId: null, + amount: $amount, + currency: $currency, + errorMessage: 'card_declined', + ), + self::CARD_INSUFFICIENT => new PaymentResult( + status: PaymentStatus::Failed, + providerPaymentId: null, + amount: $amount, + currency: $currency, + errorMessage: 'insufficient_funds', + ), + default => new PaymentResult( + status: PaymentStatus::Captured, + providerPaymentId: $this->generateId(), + amount: $amount, + currency: $currency, + ), + }; + } + + private function generateId(): string + { + return 'mock_'.Str::random(12); + } +} diff --git a/app/Services/PricingEngine.php b/app/Services/PricingEngine.php new file mode 100644 index 00000000..3b9921b2 --- /dev/null +++ b/app/Services/PricingEngine.php @@ -0,0 +1,101 @@ +cart()->with('lines.variant')->first(); + + if ($cart === null) { + return new PricingResult(0, 0, 0, [], 0, 0, 'USD'); + } + + $store = Store::withoutGlobalScopes()->find($checkout->store_id); + + $subtotal = (int) $cart->lines->sum('line_subtotal_amount'); + + $discount = 0; + $freeShipping = false; + + if ($checkout->discount_code !== null && $checkout->discount_code !== '' && $store !== null) { + $discountModel = $this->discountService->validate($checkout->discount_code, $store, $cart); + $result = $this->discountService->calculate($discountModel, $subtotal, $cart->lines); + $discount = $result->amount; + $freeShipping = $result->freeShipping; + } + + $discountedSubtotal = $subtotal - $discount; + + $shipping = 0; + if ($checkout->shipping_method_id !== null) { + $rate = ShippingRate::query()->find($checkout->shipping_method_id); + if ($rate !== null) { + $shipping = $this->shippingCalculator->calculate($rate, $cart); + } + } + + if ($freeShipping) { + $shipping = 0; + } + + $taxLines = []; + $taxTotal = 0; + $currency = (string) ($cart->currency ?? 'USD'); + + $taxSettings = $store !== null + ? TaxSettings::query()->where('store_id', $store->id)->first() + : null; + + if ($taxSettings !== null) { + if ((bool) $taxSettings->prices_include_tax) { + $rateBasisPoints = (int) ($taxSettings->config_json['rate_basis_points'] ?? 0); + $taxName = (string) ($taxSettings->config_json['name'] ?? 'Tax'); + $extractedBase = $discountedSubtotal + $shipping; + $extracted = $this->taxCalculator->extractInclusive($extractedBase, $rateBasisPoints); + $taxTotal = $extracted; + $taxLines = $extracted > 0 + ? [new TaxLine($taxName, $rateBasisPoints, $extracted)] + : []; + $total = $discountedSubtotal + $shipping; + } else { + $taxBase = $discountedSubtotal + $shipping; + $result = $this->taxCalculator->calculate( + $taxBase, + $taxSettings, + $checkout->shipping_address_json ?? [] + ); + $taxTotal = (int) $result['tax_total']; + $taxLines = $result['tax_lines']; + $total = $discountedSubtotal + $shipping + $taxTotal; + } + } else { + $total = $discountedSubtotal + $shipping; + } + + return new PricingResult( + subtotal: $subtotal, + discount: $discount, + shipping: $shipping, + taxLines: $taxLines, + taxTotal: $taxTotal, + total: $total, + currency: $currency, + freeShippingApplied: $freeShipping, + ); + } +} diff --git a/app/Services/ProductService.php b/app/Services/ProductService.php new file mode 100644 index 00000000..6fc7cab9 --- /dev/null +++ b/app/Services/ProductService.php @@ -0,0 +1,198 @@ + $data + */ + public function create(Store $store, array $data): Product + { + return DB::transaction(function () use ($store, $data): Product { + $title = (string) $data['title']; + $handle = isset($data['handle']) && $data['handle'] !== '' + ? HandleGenerator::generate((string) $data['handle'], 'products', $store->id) + : HandleGenerator::generate($title, 'products', $store->id); + + $product = new Product; + $product->store_id = $store->id; + $product->title = $title; + $product->handle = $handle; + $product->status = $data['status'] ?? ProductStatus::Draft->value; + $product->description_html = $data['description_html'] ?? null; + $product->vendor = $data['vendor'] ?? null; + $product->product_type = $data['product_type'] ?? null; + $product->tags = $data['tags'] ?? []; + $product->published_at = $data['published_at'] ?? null; + $product->save(); + + $options = $data['options'] ?? []; + $this->syncOptions($product, $options); + + if (! empty($options)) { + $this->variantMatrixService->rebuildMatrix($product->fresh(['options.values'])); + } else { + $variantsData = $data['variants'] ?? []; + + if ($variantsData === []) { + $product->variants()->create([ + 'price_amount' => 0, + 'currency' => $store->default_currency ?? 'USD', + 'is_default' => true, + 'position' => 0, + 'status' => 'active', + ]); + } else { + foreach ($variantsData as $index => $variantData) { + $product->variants()->create(array_merge([ + 'price_amount' => 0, + 'currency' => $store->default_currency ?? 'USD', + 'is_default' => $index === 0, + 'position' => $index, + 'status' => 'active', + ], $variantData)); + } + } + } + + return $product->fresh(['variants', 'options.values']); + }); + } + + /** + * @param array $data + */ + public function update(Product $product, array $data): Product + { + return DB::transaction(function () use ($product, $data): Product { + if (isset($data['title'])) { + $product->title = (string) $data['title']; + } + + if (isset($data['handle']) && $data['handle'] !== $product->handle) { + $product->handle = HandleGenerator::generate( + (string) $data['handle'], + 'products', + (int) $product->store_id, + $product->id + ); + } + + foreach (['description_html', 'vendor', 'product_type', 'tags', 'published_at'] as $field) { + if (array_key_exists($field, $data)) { + $product->{$field} = $data[$field]; + } + } + + $product->save(); + + if (array_key_exists('options', $data)) { + $this->syncOptions($product, $data['options']); + $this->variantMatrixService->rebuildMatrix($product->fresh(['options.values'])); + } + + return $product->fresh(['variants', 'options.values']); + }); + } + + public function transitionStatus(Product $product, ProductStatus $newStatus): void + { + $current = $product->status instanceof ProductStatus + ? $product->status + : ProductStatus::from((string) $product->status); + + $allowed = match ($current) { + ProductStatus::Draft => [ProductStatus::Active], + ProductStatus::Active => [ProductStatus::Archived], + ProductStatus::Archived => [ProductStatus::Active], + }; + + if (! in_array($newStatus, $allowed, true)) { + throw new InvalidArgumentException( + "Cannot transition product from {$current->value} to {$newStatus->value}" + ); + } + + $product->status = $newStatus; + $product->save(); + } + + public function delete(Product $product): void + { + $status = $product->status instanceof ProductStatus + ? $product->status + : ProductStatus::from((string) $product->status); + + if ($status !== ProductStatus::Draft) { + throw new InvalidArgumentException('Only draft products can be deleted.'); + } + + // Phase 5: also block deletion when order_lines reference this product. + $product->delete(); + } + + /** + * @param array> $options + */ + private function syncOptions(Product $product, array $options): void + { + $existingOptions = $product->options()->with('values')->get()->keyBy('name'); + + $product->options()->update(['position' => DB::raw('position + 1000')]); + + foreach ($existingOptions as $option) { + $option->values()->update(['position' => DB::raw('position + 1000')]); + } + + $keptOptionIds = []; + + foreach ($options as $optionIndex => $optionData) { + $name = (string) $optionData['name']; + $position = $optionData['position'] ?? $optionIndex; + + if ($existingOptions->has($name)) { + $option = $existingOptions->get($name); + $option->position = $position; + $option->save(); + } else { + $option = $product->options()->create([ + 'name' => $name, + 'position' => $position, + ]); + } + + $keptOptionIds[] = $option->id; + $existingValues = $option->values()->get()->keyBy('value'); + $keptValueIds = []; + + foreach ($optionData['values'] ?? [] as $valueIndex => $value) { + if ($existingValues->has($value)) { + $optionValue = $existingValues->get($value); + $optionValue->position = $valueIndex; + $optionValue->save(); + } else { + $optionValue = $option->values()->create([ + 'value' => $value, + 'position' => $valueIndex, + ]); + } + + $keptValueIds[] = $optionValue->id; + } + + $option->values()->whereNotIn('id', $keptValueIds)->delete(); + } + + $product->options()->whereNotIn('id', $keptOptionIds)->delete(); + } +} diff --git a/app/Services/RefundService.php b/app/Services/RefundService.php new file mode 100644 index 00000000..9d1d43ab --- /dev/null +++ b/app/Services/RefundService.php @@ -0,0 +1,78 @@ +refundedTotal(); + $maxRefundable = (int) $payment->amount - $alreadyRefunded; + + if ($amount <= 0 || $amount > $maxRefundable) { + throw new InvalidArgumentException('Invalid refund amount.'); + } + + return DB::transaction(function () use ($order, $payment, $amount, $reason, $restock): Refund { + $result = $this->provider->refund($payment, $amount); + + /** @var Refund $refund */ + $refund = Refund::create([ + 'order_id' => $order->id, + 'payment_id' => $payment->id, + 'amount' => $amount, + 'reason' => $reason, + 'status' => $result->status->value, + 'provider_refund_id' => $result->providerRefundId, + ]); + + if ($result->status === RefundStatus::Processed) { + $order->refresh(); + $totalRefunded = $order->refundedTotal(); + + $newFinancialStatus = $totalRefunded >= (int) $order->total_amount + ? FinancialStatus::Refunded + : FinancialStatus::PartiallyRefunded; + + $order->update(['financial_status' => $newFinancialStatus->value]); + + if ($restock) { + $order->loadMissing('lines'); + foreach ($order->lines as $line) { + if ($line->variant_id === null) { + continue; + } + + $item = InventoryItem::withoutGlobalScopes() + ->where('variant_id', $line->variant_id) + ->first(); + + if ($item !== null) { + $this->inventoryService->restock($item, (int) $line->quantity); + } + } + } + + OrderRefunded::dispatch($order->fresh() ?? $order, $refund); + } + + return $refund; + }); + } +} diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php new file mode 100644 index 00000000..5a827589 --- /dev/null +++ b/app/Services/SearchService.php @@ -0,0 +1,113 @@ +removeProduct($product->id); + + $tags = $product->tags; + $tagString = is_array($tags) ? implode(' ', $tags) : (string) ($tags ?? ''); + + DB::statement( + 'INSERT INTO products_fts (rowid, title, description, vendor, product_type, tags) VALUES (?, ?, ?, ?, ?, ?)', + [ + $product->id, + (string) ($product->title ?? ''), + strip_tags((string) ($product->description_html ?? '')), + (string) ($product->vendor ?? ''), + (string) ($product->product_type ?? ''), + $tagString, + ] + ); + } + + public function removeProduct(int $productId): void + { + DB::statement('DELETE FROM products_fts WHERE rowid = ?', [$productId]); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function search(Store $store, string $query, array $filters = [], int $perPage = 12): LengthAwarePaginator + { + $ftsQuery = $this->buildFtsQuery($query); + + if ($ftsQuery === null) { + return Product::query()->whereRaw('1 = 0')->paginate($perPage); + } + + $ids = DB::table('products_fts') + ->whereRaw('products_fts MATCH ?', [$ftsQuery]) + ->pluck('rowid') + ->all(); + + if ($ids === []) { + return Product::query()->whereRaw('1 = 0')->paginate($perPage); + } + + return Product::query() + ->whereIn('id', $ids) + ->where('store_id', $store->id) + ->where('status', ProductStatus::Active->value) + ->orderBy('title') + ->paginate($perPage); + } + + /** + * @return Collection + */ + public function autocomplete(Store $store, string $prefix, int $limit = 5): Collection + { + if (mb_strlen(trim($prefix)) < 2) { + return collect(); + } + + $ftsQuery = $this->buildFtsQuery($prefix); + + if ($ftsQuery === null) { + return collect(); + } + + $ids = DB::table('products_fts') + ->whereRaw('products_fts MATCH ?', [$ftsQuery]) + ->limit($limit * 3) + ->pluck('rowid') + ->all(); + + if ($ids === []) { + return collect(); + } + + return Product::query() + ->whereIn('id', $ids) + ->where('store_id', $store->id) + ->where('status', ProductStatus::Active->value) + ->limit($limit) + ->get(); + } + + private function buildFtsQuery(string $query): ?string + { + $sanitized = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $query) ?? ''; + $tokens = preg_split('/\s+/', trim($sanitized)) ?: []; + $tokens = array_values(array_filter($tokens, fn (string $token): bool => $token !== '')); + + if ($tokens === []) { + return null; + } + + return implode(' ', array_map(fn (string $token): string => $token.'*', $tokens)); + } +} diff --git a/app/Services/ShippingCalculator.php b/app/Services/ShippingCalculator.php new file mode 100644 index 00000000..cc474005 --- /dev/null +++ b/app/Services/ShippingCalculator.php @@ -0,0 +1,121 @@ + $address + * @return Collection + */ + public function getAvailableRates(Store $store, array $address): Collection + { + $country = (string) ($address['country'] ?? ''); + $provinceCode = $address['province_code'] ?? null; + $regionKey = $provinceCode !== null && $provinceCode !== '' + ? $country.'-'.$provinceCode + : null; + + $zones = ShippingZone::query() + ->where('store_id', $store->id) + ->get() + ->filter(function (ShippingZone $zone) use ($country, $regionKey): bool { + $countries = $zone->countries_json ?? []; + $regions = $zone->regions_json ?? []; + + if ($country !== '' && in_array($country, $countries, true)) { + return true; + } + + return $regionKey !== null && in_array($regionKey, $regions, true); + }); + + if ($zones->isEmpty()) { + return new Collection; + } + + return ShippingRate::query() + ->whereIn('zone_id', $zones->pluck('id')->all()) + ->where('is_active', true) + ->get(); + } + + public function calculate(ShippingRate $rate, Cart $cart): int + { + $lines = $cart->relationLoaded('lines') ? $cart->lines : $cart->lines()->with('variant')->get(); + + $requiresShipping = $lines->contains(function ($line): bool { + $variant = $line->variant; + + return $variant !== null && (bool) $variant->requires_shipping; + }); + + if (! $requiresShipping) { + return 0; + } + + $config = $rate->config_json ?? []; + + return match ($rate->type) { + ShippingRateType::Flat => (int) ($config['amount'] ?? 0), + ShippingRateType::Weight => $this->weightRate($config, $lines), + ShippingRateType::Price => $this->priceRate($config, $lines), + ShippingRateType::Carrier => (int) ($config['fallback_amount'] ?? 0), + }; + } + + /** + * @param array $config + * @param iterable $lines + */ + private function weightRate(array $config, iterable $lines): int + { + $totalWeight = 0; + foreach ($lines as $line) { + $variant = $line->variant; + if ($variant === null || ! $variant->requires_shipping) { + continue; + } + $totalWeight += (int) ($variant->weight_g ?? 0) * (int) $line->quantity; + } + + foreach ($config['ranges'] ?? [] as $range) { + $min = (int) ($range['min_g'] ?? 0); + $max = (int) ($range['max_g'] ?? PHP_INT_MAX); + if ($totalWeight >= $min && $totalWeight <= $max) { + return (int) ($range['amount'] ?? 0); + } + } + + return 0; + } + + /** + * @param array $config + * @param iterable $lines + */ + private function priceRate(array $config, iterable $lines): int + { + $subtotal = 0; + foreach ($lines as $line) { + $subtotal += (int) $line->line_subtotal_amount; + } + + foreach ($config['ranges'] ?? [] as $range) { + $min = (int) ($range['min_amount'] ?? 0); + $max = (int) ($range['max_amount'] ?? PHP_INT_MAX); + if ($subtotal >= $min && $subtotal <= $max) { + return (int) ($range['amount'] ?? 0); + } + } + + return 0; + } +} diff --git a/app/Services/TaxCalculator.php b/app/Services/TaxCalculator.php new file mode 100644 index 00000000..b376426a --- /dev/null +++ b/app/Services/TaxCalculator.php @@ -0,0 +1,51 @@ + $address + * @return array{tax_total: int, tax_lines: array} + */ + public function calculate(int $amount, TaxSettings $settings, array $address): array + { + $config = $settings->config_json ?? []; + $rate = (int) ($config['rate_basis_points'] ?? 0); + $name = (string) ($config['name'] ?? 'Tax'); + + if ($rate === 0 || $amount === 0) { + return ['tax_total' => 0, 'tax_lines' => []]; + } + + $taxAmount = $this->addExclusive($amount, $rate); + + return [ + 'tax_total' => $taxAmount, + 'tax_lines' => [new TaxLine($name, $rate, $taxAmount)], + ]; + } +} diff --git a/app/Services/ThemeSettingsService.php b/app/Services/ThemeSettingsService.php new file mode 100644 index 00000000..49ab4879 --- /dev/null +++ b/app/Services/ThemeSettingsService.php @@ -0,0 +1,58 @@ + + */ + public function forStore(Store $store): array + { + return Cache::remember( + "theme:settings:store:{$store->id}", + self::CACHE_TTL, + function () use ($store): array { + $theme = Theme::withoutGlobalScopes() + ->where('store_id', $store->id) + ->where('status', ThemeStatus::Published->value) + ->with('settings') + ->latest('published_at') + ->first(); + + if ($theme === null || $theme->settings === null) { + return $this->defaultSettings(); + } + + return array_merge($this->defaultSettings(), (array) $theme->settings->settings_json); + }, + ); + } + + /** + * @return array + */ + public function defaultSettings(): array + { + return [ + 'colors' => ['primary' => '#111'], + 'announcement' => null, + 'footer_text' => '(c) Shop', + ]; + } + + public function forgetStore(Store $store): void + { + Cache::forget("theme:settings:store:{$store->id}"); + } +} diff --git a/app/Services/VariantMatrixService.php b/app/Services/VariantMatrixService.php new file mode 100644 index 00000000..fb145f6b --- /dev/null +++ b/app/Services/VariantMatrixService.php @@ -0,0 +1,106 @@ +load('options.values', 'variants.optionValues'); + + $options = $product->options; + + if ($options->isEmpty()) { + return; + } + + $combinations = $this->cartesian( + $options->map(fn ($option) => $option->values->all())->all() + ); + + $currency = optional($product->store)->default_currency ?? 'USD'; + $existingByKey = []; + + foreach ($product->variants as $variant) { + $key = $this->keyFor($variant->optionValues->pluck('id')->all()); + $existingByKey[$key] = $variant; + } + + $seenKeys = []; + + foreach ($combinations as $index => $combination) { + $valueIds = array_map(fn ($value) => $value->id, $combination); + $key = $this->keyFor($valueIds); + $seenKeys[] = $key; + + if (isset($existingByKey[$key])) { + $variant = $existingByKey[$key]; + + if ($variant->status !== VariantStatus::Active) { + $variant->status = VariantStatus::Active; + $variant->save(); + } + + continue; + } + + $variant = $product->variants()->create([ + 'price_amount' => 0, + 'currency' => $currency, + 'is_default' => $index === 0 && ! $product->variants()->where('is_default', true)->exists(), + 'position' => $index, + 'status' => VariantStatus::Active->value, + ]); + + $variant->optionValues()->sync($valueIds); + } + + $seenKeysMap = array_flip($seenKeys); + + foreach ($existingByKey as $key => $variant) { + if (! array_key_exists((string) $key, $seenKeysMap)) { + $variant->status = VariantStatus::Archived; + $variant->save(); + } + } + }); + } + + /** + * @param array> $groups + * @return array> + */ + private function cartesian(array $groups): array + { + $result = [[]]; + + foreach ($groups as $group) { + $next = []; + + foreach ($result as $acc) { + foreach ($group as $item) { + $next[] = array_merge($acc, [$item]); + } + } + + $result = $next; + } + + return $result; + } + + /** + * @param array $valueIds + */ + private function keyFor(array $valueIds): string + { + sort($valueIds); + + return implode('-', $valueIds); + } +} diff --git a/app/Services/WebhookService.php b/app/Services/WebhookService.php new file mode 100644 index 00000000..3a806eef --- /dev/null +++ b/app/Services/WebhookService.php @@ -0,0 +1,36 @@ + $payload + */ + public function dispatch(Store $store, string $eventType, array $payload): void + { + $subscriptions = WebhookSubscription::query() + ->where('store_id', $store->id) + ->where('event_type', $eventType) + ->where('status', 'active') + ->get(); + + foreach ($subscriptions as $subscription) { + DeliverWebhook::dispatch($subscription, $eventType, $payload); + } + } + + public function sign(string $payload, string $secret): string + { + return 'sha256='.hash_hmac('sha256', $payload, $secret); + } + + public function verify(string $payload, string $signature, string $secret): bool + { + return hash_equals($this->sign($payload, $secret), $signature); + } +} diff --git a/app/Support/CartSession.php b/app/Support/CartSession.php new file mode 100644 index 00000000..878669ad --- /dev/null +++ b/app/Support/CartSession.php @@ -0,0 +1,49 @@ +find($cartId); + if ($cart !== null && $cart->status === CartStatus::Active) { + return $cart; + } + } + + $customer = Auth::guard('customer')->user(); + $cart = app(CartService::class)->create($store, $customer, session()->getId()); + session(['cart_id' => $cart->id]); + + return $cart; + } + + public static function current(): ?Cart + { + $id = session('cart_id'); + if ($id === null) { + return null; + } + + $cart = Cart::withoutGlobalScopes()->find($id); + if ($cart === null || $cart->status !== CartStatus::Active) { + return null; + } + + return $cart; + } + + public static function clear(): void + { + session()->forget('cart_id'); + } +} diff --git a/app/Support/HandleGenerator.php b/app/Support/HandleGenerator.php new file mode 100644 index 00000000..cbc3c8e0 --- /dev/null +++ b/app/Support/HandleGenerator.php @@ -0,0 +1,41 @@ +where('store_id', $storeId) + ->where('handle', $handle); + + if ($excludeId !== null) { + $query->where('id', '!=', $excludeId); + } + + return $query->exists(); + } +} diff --git a/app/ValueObjects/DiscountResult.php b/app/ValueObjects/DiscountResult.php new file mode 100644 index 00000000..4062a112 --- /dev/null +++ b/app/ValueObjects/DiscountResult.php @@ -0,0 +1,15 @@ + $allocations + */ + public function __construct( + public int $amount, + public array $allocations, + public bool $freeShipping = false, + ) {} +} diff --git a/app/ValueObjects/PaymentResult.php b/app/ValueObjects/PaymentResult.php new file mode 100644 index 00000000..600d1a56 --- /dev/null +++ b/app/ValueObjects/PaymentResult.php @@ -0,0 +1,31 @@ +status === PaymentStatus::Captured; + } + + public function pending(): bool + { + return $this->status === PaymentStatus::Pending; + } + + public function failed(): bool + { + return $this->status === PaymentStatus::Failed; + } +} diff --git a/app/ValueObjects/PricingResult.php b/app/ValueObjects/PricingResult.php new file mode 100644 index 00000000..d76cff67 --- /dev/null +++ b/app/ValueObjects/PricingResult.php @@ -0,0 +1,46 @@ + $taxLines + */ + public function __construct( + public int $subtotal, + public int $discount, + public int $shipping, + public array $taxLines, + public int $taxTotal, + public int $total, + public string $currency, + public bool $freeShippingApplied = false, + ) {} + + /** + * @return array{ + * subtotal: int, + * discount: int, + * shipping: int, + * tax_lines: array, + * tax_total: int, + * total: int, + * currency: string, + * free_shipping_applied: bool + * } + */ + public function toArray(): array + { + return [ + 'subtotal' => $this->subtotal, + 'discount' => $this->discount, + 'shipping' => $this->shipping, + 'tax_lines' => array_map(fn (TaxLine $line): array => $line->toArray(), $this->taxLines), + 'tax_total' => $this->taxTotal, + 'total' => $this->total, + 'currency' => $this->currency, + 'free_shipping_applied' => $this->freeShippingApplied, + ]; + } +} diff --git a/app/ValueObjects/RefundResult.php b/app/ValueObjects/RefundResult.php new file mode 100644 index 00000000..77c96933 --- /dev/null +++ b/app/ValueObjects/RefundResult.php @@ -0,0 +1,20 @@ +status === RefundStatus::Processed; + } +} diff --git a/app/ValueObjects/TaxLine.php b/app/ValueObjects/TaxLine.php new file mode 100644 index 00000000..863420c4 --- /dev/null +++ b/app/ValueObjects/TaxLine.php @@ -0,0 +1,24 @@ + $this->name, + 'rate' => $this->rate, + 'amount' => $this->amount, + ]; + } +} diff --git a/boost.json b/boost.json new file mode 100644 index 00000000..29f40ab2 --- /dev/null +++ b/boost.json @@ -0,0 +1,17 @@ +{ + "agents": [ + "claude_code" + ], + "guidelines": true, + "mcp": true, + "nightwatch_mcp": false, + "sail": false, + "skills": [ + "developing-with-fortify", + "laravel-best-practices", + "fluxui-development", + "livewire-development", + "pest-testing", + "tailwindcss-development" + ] +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c1832766..77b29108 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,11 +7,26 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->alias([ + 'store.resolve' => \App\Http\Middleware\ResolveStore::class, + ]); + + $middleware->redirectGuestsTo(function (\Illuminate\Http\Request $request): ?string { + if ($request->is('account*') || $request->routeIs('storefront.account.*')) { + return route('storefront.account.login'); + } + + if ($request->is('admin') || $request->is('admin/*')) { + return route('admin.login'); + } + + return null; + }); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/composer.json b/composer.json index 1f848aaf..034435d3 100644 --- a/composer.json +++ b/composer.json @@ -12,20 +12,22 @@ "php": "^8.2", "laravel/fortify": "^1.30", "laravel/framework": "^12.0", + "laravel/sanctum": "^4.3", "laravel/tinker": "^2.10.1", "livewire/flux": "^2.9.0", "livewire/livewire": "^4.0" }, "require-dev": { "fakerphp/faker": "^1.23", - "laravel/boost": "^1.0", + "laravel/boost": "^2.4", "laravel/pail": "^1.2.2", "laravel/pint": "^1.24", "laravel/sail": "^1.41", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", "pestphp/pest": "^4.3", - "pestphp/pest-plugin-laravel": "^4.0" + "pestphp/pest-plugin-laravel": "^4.0", + "phpmetrics/phpmetrics": "^2.9" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index e4255dbd..bbad7d4f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e4aa7ad38dac6834e5ff6bf65b1cdf23", + "content-hash": "d43dc6a9f550c4bc0ac2a7035a050de8", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1501,6 +1501,69 @@ }, "time": "2026-02-06T12:17:10+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-02-07T17:19:31+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.9", @@ -6877,35 +6940,36 @@ }, { "name": "laravel/boost", - "version": "v1.0.18", + "version": "v2.4.3", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "df2a62b5864759ea8cce8a4b7575b657e9c7d4ab" + "reference": "841d52905728cfac9f93c778a1758e740ce9a367" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/df2a62b5864759ea8cce8a4b7575b657e9c7d4ab", - "reference": "df2a62b5864759ea8cce8a4b7575b657e9c7d4ab", + "url": "https://api.github.com/repos/laravel/boost/zipball/841d52905728cfac9f93c778a1758e740ce9a367", + "reference": "841d52905728cfac9f93c778a1758e740ce9a367", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.9", - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "laravel/mcp": "^0.1.0", - "laravel/prompts": "^0.1.9|^0.3", - "laravel/roster": "^0.2", - "php": "^8.1|^8.2" + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "laravel/mcp": "^0.5.1|^0.6.0", + "laravel/prompts": "^0.3.10", + "laravel/roster": "^0.5.0", + "php": "^8.2" }, "require-dev": { - "laravel/pint": "^1.14|^1.23", - "mockery/mockery": "^1.6", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.0", - "phpstan/phpstan": "^2.0" + "laravel/pint": "^1.27.0", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" }, "type": "library", "extra": { @@ -6927,7 +6991,7 @@ "license": [ "MIT" ], - "description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.", + "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", "homepage": "https://github.com/laravel/boost", "keywords": [ "ai", @@ -6938,35 +7002,41 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-08-16T09:10:03+00:00" + "time": "2026-04-10T15:59:10+00:00" }, { "name": "laravel/mcp", - "version": "v0.1.1", + "version": "v0.6.5", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713" + "reference": "583a6282bf0f074d754f7ff5cd1fff9d34244691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/6d6284a491f07c74d34f48dfd999ed52c567c713", - "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713", + "url": "https://api.github.com/repos/laravel/mcp/zipball/583a6282bf0f074d754f7ff5cd1fff9d34244691", + "reference": "583a6282bf0f074d754f7ff5cd1fff9d34244691", "shasum": "" }, "require": { - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/http": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "illuminate/validation": "^10.0|^11.0|^12.0", - "php": "^8.1|^8.2" + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2" }, "require-dev": { - "laravel/pint": "^1.14", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "phpstan/phpstan": "^2.0" + "laravel/pint": "^1.20", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.2.4" }, "type": "library", "extra": { @@ -6982,8 +7052,6 @@ "autoload": { "psr-4": { "Laravel\\Mcp\\": "src/", - "Workbench\\App\\": "workbench/app/", - "Laravel\\Mcp\\Tests\\": "tests/", "Laravel\\Mcp\\Server\\": "src/Server/" } }, @@ -6991,10 +7059,15 @@ "license": [ "MIT" ], - "description": "The easiest way to add MCP servers to your Laravel app.", + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", "homepage": "https://github.com/laravel/mcp", "keywords": [ - "dev", "laravel", "mcp" ], @@ -7002,7 +7075,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-08-16T09:50:43+00:00" + "time": "2026-03-30T19:17:10+00:00" }, { "name": "laravel/pail", @@ -7153,30 +7226,31 @@ }, { "name": "laravel/roster", - "version": "v0.2.2", + "version": "v0.5.1", "source": { "type": "git", "url": "https://github.com/laravel/roster.git", - "reference": "67a39bce557a6cb7e7205a2a9d6c464f0e72956f" + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/roster/zipball/67a39bce557a6cb7e7205a2a9d6c464f0e72956f", - "reference": "67a39bce557a6cb7e7205a2a9d6c464f0e72956f", + "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb", "shasum": "" }, "require": { - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "php": "^8.1|^8.2" + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/yaml": "^7.2|^8.0" }, "require-dev": { "laravel/pint": "^1.14", "mockery/mockery": "^1.6", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.1", "phpstan/phpstan": "^2.0" }, "type": "library", @@ -7209,7 +7283,7 @@ "issues": "https://github.com/laravel/roster/issues", "source": "https://github.com/laravel/roster" }, - "time": "2025-07-24T12:31:13+00:00" + "time": "2026-03-05T07:58:43+00:00" }, { "name": "laravel/sail", @@ -8271,6 +8345,76 @@ }, "time": "2025-11-21T15:09:14+00:00" }, + { + "name": "phpmetrics/phpmetrics", + "version": "v2.9.1", + "source": { + "type": "git", + "url": "https://github.com/phpmetrics/PhpMetrics.git", + "reference": "e2e68ddd1543bc3f44402c383f7bccb62de1ece3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmetrics/PhpMetrics/zipball/e2e68ddd1543bc3f44402c383f7bccb62de1ece3", + "reference": "e2e68ddd1543bc3f44402c383f7bccb62de1ece3", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^3|^4|^5" + }, + "replace": { + "halleck45/php-metrics": "*", + "halleck45/phpmetrics": "*" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "bin": [ + "bin/phpmetrics" + ], + "type": "library", + "autoload": { + "files": [ + "./src/functions.php" + ], + "psr-0": { + "Hal\\": "./src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Lépine", + "email": "lepinejeanfrancois@yahoo.fr", + "homepage": "http://www.lepine.pro", + "role": "Copyright Holder" + } + ], + "description": "Static analyzer tool for PHP : Coupling, Cyclomatic complexity, Maintainability Index, Halstead's metrics... and more !", + "homepage": "http://www.phpmetrics.org", + "keywords": [ + "analysis", + "qa", + "quality", + "testing" + ], + "support": { + "issues": "https://github.com/PhpMetrics/PhpMetrics/issues", + "source": "https://github.com/phpmetrics/PhpMetrics/tree/v2.9.1" + }, + "funding": [ + { + "url": "https://github.com/Halleck45", + "type": "github" + } + ], + "time": "2025-09-25T05:21:02+00:00" + }, { "name": "phpstan/phpdoc-parser", "version": "2.3.2", diff --git a/config/auth.php b/config/auth.php index 7d1eb0de..c7e1a387 100644 --- a/config/auth.php +++ b/config/auth.php @@ -40,6 +40,11 @@ 'driver' => 'session', 'provider' => 'users', ], + + 'customer' => [ + 'driver' => 'session', + 'provider' => 'customers', + ], ], /* @@ -65,10 +70,10 @@ 'model' => env('AUTH_MODEL', App\Models\User::class), ], - // 'users' => [ - // 'driver' => 'database', - // 'table' => 'users', - // ], + 'customers' => [ + 'driver' => 'customer', + 'model' => App\Models\Customer::class, + ], ], /* @@ -97,6 +102,13 @@ 'expire' => 60, 'throttle' => 60, ], + + 'customers' => [ + 'provider' => 'customers', + 'table' => 'customer_password_reset_tokens', + 'expire' => 60, + 'throttle' => 60, + ], ], /* diff --git a/config/fortify.php b/config/fortify.php index ce67e2c3..555d34fb 100644 --- a/config/fortify.php +++ b/config/fortify.php @@ -73,7 +73,7 @@ | */ - 'home' => '/dashboard', + 'home' => '/admin', /* |-------------------------------------------------------------------------- diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 00000000..44527d68 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/database/factories/AnalyticsDailyFactory.php b/database/factories/AnalyticsDailyFactory.php new file mode 100644 index 00000000..fbe3b9e6 --- /dev/null +++ b/database/factories/AnalyticsDailyFactory.php @@ -0,0 +1,33 @@ + + */ +class AnalyticsDailyFactory extends Factory +{ + protected $model = AnalyticsDaily::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'date' => now()->toDateString(), + 'orders_count' => 0, + 'revenue_amount' => 0, + 'aov_amount' => 0, + 'visits_count' => 0, + 'add_to_cart_count' => 0, + 'checkout_started_count' => 0, + 'checkout_completed_count' => 0, + ]; + } +} diff --git a/database/factories/AnalyticsEventFactory.php b/database/factories/AnalyticsEventFactory.php new file mode 100644 index 00000000..4ce5095a --- /dev/null +++ b/database/factories/AnalyticsEventFactory.php @@ -0,0 +1,31 @@ + + */ +class AnalyticsEventFactory extends Factory +{ + protected $model = AnalyticsEvent::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'type' => fake()->randomElement(['page_view', 'add_to_cart', 'checkout_started', 'checkout_completed']), + 'session_id' => fake()->uuid(), + 'customer_id' => null, + 'properties_json' => [], + 'client_event_id' => null, + 'occurred_at' => now(), + ]; + } +} diff --git a/database/factories/AppFactory.php b/database/factories/AppFactory.php new file mode 100644 index 00000000..4de82a83 --- /dev/null +++ b/database/factories/AppFactory.php @@ -0,0 +1,31 @@ + + */ +class AppFactory extends Factory +{ + protected $model = App::class; + + /** + * @return array + */ + public function definition(): array + { + $name = fake()->unique()->company(); + + return [ + 'name' => $name, + 'slug' => Str::slug($name).'-'.fake()->unique()->randomNumber(5), + 'description' => fake()->sentence(), + 'scopes_json' => ['read_orders', 'write_orders'], + 'type' => fake()->randomElement(['first_party', 'third_party']), + ]; + } +} diff --git a/database/factories/AppInstallationFactory.php b/database/factories/AppInstallationFactory.php new file mode 100644 index 00000000..062f4ca0 --- /dev/null +++ b/database/factories/AppInstallationFactory.php @@ -0,0 +1,30 @@ + + */ +class AppInstallationFactory extends Factory +{ + protected $model = AppInstallation::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'app_id' => App::factory(), + 'status' => 'active', + 'settings_json' => [], + 'installed_at' => now(), + ]; + } +} diff --git a/database/factories/CartFactory.php b/database/factories/CartFactory.php new file mode 100644 index 00000000..50976ef8 --- /dev/null +++ b/database/factories/CartFactory.php @@ -0,0 +1,45 @@ + + */ +class CartFactory extends Factory +{ + protected $model = Cart::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'customer_id' => null, + 'session_id' => fake()->uuid(), + 'currency' => 'EUR', + 'cart_version' => 1, + 'status' => CartStatus::Active->value, + ]; + } + + public function converted(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => CartStatus::Converted->value, + ]); + } + + public function abandoned(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => CartStatus::Abandoned->value, + ]); + } +} diff --git a/database/factories/CartLineFactory.php b/database/factories/CartLineFactory.php new file mode 100644 index 00000000..0be17a93 --- /dev/null +++ b/database/factories/CartLineFactory.php @@ -0,0 +1,36 @@ + + */ +class CartLineFactory extends Factory +{ + protected $model = CartLine::class; + + /** + * @return array + */ + public function definition(): array + { + $unitPrice = 2499; + $quantity = 1; + $subtotal = $unitPrice * $quantity; + + return [ + 'cart_id' => Cart::factory(), + 'variant_id' => ProductVariant::factory(), + 'quantity' => $quantity, + 'unit_price_amount' => $unitPrice, + 'line_subtotal_amount' => $subtotal, + 'line_discount_amount' => 0, + 'line_total_amount' => $subtotal, + ]; + } +} diff --git a/database/factories/CheckoutFactory.php b/database/factories/CheckoutFactory.php new file mode 100644 index 00000000..8a061337 --- /dev/null +++ b/database/factories/CheckoutFactory.php @@ -0,0 +1,39 @@ + + */ +class CheckoutFactory extends Factory +{ + protected $model = Checkout::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'cart_id' => Cart::factory(), + 'customer_id' => null, + 'status' => CheckoutStatus::Started->value, + 'payment_method' => null, + 'email' => null, + 'shipping_address_json' => null, + 'billing_address_json' => null, + 'shipping_method_id' => null, + 'discount_code' => null, + 'tax_provider_snapshot_json' => null, + 'totals_json' => null, + 'expires_at' => null, + ]; + } +} diff --git a/database/factories/CollectionFactory.php b/database/factories/CollectionFactory.php new file mode 100644 index 00000000..0e869780 --- /dev/null +++ b/database/factories/CollectionFactory.php @@ -0,0 +1,35 @@ + + */ +class CollectionFactory extends Factory +{ + protected $model = Collection::class; + + /** + * @return array + */ + public function definition(): array + { + $title = fake()->unique()->words(2, true); + + return [ + 'store_id' => Store::factory(), + 'title' => ucfirst($title), + 'handle' => Str::slug($title).'-'.fake()->unique()->randomNumber(5), + 'description_html' => '

'.fake()->sentence().'

', + 'type' => CollectionType::Manual->value, + 'status' => CollectionStatus::Active->value, + ]; + } +} diff --git a/database/factories/CustomerAddressFactory.php b/database/factories/CustomerAddressFactory.php new file mode 100644 index 00000000..e9bbb667 --- /dev/null +++ b/database/factories/CustomerAddressFactory.php @@ -0,0 +1,48 @@ + + */ +class CustomerAddressFactory extends Factory +{ + protected $model = CustomerAddress::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'customer_id' => Customer::factory(), + 'label' => fake()->randomElement(['Home', 'Work']), + 'address_json' => [ + 'first_name' => fake()->firstName(), + 'last_name' => fake()->lastName(), + 'company' => '', + 'address1' => fake()->streetAddress(), + 'address2' => '', + 'city' => fake()->city(), + 'province' => '', + 'province_code' => '', + 'country' => 'Germany', + 'country_code' => 'DE', + 'zip' => fake()->postcode(), + 'phone' => fake()->phoneNumber(), + ], + 'is_default' => false, + ]; + } + + public function default(): static + { + return $this->state(fn (array $attributes): array => [ + 'is_default' => true, + ]); + } +} diff --git a/database/factories/CustomerFactory.php b/database/factories/CustomerFactory.php new file mode 100644 index 00000000..a7c10daa --- /dev/null +++ b/database/factories/CustomerFactory.php @@ -0,0 +1,37 @@ + + */ +class CustomerFactory extends Factory +{ + protected $model = Customer::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'email' => fake()->unique()->safeEmail(), + 'password_hash' => Hash::make('password'), + 'name' => fake()->name(), + 'marketing_opt_in' => false, + ]; + } + + public function withoutPassword(): static + { + return $this->state(fn (array $attributes): array => [ + 'password_hash' => null, + ]); + } +} diff --git a/database/factories/DiscountFactory.php b/database/factories/DiscountFactory.php new file mode 100644 index 00000000..5c4c6336 --- /dev/null +++ b/database/factories/DiscountFactory.php @@ -0,0 +1,85 @@ + + */ +class DiscountFactory extends Factory +{ + protected $model = Discount::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'type' => DiscountType::Code->value, + 'code' => strtoupper(fake()->bothify('SAVE##??')), + 'value_type' => DiscountValueType::Percent->value, + 'value_amount' => 10, + 'starts_at' => now()->subDay(), + 'ends_at' => now()->addMonth(), + 'usage_limit' => null, + 'usage_count' => 0, + 'rules_json' => [], + 'status' => DiscountStatus::Active->value, + ]; + } + + public function percent10(): static + { + return $this->state(fn (array $attributes): array => [ + 'value_type' => DiscountValueType::Percent->value, + 'value_amount' => 10, + ]); + } + + public function fixed500(): static + { + return $this->state(fn (array $attributes): array => [ + 'value_type' => DiscountValueType::Fixed->value, + 'value_amount' => 500, + ]); + } + + public function freeShipping(): static + { + return $this->state(fn (array $attributes): array => [ + 'value_type' => DiscountValueType::FreeShipping->value, + 'value_amount' => 0, + ]); + } + + public function expired(): static + { + return $this->state(fn (array $attributes): array => [ + 'starts_at' => now()->subMonths(2), + 'ends_at' => now()->subMonth(), + ]); + } + + public function disabled(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => DiscountStatus::Disabled->value, + ]); + } + + public function notYetActive(): static + { + return $this->state(fn (array $attributes): array => [ + 'starts_at' => now()->addWeek(), + 'ends_at' => now()->addMonth(), + ]); + } +} diff --git a/database/factories/FulfillmentFactory.php b/database/factories/FulfillmentFactory.php new file mode 100644 index 00000000..8659b00a --- /dev/null +++ b/database/factories/FulfillmentFactory.php @@ -0,0 +1,55 @@ + + */ +class FulfillmentFactory extends Factory +{ + protected $model = Fulfillment::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'order_id' => Order::factory(), + 'status' => FulfillmentShipmentStatus::Pending->value, + 'tracking_company' => null, + 'tracking_number' => null, + 'tracking_url' => null, + 'shipped_at' => null, + 'delivered_at' => null, + ]; + } + + public function shipped(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => FulfillmentShipmentStatus::Shipped->value, + 'tracking_company' => 'DHL', + 'tracking_number' => fake()->bothify('TRACK########'), + 'tracking_url' => fake()->url(), + 'shipped_at' => now(), + ]); + } + + public function delivered(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => FulfillmentShipmentStatus::Delivered->value, + 'tracking_company' => 'DHL', + 'tracking_number' => fake()->bothify('TRACK########'), + 'tracking_url' => fake()->url(), + 'shipped_at' => now()->subDays(2), + 'delivered_at' => now(), + ]); + } +} diff --git a/database/factories/FulfillmentLineFactory.php b/database/factories/FulfillmentLineFactory.php new file mode 100644 index 00000000..7b8a5105 --- /dev/null +++ b/database/factories/FulfillmentLineFactory.php @@ -0,0 +1,28 @@ + + */ +class FulfillmentLineFactory extends Factory +{ + protected $model = FulfillmentLine::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'fulfillment_id' => Fulfillment::factory(), + 'order_line_id' => OrderLine::factory(), + 'quantity' => 1, + ]; + } +} diff --git a/database/factories/InventoryItemFactory.php b/database/factories/InventoryItemFactory.php new file mode 100644 index 00000000..6a1e3088 --- /dev/null +++ b/database/factories/InventoryItemFactory.php @@ -0,0 +1,31 @@ + + */ +class InventoryItemFactory extends Factory +{ + protected $model = InventoryItem::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'variant_id' => ProductVariant::factory(), + 'quantity_on_hand' => 100, + 'quantity_reserved' => 0, + 'policy' => InventoryPolicy::Deny->value, + ]; + } +} diff --git a/database/factories/NavigationItemFactory.php b/database/factories/NavigationItemFactory.php new file mode 100644 index 00000000..5be351c0 --- /dev/null +++ b/database/factories/NavigationItemFactory.php @@ -0,0 +1,58 @@ + + */ +class NavigationItemFactory extends Factory +{ + protected $model = NavigationItem::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'menu_id' => NavigationMenu::factory(), + 'type' => NavigationItemType::Link->value, + 'label' => fake()->randomElement(['Home', 'Shop', 'About', 'Contact', 'Blog']), + 'url' => '/'.fake()->slug(), + 'resource_id' => null, + 'position' => fake()->numberBetween(0, 10), + ]; + } + + public function page(int $pageId): static + { + return $this->state(fn (array $attributes): array => [ + 'type' => NavigationItemType::Page->value, + 'url' => null, + 'resource_id' => $pageId, + ]); + } + + public function collection(int $collectionId): static + { + return $this->state(fn (array $attributes): array => [ + 'type' => NavigationItemType::Collection->value, + 'url' => null, + 'resource_id' => $collectionId, + ]); + } + + public function product(int $productId): static + { + return $this->state(fn (array $attributes): array => [ + 'type' => NavigationItemType::Product->value, + 'url' => null, + 'resource_id' => $productId, + ]); + } +} diff --git a/database/factories/NavigationMenuFactory.php b/database/factories/NavigationMenuFactory.php new file mode 100644 index 00000000..2bc3599b --- /dev/null +++ b/database/factories/NavigationMenuFactory.php @@ -0,0 +1,30 @@ + + */ +class NavigationMenuFactory extends Factory +{ + protected $model = NavigationMenu::class; + + /** + * @return array + */ + public function definition(): array + { + $title = fake()->randomElement(['Main Menu', 'Footer Menu', 'Mobile Menu']); + + return [ + 'store_id' => Store::factory(), + 'handle' => Str::slug($title).'-'.fake()->unique()->randomNumber(5), + 'title' => $title, + ]; + } +} diff --git a/database/factories/OrderFactory.php b/database/factories/OrderFactory.php new file mode 100644 index 00000000..d9a005f2 --- /dev/null +++ b/database/factories/OrderFactory.php @@ -0,0 +1,62 @@ + + */ +class OrderFactory extends Factory +{ + protected $model = Order::class; + + /** + * @return array + */ + public function definition(): array + { + $subtotal = 5000; + + return [ + 'store_id' => Store::factory(), + 'customer_id' => null, + 'order_number' => '#'.fake()->unique()->numberBetween(1000, 999999), + 'payment_method' => PaymentMethod::CreditCard->value, + 'status' => OrderStatus::Pending->value, + 'financial_status' => FinancialStatus::Pending->value, + 'fulfillment_status' => FulfillmentStatus::Unfulfilled->value, + 'currency' => 'EUR', + 'subtotal_amount' => $subtotal, + 'discount_amount' => 0, + 'shipping_amount' => 599, + 'tax_amount' => 0, + 'total_amount' => $subtotal + 599, + 'email' => fake()->safeEmail(), + 'billing_address_json' => null, + 'shipping_address_json' => null, + 'placed_at' => now(), + ]; + } + + public function paid(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => OrderStatus::Paid->value, + 'financial_status' => FinancialStatus::Paid->value, + ]); + } + + public function cancelled(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => OrderStatus::Cancelled->value, + ]); + } +} diff --git a/database/factories/OrderLineFactory.php b/database/factories/OrderLineFactory.php new file mode 100644 index 00000000..df244a18 --- /dev/null +++ b/database/factories/OrderLineFactory.php @@ -0,0 +1,38 @@ + + */ +class OrderLineFactory extends Factory +{ + protected $model = OrderLine::class; + + /** + * @return array + */ + public function definition(): array + { + $unitPrice = 2500; + $quantity = 2; + + return [ + 'order_id' => Order::factory(), + 'product_id' => null, + 'variant_id' => ProductVariant::factory(), + 'title_snapshot' => fake()->words(3, true), + 'sku_snapshot' => fake()->bothify('SKU-####'), + 'quantity' => $quantity, + 'unit_price_amount' => $unitPrice, + 'total_amount' => $unitPrice * $quantity, + 'tax_lines_json' => null, + 'discount_allocations_json' => null, + ]; + } +} diff --git a/database/factories/OrganizationFactory.php b/database/factories/OrganizationFactory.php new file mode 100644 index 00000000..b77d4279 --- /dev/null +++ b/database/factories/OrganizationFactory.php @@ -0,0 +1,25 @@ + + */ +class OrganizationFactory extends Factory +{ + protected $model = Organization::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->company(), + 'billing_email' => fake()->unique()->companyEmail(), + ]; + } +} diff --git a/database/factories/PageFactory.php b/database/factories/PageFactory.php new file mode 100644 index 00000000..215db973 --- /dev/null +++ b/database/factories/PageFactory.php @@ -0,0 +1,49 @@ + + */ +class PageFactory extends Factory +{ + protected $model = Page::class; + + /** + * @return array + */ + public function definition(): array + { + $title = fake()->unique()->sentence(3); + + return [ + 'store_id' => Store::factory(), + 'title' => rtrim($title, '.'), + 'handle' => Str::slug($title).'-'.fake()->unique()->randomNumber(5), + 'body_html' => '

'.fake()->paragraph().'

', + 'status' => PageStatus::Published->value, + 'published_at' => now(), + ]; + } + + public function draft(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => PageStatus::Draft->value, + 'published_at' => null, + ]); + } + + public function archived(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => PageStatus::Archived->value, + ]); + } +} diff --git a/database/factories/PaymentFactory.php b/database/factories/PaymentFactory.php new file mode 100644 index 00000000..f15b4383 --- /dev/null +++ b/database/factories/PaymentFactory.php @@ -0,0 +1,56 @@ + + */ +class PaymentFactory extends Factory +{ + protected $model = Payment::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'order_id' => Order::factory(), + 'provider' => 'mock', + 'method' => PaymentMethod::CreditCard->value, + 'provider_payment_id' => 'mock_'.Str::random(12), + 'status' => PaymentStatus::Captured->value, + 'amount' => 5000, + 'currency' => 'EUR', + 'raw_json_encrypted' => null, + ]; + } + + public function captured(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => PaymentStatus::Captured->value, + ]); + } + + public function pending(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => PaymentStatus::Pending->value, + ]); + } + + public function failed(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => PaymentStatus::Failed->value, + ]); + } +} diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php new file mode 100644 index 00000000..2cfc40b8 --- /dev/null +++ b/database/factories/ProductFactory.php @@ -0,0 +1,52 @@ + + */ +class ProductFactory extends Factory +{ + protected $model = Product::class; + + /** + * @return array + */ + public function definition(): array + { + $title = fake()->unique()->words(3, true); + + return [ + 'store_id' => Store::factory(), + 'title' => ucfirst($title), + 'handle' => Str::slug($title).'-'.fake()->unique()->randomNumber(5), + 'status' => ProductStatus::Active->value, + 'description_html' => '

'.fake()->paragraph().'

', + 'vendor' => fake()->company(), + 'product_type' => fake()->randomElement(['Apparel', 'Footwear', 'Accessories', 'Home']), + 'tags' => [fake()->word(), fake()->word()], + 'published_at' => now(), + ]; + } + + public function draft(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => ProductStatus::Draft->value, + 'published_at' => null, + ]); + } + + public function archived(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => ProductStatus::Archived->value, + ]); + } +} diff --git a/database/factories/ProductMediaFactory.php b/database/factories/ProductMediaFactory.php new file mode 100644 index 00000000..2eee5e26 --- /dev/null +++ b/database/factories/ProductMediaFactory.php @@ -0,0 +1,36 @@ + + */ +class ProductMediaFactory extends Factory +{ + protected $model = ProductMedia::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'type' => MediaType::Image->value, + 'storage_key' => 'products/'.fake()->uuid().'.jpg', + 'alt_text' => fake()->sentence(), + 'width' => 1200, + 'height' => 1200, + 'mime_type' => 'image/jpeg', + 'byte_size' => 204800, + 'position' => 0, + 'status' => MediaStatus::Processing->value, + ]; + } +} diff --git a/database/factories/ProductOptionFactory.php b/database/factories/ProductOptionFactory.php new file mode 100644 index 00000000..67acc66e --- /dev/null +++ b/database/factories/ProductOptionFactory.php @@ -0,0 +1,27 @@ + + */ +class ProductOptionFactory extends Factory +{ + protected $model = ProductOption::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'name' => fake()->randomElement(['Size', 'Color', 'Material']), + 'position' => 0, + ]; + } +} diff --git a/database/factories/ProductOptionValueFactory.php b/database/factories/ProductOptionValueFactory.php new file mode 100644 index 00000000..bb95a021 --- /dev/null +++ b/database/factories/ProductOptionValueFactory.php @@ -0,0 +1,27 @@ + + */ +class ProductOptionValueFactory extends Factory +{ + protected $model = ProductOptionValue::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'product_option_id' => ProductOption::factory(), + 'value' => fake()->word(), + 'position' => 0, + ]; + } +} diff --git a/database/factories/ProductVariantFactory.php b/database/factories/ProductVariantFactory.php new file mode 100644 index 00000000..4df703dc --- /dev/null +++ b/database/factories/ProductVariantFactory.php @@ -0,0 +1,43 @@ + + */ +class ProductVariantFactory extends Factory +{ + protected $model = ProductVariant::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'sku' => strtoupper(fake()->bothify('SKU-####-???')), + 'barcode' => fake()->ean13(), + 'price_amount' => 2499, + 'compare_at_amount' => null, + 'currency' => 'EUR', + 'weight_g' => 250, + 'requires_shipping' => true, + 'is_default' => false, + 'position' => 0, + 'status' => VariantStatus::Active->value, + ]; + } + + public function default(): static + { + return $this->state(fn (array $attributes): array => [ + 'is_default' => true, + ]); + } +} diff --git a/database/factories/RefundFactory.php b/database/factories/RefundFactory.php new file mode 100644 index 00000000..74cd85d3 --- /dev/null +++ b/database/factories/RefundFactory.php @@ -0,0 +1,33 @@ + + */ +class RefundFactory extends Factory +{ + protected $model = Refund::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'order_id' => Order::factory(), + 'payment_id' => Payment::factory(), + 'amount' => 1000, + 'reason' => 'customer_request', + 'status' => RefundStatus::Processed->value, + 'provider_refund_id' => 'mock_ref_'.Str::random(12), + ]; + } +} diff --git a/database/factories/ShippingRateFactory.php b/database/factories/ShippingRateFactory.php new file mode 100644 index 00000000..a538d6bb --- /dev/null +++ b/database/factories/ShippingRateFactory.php @@ -0,0 +1,67 @@ + + */ +class ShippingRateFactory extends Factory +{ + protected $model = ShippingRate::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'zone_id' => ShippingZone::factory(), + 'name' => 'Standard', + 'type' => ShippingRateType::Flat->value, + 'config_json' => ['amount' => 499], + 'is_active' => true, + ]; + } + + public function flat(int $amount = 499): static + { + return $this->state(fn (array $attributes): array => [ + 'type' => ShippingRateType::Flat->value, + 'config_json' => ['amount' => $amount], + ]); + } + + /** + * @param array $ranges + */ + public function weight(array $ranges): static + { + return $this->state(fn (array $attributes): array => [ + 'type' => ShippingRateType::Weight->value, + 'config_json' => ['ranges' => $ranges], + ]); + } + + /** + * @param array $ranges + */ + public function price(array $ranges): static + { + return $this->state(fn (array $attributes): array => [ + 'type' => ShippingRateType::Price->value, + 'config_json' => ['ranges' => $ranges], + ]); + } + + public function inactive(): static + { + return $this->state(fn (array $attributes): array => [ + 'is_active' => false, + ]); + } +} diff --git a/database/factories/ShippingZoneFactory.php b/database/factories/ShippingZoneFactory.php new file mode 100644 index 00000000..64860eba --- /dev/null +++ b/database/factories/ShippingZoneFactory.php @@ -0,0 +1,28 @@ + + */ +class ShippingZoneFactory extends Factory +{ + protected $model = ShippingZone::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'name' => fake()->randomElement(['Europe', 'Domestic', 'International']), + 'countries_json' => ['DE', 'AT', 'CH'], + 'regions_json' => [], + ]; + } +} diff --git a/database/factories/StoreDomainFactory.php b/database/factories/StoreDomainFactory.php new file mode 100644 index 00000000..ea92a2c5 --- /dev/null +++ b/database/factories/StoreDomainFactory.php @@ -0,0 +1,29 @@ + + */ +class StoreDomainFactory extends Factory +{ + protected $model = StoreDomain::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'hostname' => fake()->unique()->domainName(), + 'type' => 'storefront', + 'is_primary' => true, + 'tls_mode' => 'managed', + ]; + } +} diff --git a/database/factories/StoreFactory.php b/database/factories/StoreFactory.php new file mode 100644 index 00000000..dcd9d1b2 --- /dev/null +++ b/database/factories/StoreFactory.php @@ -0,0 +1,39 @@ + + */ +class StoreFactory extends Factory +{ + protected $model = Store::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'organization_id' => Organization::factory(), + 'name' => fake()->company().' Store', + 'handle' => Str::slug(fake()->unique()->company()).'-'.fake()->unique()->randomNumber(5), + 'status' => 'active', + 'default_currency' => 'EUR', + 'default_locale' => 'en', + 'timezone' => 'Europe/Berlin', + ]; + } + + public function suspended(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => 'suspended', + ]); + } +} diff --git a/database/factories/StoreSettingsFactory.php b/database/factories/StoreSettingsFactory.php new file mode 100644 index 00000000..c565fdb5 --- /dev/null +++ b/database/factories/StoreSettingsFactory.php @@ -0,0 +1,26 @@ + + */ +class StoreSettingsFactory extends Factory +{ + protected $model = StoreSettings::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'settings_json' => [], + ]; + } +} diff --git a/database/factories/TaxSettingsFactory.php b/database/factories/TaxSettingsFactory.php new file mode 100644 index 00000000..2965717e --- /dev/null +++ b/database/factories/TaxSettingsFactory.php @@ -0,0 +1,40 @@ + + */ +class TaxSettingsFactory extends Factory +{ + protected $model = TaxSettings::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'mode' => TaxMode::Manual->value, + 'provider' => null, + 'prices_include_tax' => false, + 'config_json' => [ + 'name' => 'VAT', + 'rate_basis_points' => 1900, + ], + ]; + } + + public function pricesInclude(): static + { + return $this->state(fn (array $attributes): array => [ + 'prices_include_tax' => true, + ]); + } +} diff --git a/database/factories/ThemeFactory.php b/database/factories/ThemeFactory.php new file mode 100644 index 00000000..0337c7ed --- /dev/null +++ b/database/factories/ThemeFactory.php @@ -0,0 +1,38 @@ + + */ +class ThemeFactory extends Factory +{ + protected $model = Theme::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'name' => fake()->randomElement(['Minimal', 'Classic', 'Modern', 'Vintage']).' Theme', + 'version' => '1.0.'.fake()->numberBetween(0, 9), + 'status' => ThemeStatus::Draft->value, + 'published_at' => null, + ]; + } + + public function published(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => ThemeStatus::Published->value, + 'published_at' => now(), + ]); + } +} diff --git a/database/factories/ThemeFileFactory.php b/database/factories/ThemeFileFactory.php new file mode 100644 index 00000000..3bbb331d --- /dev/null +++ b/database/factories/ThemeFileFactory.php @@ -0,0 +1,37 @@ + + */ +class ThemeFileFactory extends Factory +{ + protected $model = ThemeFile::class; + + /** + * @return array + */ + public function definition(): array + { + $path = fake()->randomElement([ + 'templates/index.liquid', + 'templates/product.liquid', + 'sections/header.liquid', + 'snippets/cart.liquid', + 'assets/styles.css', + ]); + + return [ + 'theme_id' => Theme::factory(), + 'path' => $path.'.'.fake()->unique()->randomNumber(5), + 'storage_key' => 'themes/'.fake()->uuid().'/'.$path, + 'sha256' => hash('sha256', fake()->text(50)), + 'byte_size' => fake()->numberBetween(100, 50_000), + ]; + } +} diff --git a/database/factories/ThemeSettingsFactory.php b/database/factories/ThemeSettingsFactory.php new file mode 100644 index 00000000..f87a81ea --- /dev/null +++ b/database/factories/ThemeSettingsFactory.php @@ -0,0 +1,32 @@ + + */ +class ThemeSettingsFactory extends Factory +{ + protected $model = ThemeSettings::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'theme_id' => Theme::factory(), + 'settings_json' => [ + 'colors' => [ + 'primary' => fake()->hexColor(), + ], + 'announcement' => fake()->sentence(), + 'footer_text' => '(c) '.fake()->company(), + ], + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 80da5ac7..eee75f1c 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -29,6 +29,8 @@ public function definition(): array 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), + 'status' => 'active', + 'last_login_at' => now(), 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, 'two_factor_confirmed_at' => null, diff --git a/database/factories/WebhookDeliveryFactory.php b/database/factories/WebhookDeliveryFactory.php new file mode 100644 index 00000000..a0196e20 --- /dev/null +++ b/database/factories/WebhookDeliveryFactory.php @@ -0,0 +1,31 @@ + + */ +class WebhookDeliveryFactory extends Factory +{ + protected $model = WebhookDelivery::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'subscription_id' => WebhookSubscription::factory(), + 'event_type' => 'order.created', + 'payload_json' => ['order_id' => 1], + 'response_status' => null, + 'response_body' => null, + 'attempts' => 0, + 'delivered_at' => null, + ]; + } +} diff --git a/database/factories/WebhookSubscriptionFactory.php b/database/factories/WebhookSubscriptionFactory.php new file mode 100644 index 00000000..79e2b2a0 --- /dev/null +++ b/database/factories/WebhookSubscriptionFactory.php @@ -0,0 +1,32 @@ + + */ +class WebhookSubscriptionFactory extends Factory +{ + protected $model = WebhookSubscription::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'store_id' => Store::factory(), + 'app_installation_id' => null, + 'event_type' => fake()->randomElement(['order.created', 'order.paid', 'order.fulfilled']), + 'url' => fake()->url(), + 'secret' => Str::random(40), + 'status' => 'active', + 'failed_count' => 0, + ]; + } +} diff --git a/database/migrations/2026_04_12_100001_create_organizations_table.php b/database/migrations/2026_04_12_100001_create_organizations_table.php new file mode 100644 index 00000000..4eb2eab2 --- /dev/null +++ b/database/migrations/2026_04_12_100001_create_organizations_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('name'); + $table->string('billing_email'); + $table->timestamps(); + + $table->index('billing_email', 'idx_organizations_billing_email'); + }); + } + + public function down(): void + { + Schema::dropIfExists('organizations'); + } +}; diff --git a/database/migrations/2026_04_12_100002_create_stores_table.php b/database/migrations/2026_04_12_100002_create_stores_table.php new file mode 100644 index 00000000..001066b4 --- /dev/null +++ b/database/migrations/2026_04_12_100002_create_stores_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('organization_id') + ->constrained('organizations') + ->cascadeOnDelete(); + $table->string('name'); + $table->string('handle')->unique('idx_stores_handle'); + $table->string('status')->default('active'); + $table->string('default_currency')->default('USD'); + $table->string('default_locale')->default('en'); + $table->string('timezone')->default('UTC'); + $table->timestamps(); + + $table->index('organization_id', 'idx_stores_organization_id'); + $table->index('status', 'idx_stores_status'); + }); + + DB::statement("CREATE TRIGGER stores_status_check BEFORE INSERT ON stores FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','suspended') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER stores_status_check_update BEFORE UPDATE ON stores FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','suspended') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS stores_status_check'); + DB::statement('DROP TRIGGER IF EXISTS stores_status_check_update'); + Schema::dropIfExists('stores'); + } +}; diff --git a/database/migrations/2026_04_12_100003_create_store_domains_table.php b/database/migrations/2026_04_12_100003_create_store_domains_table.php new file mode 100644 index 00000000..c09e5a6c --- /dev/null +++ b/database/migrations/2026_04_12_100003_create_store_domains_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('hostname')->unique('idx_store_domains_hostname'); + $table->string('type')->default('storefront'); + $table->boolean('is_primary')->default(false); + $table->string('tls_mode')->default('managed'); + $table->timestamp('created_at')->nullable(); + + $table->index('store_id', 'idx_store_domains_store_id'); + $table->index(['store_id', 'is_primary'], 'idx_store_domains_store_primary'); + }); + + DB::statement("CREATE TRIGGER store_domains_type_check BEFORE INSERT ON store_domains FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('storefront','admin','api') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER store_domains_tls_check BEFORE INSERT ON store_domains FOR EACH ROW BEGIN SELECT CASE WHEN NEW.tls_mode NOT IN ('managed','bring_your_own') THEN RAISE(ABORT, 'invalid tls_mode') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS store_domains_type_check'); + DB::statement('DROP TRIGGER IF EXISTS store_domains_tls_check'); + Schema::dropIfExists('store_domains'); + } +}; diff --git a/database/migrations/2026_04_12_100004_modify_users_table_for_shop.php b/database/migrations/2026_04_12_100004_modify_users_table_for_shop.php new file mode 100644 index 00000000..92b26988 --- /dev/null +++ b/database/migrations/2026_04_12_100004_modify_users_table_for_shop.php @@ -0,0 +1,26 @@ +string('status')->default('active')->after('password'); + $table->timestamp('last_login_at')->nullable()->after('email_verified_at'); + + $table->index('status', 'idx_users_status'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropIndex('idx_users_status'); + $table->dropColumn(['status', 'last_login_at']); + }); + } +}; diff --git a/database/migrations/2026_04_12_100005_create_store_users_table.php b/database/migrations/2026_04_12_100005_create_store_users_table.php new file mode 100644 index 00000000..0529cded --- /dev/null +++ b/database/migrations/2026_04_12_100005_create_store_users_table.php @@ -0,0 +1,35 @@ +foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->foreignId('user_id') + ->constrained('users') + ->cascadeOnDelete(); + $table->string('role')->default('staff'); + $table->timestamp('created_at')->nullable(); + + $table->primary(['store_id', 'user_id']); + $table->index('user_id', 'idx_store_users_user_id'); + $table->index(['store_id', 'role'], 'idx_store_users_role'); + }); + + DB::statement("CREATE TRIGGER store_users_role_check BEFORE INSERT ON store_users FOR EACH ROW BEGIN SELECT CASE WHEN NEW.role NOT IN ('owner','admin','staff','support') THEN RAISE(ABORT, 'invalid role') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS store_users_role_check'); + Schema::dropIfExists('store_users'); + } +}; diff --git a/database/migrations/2026_04_12_100006_create_store_settings_table.php b/database/migrations/2026_04_12_100006_create_store_settings_table.php new file mode 100644 index 00000000..3a1cf439 --- /dev/null +++ b/database/migrations/2026_04_12_100006_create_store_settings_table.php @@ -0,0 +1,25 @@ +foreignId('store_id') + ->primary() + ->constrained('stores') + ->cascadeOnDelete(); + $table->text('settings_json')->default('{}'); + $table->timestamp('updated_at')->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists('store_settings'); + } +}; diff --git a/database/migrations/2026_04_12_101001_create_products_table.php b/database/migrations/2026_04_12_101001_create_products_table.php new file mode 100644 index 00000000..40789963 --- /dev/null +++ b/database/migrations/2026_04_12_101001_create_products_table.php @@ -0,0 +1,45 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('title'); + $table->string('handle'); + $table->string('status')->default('draft'); + $table->text('description_html')->nullable(); + $table->string('vendor')->nullable(); + $table->string('product_type')->nullable(); + $table->json('tags')->default(DB::raw("('[]')")); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + + $table->unique(['store_id', 'handle'], 'idx_products_store_handle'); + $table->index('store_id', 'idx_products_store_id'); + $table->index(['store_id', 'status'], 'idx_products_store_status'); + $table->index(['store_id', 'published_at'], 'idx_products_published_at'); + $table->index(['store_id', 'vendor'], 'idx_products_vendor'); + $table->index(['store_id', 'product_type'], 'idx_products_product_type'); + }); + + DB::statement("CREATE TRIGGER products_status_check BEFORE INSERT ON products FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','active','archived') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER products_status_check_update BEFORE UPDATE ON products FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','active','archived') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS products_status_check'); + DB::statement('DROP TRIGGER IF EXISTS products_status_check_update'); + Schema::dropIfExists('products'); + } +}; diff --git a/database/migrations/2026_04_12_101002_create_product_options_table.php b/database/migrations/2026_04_12_101002_create_product_options_table.php new file mode 100644 index 00000000..a4c489de --- /dev/null +++ b/database/migrations/2026_04_12_101002_create_product_options_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('product_id') + ->constrained('products') + ->cascadeOnDelete(); + $table->string('name'); + $table->integer('position')->default(0); + + $table->index('product_id', 'idx_product_options_product_id'); + $table->unique(['product_id', 'position'], 'idx_product_options_product_position'); + }); + } + + public function down(): void + { + Schema::dropIfExists('product_options'); + } +}; diff --git a/database/migrations/2026_04_12_101003_create_product_option_values_table.php b/database/migrations/2026_04_12_101003_create_product_option_values_table.php new file mode 100644 index 00000000..0a9ba197 --- /dev/null +++ b/database/migrations/2026_04_12_101003_create_product_option_values_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('product_option_id') + ->constrained('product_options') + ->cascadeOnDelete(); + $table->string('value'); + $table->integer('position')->default(0); + + $table->index('product_option_id', 'idx_product_option_values_option_id'); + $table->unique(['product_option_id', 'position'], 'idx_product_option_values_option_position'); + }); + } + + public function down(): void + { + Schema::dropIfExists('product_option_values'); + } +}; diff --git a/database/migrations/2026_04_12_101004_create_product_variants_table.php b/database/migrations/2026_04_12_101004_create_product_variants_table.php new file mode 100644 index 00000000..8ce84916 --- /dev/null +++ b/database/migrations/2026_04_12_101004_create_product_variants_table.php @@ -0,0 +1,46 @@ +id(); + $table->foreignId('product_id') + ->constrained('products') + ->cascadeOnDelete(); + $table->string('sku')->nullable(); + $table->string('barcode')->nullable(); + $table->integer('price_amount')->default(0); + $table->integer('compare_at_amount')->nullable(); + $table->string('currency', 3)->default('USD'); + $table->integer('weight_g')->nullable(); + $table->boolean('requires_shipping')->default(true); + $table->boolean('is_default')->default(false); + $table->integer('position')->default(0); + $table->string('status')->default('active'); + $table->timestamps(); + + $table->index('product_id', 'idx_product_variants_product_id'); + $table->index('sku', 'idx_product_variants_sku'); + $table->index('barcode', 'idx_product_variants_barcode'); + $table->index(['product_id', 'position'], 'idx_product_variants_product_position'); + $table->index(['product_id', 'is_default'], 'idx_product_variants_product_default'); + }); + + DB::statement("CREATE TRIGGER product_variants_status_check BEFORE INSERT ON product_variants FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','archived') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER product_variants_status_check_update BEFORE UPDATE ON product_variants FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','archived') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS product_variants_status_check'); + DB::statement('DROP TRIGGER IF EXISTS product_variants_status_check_update'); + Schema::dropIfExists('product_variants'); + } +}; diff --git a/database/migrations/2026_04_12_101005_create_variant_option_values_table.php b/database/migrations/2026_04_12_101005_create_variant_option_values_table.php new file mode 100644 index 00000000..e050f4cd --- /dev/null +++ b/database/migrations/2026_04_12_101005_create_variant_option_values_table.php @@ -0,0 +1,28 @@ +foreignId('variant_id') + ->constrained('product_variants') + ->cascadeOnDelete(); + $table->foreignId('product_option_value_id') + ->constrained('product_option_values') + ->cascadeOnDelete(); + + $table->primary(['variant_id', 'product_option_value_id']); + $table->index('product_option_value_id', 'idx_variant_option_values_value_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('variant_option_values'); + } +}; diff --git a/database/migrations/2026_04_12_101006_create_inventory_items_table.php b/database/migrations/2026_04_12_101006_create_inventory_items_table.php new file mode 100644 index 00000000..44135633 --- /dev/null +++ b/database/migrations/2026_04_12_101006_create_inventory_items_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->foreignId('variant_id') + ->unique('idx_inventory_items_variant_id') + ->constrained('product_variants') + ->cascadeOnDelete(); + $table->integer('quantity_on_hand')->default(0); + $table->integer('quantity_reserved')->default(0); + $table->string('policy')->default('deny'); + $table->timestamp('updated_at')->nullable(); + + $table->index('store_id', 'idx_inventory_items_store_id'); + }); + + DB::statement("CREATE TRIGGER inventory_items_policy_check BEFORE INSERT ON inventory_items FOR EACH ROW BEGIN SELECT CASE WHEN NEW.policy NOT IN ('deny','continue') THEN RAISE(ABORT, 'invalid policy') END; END"); + DB::statement("CREATE TRIGGER inventory_items_policy_check_update BEFORE UPDATE ON inventory_items FOR EACH ROW BEGIN SELECT CASE WHEN NEW.policy NOT IN ('deny','continue') THEN RAISE(ABORT, 'invalid policy') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS inventory_items_policy_check'); + DB::statement('DROP TRIGGER IF EXISTS inventory_items_policy_check_update'); + Schema::dropIfExists('inventory_items'); + } +}; diff --git a/database/migrations/2026_04_12_101007_create_collections_table.php b/database/migrations/2026_04_12_101007_create_collections_table.php new file mode 100644 index 00000000..9296a78e --- /dev/null +++ b/database/migrations/2026_04_12_101007_create_collections_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('title'); + $table->string('handle'); + $table->text('description_html')->nullable(); + $table->string('type')->default('manual'); + $table->string('status')->default('active'); + $table->timestamps(); + + $table->unique(['store_id', 'handle'], 'idx_collections_store_handle'); + $table->index('store_id', 'idx_collections_store_id'); + $table->index(['store_id', 'status'], 'idx_collections_store_status'); + }); + + DB::statement("CREATE TRIGGER collections_type_check BEFORE INSERT ON collections FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('manual','automated') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER collections_type_check_update BEFORE UPDATE ON collections FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('manual','automated') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER collections_status_check BEFORE INSERT ON collections FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','active','archived') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER collections_status_check_update BEFORE UPDATE ON collections FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','active','archived') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS collections_type_check'); + DB::statement('DROP TRIGGER IF EXISTS collections_type_check_update'); + DB::statement('DROP TRIGGER IF EXISTS collections_status_check'); + DB::statement('DROP TRIGGER IF EXISTS collections_status_check_update'); + Schema::dropIfExists('collections'); + } +}; diff --git a/database/migrations/2026_04_12_101008_create_collection_products_table.php b/database/migrations/2026_04_12_101008_create_collection_products_table.php new file mode 100644 index 00000000..ef5e27ee --- /dev/null +++ b/database/migrations/2026_04_12_101008_create_collection_products_table.php @@ -0,0 +1,30 @@ +foreignId('collection_id') + ->constrained('collections') + ->cascadeOnDelete(); + $table->foreignId('product_id') + ->constrained('products') + ->cascadeOnDelete(); + $table->integer('position')->default(0); + + $table->primary(['collection_id', 'product_id']); + $table->index('product_id', 'idx_collection_products_product_id'); + $table->index(['collection_id', 'position'], 'idx_collection_products_position'); + }); + } + + public function down(): void + { + Schema::dropIfExists('collection_products'); + } +}; diff --git a/database/migrations/2026_04_12_101009_create_product_media_table.php b/database/migrations/2026_04_12_101009_create_product_media_table.php new file mode 100644 index 00000000..906ca6b2 --- /dev/null +++ b/database/migrations/2026_04_12_101009_create_product_media_table.php @@ -0,0 +1,47 @@ +id(); + $table->foreignId('product_id') + ->constrained('products') + ->cascadeOnDelete(); + $table->string('type')->default('image'); + $table->string('storage_key'); + $table->string('alt_text')->nullable(); + $table->integer('width')->nullable(); + $table->integer('height')->nullable(); + $table->string('mime_type')->nullable(); + $table->integer('byte_size')->nullable(); + $table->integer('position')->default(0); + $table->string('status')->default('processing'); + $table->timestamp('created_at')->nullable(); + + $table->index('product_id', 'idx_product_media_product_id'); + $table->index(['product_id', 'position'], 'idx_product_media_product_position'); + $table->index('status', 'idx_product_media_status'); + }); + + DB::statement("CREATE TRIGGER product_media_type_check BEFORE INSERT ON product_media FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('image','video') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER product_media_type_check_update BEFORE UPDATE ON product_media FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('image','video') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER product_media_status_check BEFORE INSERT ON product_media FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('processing','ready','failed') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER product_media_status_check_update BEFORE UPDATE ON product_media FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('processing','ready','failed') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS product_media_type_check'); + DB::statement('DROP TRIGGER IF EXISTS product_media_type_check_update'); + DB::statement('DROP TRIGGER IF EXISTS product_media_status_check'); + DB::statement('DROP TRIGGER IF EXISTS product_media_status_check_update'); + Schema::dropIfExists('product_media'); + } +}; diff --git a/database/migrations/2026_04_12_102001_create_themes_table.php b/database/migrations/2026_04_12_102001_create_themes_table.php new file mode 100644 index 00000000..8bcb2fa1 --- /dev/null +++ b/database/migrations/2026_04_12_102001_create_themes_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('name'); + $table->string('version')->nullable(); + $table->string('status')->default('draft'); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + + $table->index('store_id', 'idx_themes_store_id'); + $table->index(['store_id', 'status'], 'idx_themes_store_status'); + }); + + DB::statement("CREATE TRIGGER themes_status_check BEFORE INSERT ON themes FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','published') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER themes_status_check_update BEFORE UPDATE ON themes FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','published') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS themes_status_check'); + DB::statement('DROP TRIGGER IF EXISTS themes_status_check_update'); + Schema::dropIfExists('themes'); + } +}; diff --git a/database/migrations/2026_04_12_102002_create_theme_files_table.php b/database/migrations/2026_04_12_102002_create_theme_files_table.php new file mode 100644 index 00000000..a662cf8e --- /dev/null +++ b/database/migrations/2026_04_12_102002_create_theme_files_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('theme_id') + ->constrained('themes') + ->cascadeOnDelete(); + $table->string('path'); + $table->string('storage_key'); + $table->string('sha256', 64); + $table->unsignedBigInteger('byte_size')->default(0); + + $table->unique(['theme_id', 'path'], 'idx_theme_files_theme_path'); + $table->index('theme_id', 'idx_theme_files_theme_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('theme_files'); + } +}; diff --git a/database/migrations/2026_04_12_102003_create_theme_settings_table.php b/database/migrations/2026_04_12_102003_create_theme_settings_table.php new file mode 100644 index 00000000..19f14bb3 --- /dev/null +++ b/database/migrations/2026_04_12_102003_create_theme_settings_table.php @@ -0,0 +1,26 @@ +foreignId('theme_id') + ->primary() + ->constrained('themes') + ->cascadeOnDelete(); + $table->json('settings_json')->default(DB::raw("('{}')")); + $table->timestamp('updated_at')->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists('theme_settings'); + } +}; diff --git a/database/migrations/2026_04_12_102004_create_pages_table.php b/database/migrations/2026_04_12_102004_create_pages_table.php new file mode 100644 index 00000000..07dc27ba --- /dev/null +++ b/database/migrations/2026_04_12_102004_create_pages_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('title'); + $table->string('handle'); + $table->longText('body_html')->nullable(); + $table->string('status')->default('draft'); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + + $table->unique(['store_id', 'handle'], 'idx_pages_store_handle'); + $table->index('store_id', 'idx_pages_store_id'); + $table->index(['store_id', 'status'], 'idx_pages_store_status'); + }); + + DB::statement("CREATE TRIGGER pages_status_check BEFORE INSERT ON pages FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','published','archived') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER pages_status_check_update BEFORE UPDATE ON pages FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','published','archived') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS pages_status_check'); + DB::statement('DROP TRIGGER IF EXISTS pages_status_check_update'); + Schema::dropIfExists('pages'); + } +}; diff --git a/database/migrations/2026_04_12_102005_create_navigation_menus_table.php b/database/migrations/2026_04_12_102005_create_navigation_menus_table.php new file mode 100644 index 00000000..4e92cfdf --- /dev/null +++ b/database/migrations/2026_04_12_102005_create_navigation_menus_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('handle'); + $table->string('title'); + $table->timestamps(); + + $table->unique(['store_id', 'handle'], 'idx_navigation_menus_store_handle'); + $table->index('store_id', 'idx_navigation_menus_store_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('navigation_menus'); + } +}; diff --git a/database/migrations/2026_04_12_102006_create_navigation_items_table.php b/database/migrations/2026_04_12_102006_create_navigation_items_table.php new file mode 100644 index 00000000..af51d3e7 --- /dev/null +++ b/database/migrations/2026_04_12_102006_create_navigation_items_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('menu_id') + ->constrained('navigation_menus') + ->cascadeOnDelete(); + $table->string('type')->default('link'); + $table->string('label'); + $table->string('url')->nullable(); + $table->unsignedBigInteger('resource_id')->nullable(); + $table->unsignedInteger('position')->default(0); + + $table->index('menu_id', 'idx_navigation_items_menu_id'); + $table->index(['menu_id', 'position'], 'idx_navigation_items_menu_position'); + }); + + DB::statement("CREATE TRIGGER navigation_items_type_check BEFORE INSERT ON navigation_items FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('link','page','collection','product') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER navigation_items_type_check_update BEFORE UPDATE ON navigation_items FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('link','page','collection','product') THEN RAISE(ABORT, 'invalid type') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS navigation_items_type_check'); + DB::statement('DROP TRIGGER IF EXISTS navigation_items_type_check_update'); + Schema::dropIfExists('navigation_items'); + } +}; diff --git a/database/migrations/2026_04_12_103001_create_carts_table.php b/database/migrations/2026_04_12_103001_create_carts_table.php new file mode 100644 index 00000000..e6feb973 --- /dev/null +++ b/database/migrations/2026_04_12_103001_create_carts_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->unsignedBigInteger('customer_id')->nullable()->comment('FK to customers added in Phase 6'); + $table->string('session_id')->nullable(); + $table->string('currency', 3)->default('USD'); + $table->integer('cart_version')->default(1); + $table->string('status')->default('active'); + $table->timestamps(); + + $table->index('store_id', 'idx_carts_store_id'); + $table->index('customer_id', 'idx_carts_customer_id'); + $table->index(['store_id', 'status'], 'idx_carts_store_status'); + $table->index('session_id', 'idx_carts_session_id'); + }); + + DB::statement("CREATE TRIGGER carts_status_check BEFORE INSERT ON carts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','converted','abandoned') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER carts_status_check_update BEFORE UPDATE ON carts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','converted','abandoned') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS carts_status_check'); + DB::statement('DROP TRIGGER IF EXISTS carts_status_check_update'); + Schema::dropIfExists('carts'); + } +}; diff --git a/database/migrations/2026_04_12_103002_create_cart_lines_table.php b/database/migrations/2026_04_12_103002_create_cart_lines_table.php new file mode 100644 index 00000000..8a82dadb --- /dev/null +++ b/database/migrations/2026_04_12_103002_create_cart_lines_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('cart_id') + ->constrained('carts') + ->cascadeOnDelete(); + $table->foreignId('variant_id') + ->constrained('product_variants') + ->cascadeOnDelete(); + $table->integer('quantity')->default(1); + $table->integer('unit_price_amount')->default(0); + $table->integer('line_subtotal_amount')->default(0); + $table->integer('line_discount_amount')->default(0); + $table->integer('line_total_amount')->default(0); + + $table->index('cart_id', 'idx_cart_lines_cart_id'); + $table->unique(['cart_id', 'variant_id'], 'idx_cart_lines_cart_variant'); + }); + } + + public function down(): void + { + Schema::dropIfExists('cart_lines'); + } +}; diff --git a/database/migrations/2026_04_12_103003_create_checkouts_table.php b/database/migrations/2026_04_12_103003_create_checkouts_table.php new file mode 100644 index 00000000..07c23666 --- /dev/null +++ b/database/migrations/2026_04_12_103003_create_checkouts_table.php @@ -0,0 +1,54 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->foreignId('cart_id') + ->constrained('carts') + ->cascadeOnDelete(); + $table->unsignedBigInteger('customer_id')->nullable()->comment('FK to customers added in Phase 6'); + $table->string('status')->default('started'); + $table->string('payment_method')->nullable(); + $table->string('email')->nullable(); + $table->text('shipping_address_json')->nullable(); + $table->text('billing_address_json')->nullable(); + $table->unsignedBigInteger('shipping_method_id')->nullable(); + $table->string('discount_code')->nullable(); + $table->text('tax_provider_snapshot_json')->nullable(); + $table->text('totals_json')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + + $table->index('store_id', 'idx_checkouts_store_id'); + $table->index('cart_id', 'idx_checkouts_cart_id'); + $table->index('customer_id', 'idx_checkouts_customer_id'); + $table->index(['store_id', 'status'], 'idx_checkouts_status'); + $table->index('expires_at', 'idx_checkouts_expires_at'); + }); + + DB::statement("CREATE TRIGGER checkouts_status_check BEFORE INSERT ON checkouts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('started','addressed','shipping_selected','payment_selected','completed','expired') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER checkouts_status_check_update BEFORE UPDATE ON checkouts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('started','addressed','shipping_selected','payment_selected','completed','expired') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER checkouts_payment_method_check BEFORE INSERT ON checkouts FOR EACH ROW WHEN NEW.payment_method IS NOT NULL BEGIN SELECT CASE WHEN NEW.payment_method NOT IN ('credit_card','paypal','bank_transfer') THEN RAISE(ABORT, 'invalid payment_method') END; END"); + DB::statement("CREATE TRIGGER checkouts_payment_method_check_update BEFORE UPDATE ON checkouts FOR EACH ROW WHEN NEW.payment_method IS NOT NULL BEGIN SELECT CASE WHEN NEW.payment_method NOT IN ('credit_card','paypal','bank_transfer') THEN RAISE(ABORT, 'invalid payment_method') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS checkouts_status_check'); + DB::statement('DROP TRIGGER IF EXISTS checkouts_status_check_update'); + DB::statement('DROP TRIGGER IF EXISTS checkouts_payment_method_check'); + DB::statement('DROP TRIGGER IF EXISTS checkouts_payment_method_check_update'); + Schema::dropIfExists('checkouts'); + } +}; diff --git a/database/migrations/2026_04_12_103004_create_shipping_zones_table.php b/database/migrations/2026_04_12_103004_create_shipping_zones_table.php new file mode 100644 index 00000000..fce22d10 --- /dev/null +++ b/database/migrations/2026_04_12_103004_create_shipping_zones_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('name'); + $table->text('countries_json')->default('[]'); + $table->text('regions_json')->default('[]'); + $table->timestamps(); + + $table->index('store_id', 'idx_shipping_zones_store_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipping_zones'); + } +}; diff --git a/database/migrations/2026_04_12_103005_create_shipping_rates_table.php b/database/migrations/2026_04_12_103005_create_shipping_rates_table.php new file mode 100644 index 00000000..544ae663 --- /dev/null +++ b/database/migrations/2026_04_12_103005_create_shipping_rates_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('zone_id') + ->constrained('shipping_zones') + ->cascadeOnDelete(); + $table->string('name'); + $table->string('type')->default('flat'); + $table->text('config_json')->default('{}'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index('zone_id', 'idx_shipping_rates_zone_id'); + $table->index(['zone_id', 'is_active'], 'idx_shipping_rates_zone_active'); + }); + + DB::statement("CREATE TRIGGER shipping_rates_type_check BEFORE INSERT ON shipping_rates FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('flat','weight','price','carrier') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER shipping_rates_type_check_update BEFORE UPDATE ON shipping_rates FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('flat','weight','price','carrier') THEN RAISE(ABORT, 'invalid type') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS shipping_rates_type_check'); + DB::statement('DROP TRIGGER IF EXISTS shipping_rates_type_check_update'); + Schema::dropIfExists('shipping_rates'); + } +}; diff --git a/database/migrations/2026_04_12_103006_create_tax_settings_table.php b/database/migrations/2026_04_12_103006_create_tax_settings_table.php new file mode 100644 index 00000000..2bbbcd13 --- /dev/null +++ b/database/migrations/2026_04_12_103006_create_tax_settings_table.php @@ -0,0 +1,34 @@ +foreignId('store_id') + ->primary() + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('mode')->default('manual'); + $table->string('provider')->nullable(); + $table->boolean('prices_include_tax')->default(false); + $table->text('config_json')->default('{}'); + $table->timestamp('updated_at')->nullable(); + }); + + DB::statement("CREATE TRIGGER tax_settings_mode_check BEFORE INSERT ON tax_settings FOR EACH ROW BEGIN SELECT CASE WHEN NEW.mode NOT IN ('manual','provider') THEN RAISE(ABORT, 'invalid mode') END; END"); + DB::statement("CREATE TRIGGER tax_settings_mode_check_update BEFORE UPDATE ON tax_settings FOR EACH ROW BEGIN SELECT CASE WHEN NEW.mode NOT IN ('manual','provider') THEN RAISE(ABORT, 'invalid mode') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS tax_settings_mode_check'); + DB::statement('DROP TRIGGER IF EXISTS tax_settings_mode_check_update'); + Schema::dropIfExists('tax_settings'); + } +}; diff --git a/database/migrations/2026_04_12_103007_create_discounts_table.php b/database/migrations/2026_04_12_103007_create_discounts_table.php new file mode 100644 index 00000000..40dcf1d1 --- /dev/null +++ b/database/migrations/2026_04_12_103007_create_discounts_table.php @@ -0,0 +1,53 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('type')->default('code'); + $table->string('code')->nullable(); + $table->string('value_type'); + $table->integer('value_amount')->default(0); + $table->timestamp('starts_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->integer('usage_limit')->nullable(); + $table->integer('usage_count')->default(0); + $table->text('rules_json')->nullable(); + $table->string('status')->default('draft'); + $table->timestamps(); + + $table->unique(['store_id', 'code'], 'idx_discounts_store_code'); + $table->index('store_id', 'idx_discounts_store_id'); + $table->index(['store_id', 'status'], 'idx_discounts_store_status'); + $table->index(['store_id', 'type'], 'idx_discounts_store_type'); + }); + + DB::statement("CREATE TRIGGER discounts_type_check BEFORE INSERT ON discounts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('code','automatic') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER discounts_type_check_update BEFORE UPDATE ON discounts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('code','automatic') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER discounts_value_type_check BEFORE INSERT ON discounts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.value_type NOT IN ('percent','fixed','free_shipping') THEN RAISE(ABORT, 'invalid value_type') END; END"); + DB::statement("CREATE TRIGGER discounts_value_type_check_update BEFORE UPDATE ON discounts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.value_type NOT IN ('percent','fixed','free_shipping') THEN RAISE(ABORT, 'invalid value_type') END; END"); + DB::statement("CREATE TRIGGER discounts_status_check BEFORE INSERT ON discounts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','active','expired','disabled') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER discounts_status_check_update BEFORE UPDATE ON discounts FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('draft','active','expired','disabled') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS discounts_type_check'); + DB::statement('DROP TRIGGER IF EXISTS discounts_type_check_update'); + DB::statement('DROP TRIGGER IF EXISTS discounts_value_type_check'); + DB::statement('DROP TRIGGER IF EXISTS discounts_value_type_check_update'); + DB::statement('DROP TRIGGER IF EXISTS discounts_status_check'); + DB::statement('DROP TRIGGER IF EXISTS discounts_status_check_update'); + Schema::dropIfExists('discounts'); + } +}; diff --git a/database/migrations/2026_04_12_104001_create_customers_table.php b/database/migrations/2026_04_12_104001_create_customers_table.php new file mode 100644 index 00000000..a8eab2b1 --- /dev/null +++ b/database/migrations/2026_04_12_104001_create_customers_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('email'); + $table->text('password_hash')->nullable(); + $table->string('name')->nullable(); + $table->boolean('marketing_opt_in')->default(false); + $table->rememberToken(); + $table->timestamps(); + + $table->unique(['store_id', 'email'], 'idx_customers_store_email'); + $table->index('store_id', 'idx_customers_store_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('customers'); + } +}; diff --git a/database/migrations/2026_04_12_104002_create_customer_addresses_table.php b/database/migrations/2026_04_12_104002_create_customer_addresses_table.php new file mode 100644 index 00000000..09465bd3 --- /dev/null +++ b/database/migrations/2026_04_12_104002_create_customer_addresses_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('customer_id') + ->constrained('customers') + ->cascadeOnDelete(); + $table->string('label')->nullable(); + $table->text('address_json')->default('{}'); + $table->boolean('is_default')->default(false); + + $table->index('customer_id', 'idx_customer_addresses_customer_id'); + $table->index(['customer_id', 'is_default'], 'idx_customer_addresses_default'); + }); + } + + public function down(): void + { + Schema::dropIfExists('customer_addresses'); + } +}; diff --git a/database/migrations/2026_04_12_104003_create_orders_table.php b/database/migrations/2026_04_12_104003_create_orders_table.php new file mode 100644 index 00000000..ced21a5b --- /dev/null +++ b/database/migrations/2026_04_12_104003_create_orders_table.php @@ -0,0 +1,69 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->foreignId('customer_id') + ->nullable() + ->constrained('customers') + ->nullOnDelete(); + $table->string('order_number'); + $table->string('payment_method'); + $table->string('status')->default('pending'); + $table->string('financial_status')->default('pending'); + $table->string('fulfillment_status')->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'], 'idx_orders_store_order_number'); + $table->index('store_id', 'idx_orders_store_id'); + $table->index('customer_id', 'idx_orders_customer_id'); + $table->index(['store_id', 'status'], 'idx_orders_store_status'); + $table->index(['store_id', 'financial_status'], 'idx_orders_store_financial'); + $table->index(['store_id', 'fulfillment_status'], 'idx_orders_store_fulfillment'); + $table->index(['store_id', 'placed_at'], 'idx_orders_placed_at'); + }); + + DB::statement("CREATE TRIGGER orders_payment_method_check BEFORE INSERT ON orders FOR EACH ROW BEGIN SELECT CASE WHEN NEW.payment_method NOT IN ('credit_card','paypal','bank_transfer') THEN RAISE(ABORT, 'invalid payment_method') END; END"); + DB::statement("CREATE TRIGGER orders_payment_method_check_update BEFORE UPDATE ON orders FOR EACH ROW BEGIN SELECT CASE WHEN NEW.payment_method NOT IN ('credit_card','paypal','bank_transfer') THEN RAISE(ABORT, 'invalid payment_method') END; END"); + DB::statement("CREATE TRIGGER orders_status_check BEFORE INSERT ON orders FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('pending','paid','fulfilled','cancelled','refunded') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER orders_status_check_update BEFORE UPDATE ON orders FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('pending','paid','fulfilled','cancelled','refunded') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER orders_financial_status_check BEFORE INSERT ON orders FOR EACH ROW BEGIN SELECT CASE WHEN NEW.financial_status NOT IN ('pending','authorized','paid','partially_refunded','refunded','voided') THEN RAISE(ABORT, 'invalid financial_status') END; END"); + DB::statement("CREATE TRIGGER orders_financial_status_check_update BEFORE UPDATE ON orders FOR EACH ROW BEGIN SELECT CASE WHEN NEW.financial_status NOT IN ('pending','authorized','paid','partially_refunded','refunded','voided') THEN RAISE(ABORT, 'invalid financial_status') END; END"); + DB::statement("CREATE TRIGGER orders_fulfillment_status_check BEFORE INSERT ON orders FOR EACH ROW BEGIN SELECT CASE WHEN NEW.fulfillment_status NOT IN ('unfulfilled','partial','fulfilled') THEN RAISE(ABORT, 'invalid fulfillment_status') END; END"); + DB::statement("CREATE TRIGGER orders_fulfillment_status_check_update BEFORE UPDATE ON orders FOR EACH ROW BEGIN SELECT CASE WHEN NEW.fulfillment_status NOT IN ('unfulfilled','partial','fulfilled') THEN RAISE(ABORT, 'invalid fulfillment_status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS orders_payment_method_check'); + DB::statement('DROP TRIGGER IF EXISTS orders_payment_method_check_update'); + DB::statement('DROP TRIGGER IF EXISTS orders_status_check'); + DB::statement('DROP TRIGGER IF EXISTS orders_status_check_update'); + DB::statement('DROP TRIGGER IF EXISTS orders_financial_status_check'); + DB::statement('DROP TRIGGER IF EXISTS orders_financial_status_check_update'); + DB::statement('DROP TRIGGER IF EXISTS orders_fulfillment_status_check'); + DB::statement('DROP TRIGGER IF EXISTS orders_fulfillment_status_check_update'); + Schema::dropIfExists('orders'); + } +}; diff --git a/database/migrations/2026_04_12_104004_create_order_lines_table.php b/database/migrations/2026_04_12_104004_create_order_lines_table.php new file mode 100644 index 00000000..abda70d6 --- /dev/null +++ b/database/migrations/2026_04_12_104004_create_order_lines_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('order_id') + ->constrained('orders') + ->cascadeOnDelete(); + $table->foreignId('product_id') + ->nullable() + ->constrained('products') + ->nullOnDelete(); + $table->foreignId('variant_id') + ->nullable() + ->constrained('product_variants') + ->nullOnDelete(); + $table->string('title_snapshot'); + $table->string('sku_snapshot')->nullable(); + $table->integer('quantity')->default(1); + $table->integer('unit_price_amount')->default(0); + $table->integer('total_amount')->default(0); + $table->text('tax_lines_json')->nullable(); + $table->text('discount_allocations_json')->nullable(); + + $table->index('order_id', 'idx_order_lines_order_id'); + $table->index('product_id', 'idx_order_lines_product_id'); + $table->index('variant_id', 'idx_order_lines_variant_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('order_lines'); + } +}; diff --git a/database/migrations/2026_04_12_104005_create_payments_table.php b/database/migrations/2026_04_12_104005_create_payments_table.php new file mode 100644 index 00000000..332feed9 --- /dev/null +++ b/database/migrations/2026_04_12_104005_create_payments_table.php @@ -0,0 +1,50 @@ +id(); + $table->foreignId('order_id') + ->constrained('orders') + ->cascadeOnDelete(); + $table->string('provider')->default('mock'); + $table->string('method'); + $table->string('provider_payment_id')->nullable(); + $table->string('status')->default('pending'); + $table->integer('amount')->default(0); + $table->string('currency', 3)->default('USD'); + $table->text('raw_json_encrypted')->nullable(); + $table->timestamp('created_at')->nullable(); + + $table->index('order_id', 'idx_payments_order_id'); + $table->index(['provider', 'provider_payment_id'], 'idx_payments_provider_id'); + $table->index('method', 'idx_payments_method'); + $table->index('status', 'idx_payments_status'); + }); + + DB::statement("CREATE TRIGGER payments_provider_check BEFORE INSERT ON payments FOR EACH ROW BEGIN SELECT CASE WHEN NEW.provider NOT IN ('mock') THEN RAISE(ABORT, 'invalid provider') END; END"); + DB::statement("CREATE TRIGGER payments_provider_check_update BEFORE UPDATE ON payments FOR EACH ROW BEGIN SELECT CASE WHEN NEW.provider NOT IN ('mock') THEN RAISE(ABORT, 'invalid provider') END; END"); + DB::statement("CREATE TRIGGER payments_method_check BEFORE INSERT ON payments FOR EACH ROW BEGIN SELECT CASE WHEN NEW.method NOT IN ('credit_card','paypal','bank_transfer') THEN RAISE(ABORT, 'invalid method') END; END"); + DB::statement("CREATE TRIGGER payments_method_check_update BEFORE UPDATE ON payments FOR EACH ROW BEGIN SELECT CASE WHEN NEW.method NOT IN ('credit_card','paypal','bank_transfer') THEN RAISE(ABORT, 'invalid method') END; END"); + DB::statement("CREATE TRIGGER payments_status_check BEFORE INSERT ON payments FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('pending','captured','failed','refunded') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER payments_status_check_update BEFORE UPDATE ON payments FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('pending','captured','failed','refunded') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS payments_provider_check'); + DB::statement('DROP TRIGGER IF EXISTS payments_provider_check_update'); + DB::statement('DROP TRIGGER IF EXISTS payments_method_check'); + DB::statement('DROP TRIGGER IF EXISTS payments_method_check_update'); + DB::statement('DROP TRIGGER IF EXISTS payments_status_check'); + DB::statement('DROP TRIGGER IF EXISTS payments_status_check_update'); + Schema::dropIfExists('payments'); + } +}; diff --git a/database/migrations/2026_04_12_104006_create_refunds_table.php b/database/migrations/2026_04_12_104006_create_refunds_table.php new file mode 100644 index 00000000..1209e06a --- /dev/null +++ b/database/migrations/2026_04_12_104006_create_refunds_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('order_id') + ->constrained('orders') + ->cascadeOnDelete(); + $table->foreignId('payment_id') + ->constrained('payments') + ->cascadeOnDelete(); + $table->integer('amount')->default(0); + $table->string('reason')->nullable(); + $table->string('status')->default('pending'); + $table->string('provider_refund_id')->nullable(); + $table->timestamp('created_at')->nullable(); + + $table->index('order_id', 'idx_refunds_order_id'); + $table->index('payment_id', 'idx_refunds_payment_id'); + $table->index('status', 'idx_refunds_status'); + }); + + DB::statement("CREATE TRIGGER refunds_status_check BEFORE INSERT ON refunds FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('pending','processed','failed') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER refunds_status_check_update BEFORE UPDATE ON refunds FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('pending','processed','failed') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS refunds_status_check'); + DB::statement('DROP TRIGGER IF EXISTS refunds_status_check_update'); + Schema::dropIfExists('refunds'); + } +}; diff --git a/database/migrations/2026_04_12_104007_create_fulfillments_table.php b/database/migrations/2026_04_12_104007_create_fulfillments_table.php new file mode 100644 index 00000000..b740d634 --- /dev/null +++ b/database/migrations/2026_04_12_104007_create_fulfillments_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('order_id') + ->constrained('orders') + ->cascadeOnDelete(); + $table->string('status')->default('pending'); + $table->string('tracking_company')->nullable(); + $table->string('tracking_number')->nullable(); + $table->string('tracking_url')->nullable(); + $table->timestamp('shipped_at')->nullable(); + $table->timestamp('delivered_at')->nullable(); + $table->timestamp('created_at')->nullable(); + + $table->index('order_id', 'idx_fulfillments_order_id'); + $table->index('status', 'idx_fulfillments_status'); + $table->index(['tracking_company', 'tracking_number'], 'idx_fulfillments_tracking'); + }); + + DB::statement("CREATE TRIGGER fulfillments_status_check BEFORE INSERT ON fulfillments FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('pending','shipped','delivered') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER fulfillments_status_check_update BEFORE UPDATE ON fulfillments FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('pending','shipped','delivered') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS fulfillments_status_check'); + DB::statement('DROP TRIGGER IF EXISTS fulfillments_status_check_update'); + Schema::dropIfExists('fulfillments'); + } +}; diff --git a/database/migrations/2026_04_12_104008_create_fulfillment_lines_table.php b/database/migrations/2026_04_12_104008_create_fulfillment_lines_table.php new file mode 100644 index 00000000..f0392bc5 --- /dev/null +++ b/database/migrations/2026_04_12_104008_create_fulfillment_lines_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('fulfillment_id') + ->constrained('fulfillments') + ->cascadeOnDelete(); + $table->foreignId('order_line_id') + ->constrained('order_lines') + ->cascadeOnDelete(); + $table->integer('quantity')->default(1); + + $table->index('fulfillment_id', 'idx_fulfillment_lines_fulfillment_id'); + $table->unique(['fulfillment_id', 'order_line_id'], 'idx_fulfillment_lines_fulfillment_order_line'); + }); + } + + public function down(): void + { + Schema::dropIfExists('fulfillment_lines'); + } +}; diff --git a/database/migrations/2026_04_12_104009_create_customer_password_reset_tokens_table.php b/database/migrations/2026_04_12_104009_create_customer_password_reset_tokens_table.php new file mode 100644 index 00000000..ba0f798d --- /dev/null +++ b/database/migrations/2026_04_12_104009_create_customer_password_reset_tokens_table.php @@ -0,0 +1,22 @@ +string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists('customer_password_reset_tokens'); + } +}; diff --git a/database/migrations/2026_04_12_105001_create_search_settings_table.php b/database/migrations/2026_04_12_105001_create_search_settings_table.php new file mode 100644 index 00000000..771cf943 --- /dev/null +++ b/database/migrations/2026_04_12_105001_create_search_settings_table.php @@ -0,0 +1,26 @@ +foreignId('store_id') + ->primary() + ->constrained('stores') + ->cascadeOnDelete(); + $table->text('synonyms_json')->nullable(); + $table->text('stop_words_json')->nullable(); + $table->timestamp('updated_at')->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists('search_settings'); + } +}; diff --git a/database/migrations/2026_04_12_105002_create_search_queries_table.php b/database/migrations/2026_04_12_105002_create_search_queries_table.php new file mode 100644 index 00000000..b90da15a --- /dev/null +++ b/database/migrations/2026_04_12_105002_create_search_queries_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('query'); + $table->integer('results_count')->default(0); + $table->unsignedBigInteger('customer_id')->nullable(); + $table->timestamp('created_at')->nullable(); + + $table->index(['store_id', 'created_at'], 'idx_search_queries_store_created'); + $table->index('customer_id', 'idx_search_queries_customer_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('search_queries'); + } +}; diff --git a/database/migrations/2026_04_12_105003_create_products_fts_table.php b/database/migrations/2026_04_12_105003_create_products_fts_table.php new file mode 100644 index 00000000..e0cf85d8 --- /dev/null +++ b/database/migrations/2026_04_12_105003_create_products_fts_table.php @@ -0,0 +1,17 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->string('type'); + $table->string('session_id')->nullable(); + $table->unsignedBigInteger('customer_id')->nullable(); + $table->text('properties_json')->nullable(); + $table->string('client_event_id')->nullable(); + $table->timestamp('occurred_at'); + $table->timestamp('created_at')->nullable(); + + $table->index(['store_id', 'type', 'occurred_at'], 'idx_analytics_events_store_type_at'); + $table->index(['store_id', 'occurred_at'], 'idx_analytics_events_store_at'); + $table->index('session_id', 'idx_analytics_events_session'); + $table->index('customer_id', 'idx_analytics_events_customer'); + }); + } + + public function down(): void + { + Schema::dropIfExists('analytics_events'); + } +}; diff --git a/database/migrations/2026_04_12_106002_create_analytics_daily_table.php b/database/migrations/2026_04_12_106002_create_analytics_daily_table.php new file mode 100644 index 00000000..3971244d --- /dev/null +++ b/database/migrations/2026_04_12_106002_create_analytics_daily_table.php @@ -0,0 +1,32 @@ +foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->date('date'); + $table->integer('orders_count')->default(0); + $table->integer('revenue_amount')->default(0); + $table->integer('aov_amount')->default(0); + $table->integer('visits_count')->default(0); + $table->integer('add_to_cart_count')->default(0); + $table->integer('checkout_started_count')->default(0); + $table->integer('checkout_completed_count')->default(0); + + $table->primary(['store_id', 'date']); + }); + } + + public function down(): void + { + Schema::dropIfExists('analytics_daily'); + } +}; diff --git a/database/migrations/2026_04_12_107001_create_personal_access_tokens_table.php b/database/migrations/2026_04_12_107001_create_personal_access_tokens_table.php new file mode 100644 index 00000000..40ff706e --- /dev/null +++ b/database/migrations/2026_04_12_107001_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/migrations/2026_04_12_107002_create_apps_table.php b/database/migrations/2026_04_12_107002_create_apps_table.php new file mode 100644 index 00000000..a8bc84c5 --- /dev/null +++ b/database/migrations/2026_04_12_107002_create_apps_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->text('scopes_json')->nullable(); + $table->string('type')->default('first_party'); + $table->timestamps(); + }); + + DB::statement("CREATE TRIGGER apps_type_check BEFORE INSERT ON apps FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('first_party','third_party') THEN RAISE(ABORT, 'invalid type') END; END"); + DB::statement("CREATE TRIGGER apps_type_check_update BEFORE UPDATE ON apps FOR EACH ROW BEGIN SELECT CASE WHEN NEW.type NOT IN ('first_party','third_party') THEN RAISE(ABORT, 'invalid type') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS apps_type_check'); + DB::statement('DROP TRIGGER IF EXISTS apps_type_check_update'); + Schema::dropIfExists('apps'); + } +}; diff --git a/database/migrations/2026_04_12_107003_create_app_installations_table.php b/database/migrations/2026_04_12_107003_create_app_installations_table.php new file mode 100644 index 00000000..a22e0c21 --- /dev/null +++ b/database/migrations/2026_04_12_107003_create_app_installations_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->foreignId('app_id') + ->constrained('apps') + ->cascadeOnDelete(); + $table->string('status')->default('active'); + $table->text('settings_json')->nullable(); + $table->timestamp('installed_at')->nullable(); + $table->timestamp('updated_at')->nullable(); + + $table->unique(['store_id', 'app_id'], 'idx_app_installations_store_app'); + $table->index('store_id', 'idx_app_installations_store_id'); + }); + + DB::statement("CREATE TRIGGER app_installations_status_check BEFORE INSERT ON app_installations FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','paused','uninstalled') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER app_installations_status_check_update BEFORE UPDATE ON app_installations FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','paused','uninstalled') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS app_installations_status_check'); + DB::statement('DROP TRIGGER IF EXISTS app_installations_status_check_update'); + Schema::dropIfExists('app_installations'); + } +}; diff --git a/database/migrations/2026_04_12_107004_create_webhook_subscriptions_table.php b/database/migrations/2026_04_12_107004_create_webhook_subscriptions_table.php new file mode 100644 index 00000000..b1992c0f --- /dev/null +++ b/database/migrations/2026_04_12_107004_create_webhook_subscriptions_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('store_id') + ->constrained('stores') + ->cascadeOnDelete(); + $table->foreignId('app_installation_id') + ->nullable() + ->constrained('app_installations') + ->nullOnDelete(); + $table->string('event_type'); + $table->string('url'); + $table->string('secret'); + $table->string('status')->default('active'); + $table->integer('failed_count')->default(0); + $table->timestamps(); + + $table->index(['store_id', 'event_type', 'status'], 'idx_webhook_subs_store_event_status'); + $table->index('app_installation_id', 'idx_webhook_subs_installation'); + }); + + DB::statement("CREATE TRIGGER webhook_subs_status_check BEFORE INSERT ON webhook_subscriptions FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','paused','disabled') THEN RAISE(ABORT, 'invalid status') END; END"); + DB::statement("CREATE TRIGGER webhook_subs_status_check_update BEFORE UPDATE ON webhook_subscriptions FOR EACH ROW BEGIN SELECT CASE WHEN NEW.status NOT IN ('active','paused','disabled') THEN RAISE(ABORT, 'invalid status') END; END"); + } + + public function down(): void + { + DB::statement('DROP TRIGGER IF EXISTS webhook_subs_status_check'); + DB::statement('DROP TRIGGER IF EXISTS webhook_subs_status_check_update'); + Schema::dropIfExists('webhook_subscriptions'); + } +}; diff --git a/database/migrations/2026_04_12_107005_create_webhook_deliveries_table.php b/database/migrations/2026_04_12_107005_create_webhook_deliveries_table.php new file mode 100644 index 00000000..b3917973 --- /dev/null +++ b/database/migrations/2026_04_12_107005_create_webhook_deliveries_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('subscription_id') + ->constrained('webhook_subscriptions') + ->cascadeOnDelete(); + $table->string('event_type'); + $table->text('payload_json'); + $table->integer('response_status')->nullable(); + $table->text('response_body')->nullable(); + $table->integer('attempts')->default(0); + $table->timestamp('delivered_at')->nullable(); + $table->timestamp('created_at')->nullable(); + + $table->index(['subscription_id', 'created_at'], 'idx_webhook_deliveries_sub_created'); + }); + } + + public function down(): void + { + Schema::dropIfExists('webhook_deliveries'); + } +}; diff --git a/database/seeders/AdminUserSeeder.php b/database/seeders/AdminUserSeeder.php new file mode 100644 index 00000000..a0461ad0 --- /dev/null +++ b/database/seeders/AdminUserSeeder.php @@ -0,0 +1,23 @@ +firstOrCreate( + ['email' => 'admin@shop.test'], + [ + 'name' => 'Shop Admin', + 'password' => Hash::make('password'), + 'status' => 'active', + 'email_verified_at' => now(), + ] + ); + } +} diff --git a/database/seeders/CatalogSeeder.php b/database/seeders/CatalogSeeder.php new file mode 100644 index 00000000..0673bbc8 --- /dev/null +++ b/database/seeders/CatalogSeeder.php @@ -0,0 +1,160 @@ +}> */ + private array $products = [ + [ + 'title' => 'Classic Tee', + 'handle' => 'classic-tee', + 'vendor' => 'Demo Brand', + 'type' => 'Apparel', + 'variants' => [ + ['sku' => 'TEE-S', 'price' => 1999], + ['sku' => 'TEE-M', 'price' => 1999], + ['sku' => 'TEE-L', 'price' => 1999], + ], + ], + [ + 'title' => 'Hoodie', + 'handle' => 'hoodie', + 'vendor' => 'Demo Brand', + 'type' => 'Apparel', + 'variants' => [ + ['sku' => 'HOOD-M', 'price' => 4999], + ['sku' => 'HOOD-L', 'price' => 4999], + ], + ], + [ + 'title' => 'Cap', + 'handle' => 'cap', + 'vendor' => 'Demo Brand', + 'type' => 'Accessories', + 'variants' => [ + ['sku' => 'CAP-001', 'price' => 2499], + ], + ], + [ + 'title' => 'Tote Bag', + 'handle' => 'tote-bag', + 'vendor' => 'Demo Brand', + 'type' => 'Accessories', + 'variants' => [ + ['sku' => 'TOTE-001', 'price' => 1499], + ], + ], + [ + 'title' => 'Sneakers', + 'handle' => 'sneakers', + 'vendor' => 'Demo Brand', + 'type' => 'Footwear', + 'variants' => [ + ['sku' => 'SNK-42', 'price' => 7999], + ['sku' => 'SNK-43', 'price' => 7999], + ['sku' => 'SNK-44', 'price' => 7999], + ], + ], + [ + 'title' => 'Mug', + 'handle' => 'mug', + 'vendor' => 'Demo Brand', + 'type' => 'Home', + 'variants' => [ + ['sku' => 'MUG-001', 'price' => 999], + ], + ], + ]; + + public function run(): void + { + /** @var Store $store */ + $store = app('current_store'); + + $createdProducts = []; + + foreach ($this->products as $data) { + $product = Product::withoutGlobalScopes() + ->where('store_id', $store->id) + ->where('handle', $data['handle']) + ->first(); + + if ($product === null) { + $product = Product::create([ + 'store_id' => $store->id, + 'title' => $data['title'], + 'handle' => $data['handle'], + 'status' => 'active', + 'description_html' => '

'.$data['title'].' description.

', + 'vendor' => $data['vendor'], + 'product_type' => $data['type'], + 'tags' => [$data['type'], $data['vendor']], + 'published_at' => now(), + ]); + + foreach ($data['variants'] as $index => $variantData) { + $variant = ProductVariant::create([ + 'product_id' => $product->id, + 'sku' => $variantData['sku'], + 'price_amount' => $variantData['price'], + 'currency' => 'EUR', + 'weight_g' => 250, + 'requires_shipping' => true, + 'is_default' => $index === 0, + 'position' => $index, + 'status' => 'active', + ]); + + InventoryItem::create([ + 'store_id' => $store->id, + 'variant_id' => $variant->id, + 'quantity_on_hand' => 50, + 'quantity_reserved' => 0, + 'policy' => 'deny', + ]); + } + } + + $createdProducts[] = $product; + } + + $featured = Collection::query()->firstOrCreate( + ['store_id' => $store->id, 'handle' => 'featured'], + [ + 'title' => 'Featured', + 'type' => 'manual', + 'status' => 'active', + ] + ); + + $sale = Collection::query()->firstOrCreate( + ['store_id' => $store->id, 'handle' => 'sale'], + [ + 'title' => 'Sale', + 'type' => 'manual', + 'status' => 'active', + ] + ); + + $featuredIds = array_slice(array_map(fn (Product $p): int => $p->id, $createdProducts), 0, 4); + $saleIds = array_slice(array_map(fn (Product $p): int => $p->id, $createdProducts), 2, 4); + + $featured->products()->syncWithoutDetaching(array_combine( + $featuredIds, + array_map(fn (int $position): array => ['position' => $position], array_keys($featuredIds)) + )); + + $sale->products()->syncWithoutDetaching(array_combine( + $saleIds, + array_map(fn (int $position): array => ['position' => $position], array_keys($saleIds)) + )); + } +} diff --git a/database/seeders/ContentSeeder.php b/database/seeders/ContentSeeder.php new file mode 100644 index 00000000..bbca6b6c --- /dev/null +++ b/database/seeders/ContentSeeder.php @@ -0,0 +1,98 @@ + 'about-us', + 'title' => 'About Us', + 'body_html' => '

Welcome

We are a demo store built on the shop platform.

', + ], + [ + 'handle' => 'contact', + 'title' => 'Contact', + 'body_html' => '

Get in touch

Email us at support@shop.test.

', + ], + [ + 'handle' => 'faq', + 'title' => 'FAQ', + 'body_html' => '

Frequently Asked Questions

How do I place an order? Just add products to your cart and check out.

', + ], + ]; + + foreach ($pages as $data) { + Page::query()->firstOrCreate( + ['store_id' => $store->id, 'handle' => $data['handle']], + [ + 'title' => $data['title'], + 'body_html' => $data['body_html'], + 'status' => 'published', + 'published_at' => now(), + ] + ); + } + + $menu = NavigationMenu::query()->firstOrCreate( + ['store_id' => $store->id, 'handle' => 'main-menu'], + ['title' => 'Main Menu'] + ); + + $items = [ + ['label' => 'Home', 'type' => 'link', 'url' => '/', 'resource_id' => null], + ['label' => 'Collections', 'type' => 'link', 'url' => '/collections', 'resource_id' => null], + ['label' => 'About', 'type' => 'link', 'url' => '/pages/about-us', 'resource_id' => null], + ['label' => 'Contact', 'type' => 'link', 'url' => '/pages/contact', 'resource_id' => null], + ]; + + foreach ($items as $position => $item) { + $exists = $menu->items()->where('label', $item['label'])->exists(); + + if (! $exists) { + NavigationItem::create([ + 'menu_id' => $menu->id, + 'type' => $item['type'], + 'label' => $item['label'], + 'url' => $item['url'], + 'resource_id' => $item['resource_id'], + 'position' => $position, + ]); + } + } + + Discount::query()->firstOrCreate( + ['store_id' => $store->id, 'code' => 'WELCOME10'], + [ + 'type' => 'code', + 'value_type' => 'percent', + 'value_amount' => 10, + 'status' => 'active', + 'usage_count' => 0, + ] + ); + + Discount::query()->firstOrCreate( + ['store_id' => $store->id, 'code' => 'FREESHIP'], + [ + 'type' => 'code', + 'value_type' => 'free_shipping', + 'value_amount' => 0, + 'status' => 'active', + 'usage_count' => 0, + ] + ); + } +} diff --git a/database/seeders/CustomersAndOrdersSeeder.php b/database/seeders/CustomersAndOrdersSeeder.php new file mode 100644 index 00000000..e0705cad --- /dev/null +++ b/database/seeders/CustomersAndOrdersSeeder.php @@ -0,0 +1,107 @@ + 'alice@shop.test', 'name' => 'Alice Example'], + ['email' => 'bob@shop.test', 'name' => 'Bob Example'], + ['email' => 'carol@shop.test', 'name' => 'Carol Example'], + ['email' => 'dan@shop.test', 'name' => 'Dan Example'], + ['email' => 'eve@shop.test', 'name' => 'Eve Example'], + ]; + + $customers = []; + + foreach ($customerData as $data) { + $customers[] = Customer::query()->firstOrCreate( + ['store_id' => $store->id, 'email' => $data['email']], + [ + 'name' => $data['name'], + 'password_hash' => Hash::make('password'), + 'marketing_opt_in' => false, + ] + ); + } + + $products = Product::query()->where('store_id', $store->id)->with('variants')->take(3)->get(); + + if ($products->isEmpty()) { + return; + } + + $orderStates = [ + ['number' => 'D-1001', 'status' => 'pending', 'financial' => 'pending', 'fulfillment' => 'unfulfilled'], + ['number' => 'D-1002', 'status' => 'fulfilled', 'financial' => 'paid', 'fulfillment' => 'fulfilled'], + ['number' => 'D-1003', 'status' => 'refunded', 'financial' => 'refunded', 'fulfillment' => 'fulfilled'], + ]; + + foreach ($orderStates as $index => $state) { + $existing = Order::query() + ->where('store_id', $store->id) + ->where('order_number', $state['number']) + ->first(); + + if ($existing !== null) { + continue; + } + + $customer = $customers[$index]; + $product = $products[$index % $products->count()]; + $variant = $product->variants->first(); + + if ($variant === null) { + continue; + } + + $unitPrice = (int) $variant->price_amount; + $quantity = 1; + $lineTotal = $unitPrice * $quantity; + $shipping = 599; + $total = $lineTotal + $shipping; + + $order = Order::create([ + 'store_id' => $store->id, + 'customer_id' => $customer->id, + 'order_number' => $state['number'], + 'payment_method' => 'credit_card', + 'status' => $state['status'], + 'financial_status' => $state['financial'], + 'fulfillment_status' => $state['fulfillment'], + 'currency' => 'EUR', + 'subtotal_amount' => $lineTotal, + 'discount_amount' => 0, + 'shipping_amount' => $shipping, + 'tax_amount' => 0, + 'total_amount' => $total, + 'email' => $customer->email, + 'placed_at' => now()->subDays($index + 1), + ]); + + OrderLine::create([ + 'order_id' => $order->id, + 'product_id' => $product->id, + 'variant_id' => $variant->id, + 'title_snapshot' => $product->title, + 'sku_snapshot' => $variant->sku, + 'quantity' => $quantity, + 'unit_price_amount' => $unitPrice, + 'total_amount' => $lineTotal, + ]); + } + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef2..be7c0009 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,22 +2,18 @@ namespace Database\Seeders; -use App\Models\User; -// use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { - /** - * Seed the application's database. - */ public function run(): void { - // User::factory(10)->create(); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $this->call([ + AdminUserSeeder::class, + DemoStoreSeeder::class, + CatalogSeeder::class, + ContentSeeder::class, + CustomersAndOrdersSeeder::class, ]); } } diff --git a/database/seeders/DemoStoreSeeder.php b/database/seeders/DemoStoreSeeder.php new file mode 100644 index 00000000..e86da081 --- /dev/null +++ b/database/seeders/DemoStoreSeeder.php @@ -0,0 +1,124 @@ +firstOrCreate( + ['billing_email' => 'demo@shop.test'], + ['name' => 'Demo Org'] + ); + + $store = Store::query()->firstOrCreate( + ['handle' => 'demo'], + [ + 'organization_id' => $organization->id, + 'name' => 'Demo Store', + 'status' => 'active', + 'default_currency' => 'EUR', + 'default_locale' => 'en', + 'timezone' => 'Europe/Berlin', + ] + ); + + app()->instance('current_store', $store); + + StoreDomain::query()->firstOrCreate( + ['hostname' => 'shop.test'], + [ + 'store_id' => $store->id, + 'type' => 'storefront', + 'is_primary' => true, + 'tls_mode' => 'managed', + 'created_at' => now(), + ] + ); + + $admin = User::query()->where('email', 'admin@shop.test')->first(); + if ($admin !== null) { + $exists = DB::table('store_users') + ->where('store_id', $store->id) + ->where('user_id', $admin->id) + ->exists(); + + if (! $exists) { + DB::table('store_users')->insert([ + 'store_id' => $store->id, + 'user_id' => $admin->id, + 'role' => 'owner', + 'created_at' => now(), + ]); + } + } + + StoreSettings::query()->updateOrCreate( + ['store_id' => $store->id], + ['settings_json' => [ + 'support_email' => 'support@shop.test', + 'brand_color' => '#18181b', + ]] + ); + + $theme = Theme::query()->firstOrCreate( + ['store_id' => $store->id, 'name' => 'Default Theme'], + [ + 'version' => '1.0.0', + 'status' => 'published', + 'published_at' => now(), + ] + ); + + ThemeSettings::query()->updateOrCreate( + ['theme_id' => $theme->id], + ['settings_json' => [ + 'primary_color' => '#18181b', + 'accent_color' => '#059669', + 'font_family' => 'Inter', + ]] + ); + + $zone = ShippingZone::query()->firstOrCreate( + ['store_id' => $store->id, 'name' => 'Europe'], + [ + 'countries_json' => ['DE', 'AT', 'CH'], + 'regions_json' => [], + ] + ); + + ShippingRate::query()->firstOrCreate( + ['zone_id' => $zone->id, 'name' => 'Standard'], + [ + 'type' => 'flat', + 'config_json' => ['amount' => 599], + 'is_active' => true, + ] + ); + + TaxSettings::query()->updateOrCreate( + ['store_id' => $store->id], + [ + 'mode' => 'manual', + 'prices_include_tax' => true, + 'config_json' => [ + 'name' => 'VAT', + 'rate_basis_points' => 1900, + ], + ] + ); + } +} diff --git a/report/all.html b/report/all.html new file mode 100644 index 00000000..ffe7d3e6 --- /dev/null +++ b/report/all.html @@ -0,0 +1,7386 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nameabstractfinalmethodsnbMethodsIncludingGettersSettersnbMethodsnbMethodsPrivatenbMethodsPublicnbMethodsGetternbMethodsSetterswmcccnccnMethodMaxexternalsparentsimplementslcomlengthvocabularyvolumedifficultyeffortlevelbugstimeintelligentContentnumber_operatorsnumber_operandsnumber_operators_uniquenumber_operands_uniquecloclocllocmimIwoCcommentWeightkanDefectrelativeStructuralComplexityrelativeDataComplexityrelativeSystemComplexitytotalStructuralComplexitytotalDataComplexitytotalSystemComplexitypackagepageRankafferentCouplingefferentCouplinginstabilityviolations
App\Auth\CustomerUserProvider1010103700292081101315833797.0119.8815840.660.050.2788040.152106924161099362.0234.0527.971.014840.63484.6348406.264846.26App\Auth\0170.88
App\Providers\AppServiceProvider66642007221310226161041.71178.290.580.031060.6722421413534085.3750.6634.710.155290.04529.0431740.253174.25App\Providers\00101
App\Providers\FortifyServiceProvider5553200511171022818116.762.3268.540.430.041550.7652331516543888.2750.9337.340.154840.09484.0924200.432420.43App\Providers\0051
App\Models\OrderLine55514005115103241388.810.7970.311.260.034112.1851911216432795.525540.520.1541.675.67208.3328.33App\Models\0130.75
App\Models\WebhookSubscription22202002113102131144.970.5524.731.820.01181.7721111072215100.9862.6438.340.1540.674.6781.339.33App\Models\0.02330.5
App\Models\ThemeFile22211002112102108300.5717.141.750.01152.5281772215102.2163.8738.340.15112224App\Models\0021
App\Models\ProductOption222020021131027516.250.6310.161.60.01126.01251472215104.0765.7338.340.1540.674.6781.339.33App\Models\0031
App\Models\NavigationItem3331200422210330161202.65318.460.380.041845.227233137312488.6455.0633.570.1511.52.534.57.5App\Models\0.01220.5
App\Models\Refund33312003113102128360.6423.141.560.011563917102919100.5461.0739.470.1511.52.534.57.5App\Models\0.01220.5
App\Models\InventoryItem3331200311210316950.721.7186.950.580.02529.59412277261996.0360.03360.1511.52.534.57.5App\Models\0.01420.33
App\Models\NavigationMenu111010011121017719.650.59.8320.01139.3161641410105.836936.830.1540.334.3340.334.33App\Models\0.01220.5
App\Models\App2221100211210211934.870.5619.611.780.01161.99291872215101.7563.4138.340.15112224App\Models\0120.67
App\Models\CartLine2220200211310112938.040.6323.771.60.01160.862101872215101.4963.1538.340.15112224App\Models\0120.67
App\Models\AppInstallation33312003113103161155.350.6535.981.540.02285.1631311010312197.3558.8238.530.1541512315App\Models\0.01130.75
App\Models\Cart44413004113103181264.532.17139.810.460.02829.78513391032229657.9138.090.1511.52.54610App\Models\0.04620.25
App\Models\Discount222110098811025022222.975.811296.020.170.077238.3619316164312777.6751.2626.410.434268412App\Models\0.01310.25
App\Models\Product55514005115103251597.670.7169.771.40.034136.7452011416422695.9255.0740.850.1591.2510.25456.2551.25App\Models\0.041030.23
App\Models\Order9991800135561036531322.025.761854.850.170.1110355.91174862519695081.0244.7136.320.52361.5737.5732414.14338.14App\Models\0.12030.13
App\Models\Store55514005115105191165.730.746.011.430.02393.951411016422697.1256.2740.850.15360.7136.711803.57183.57App\Models\0.061750.23
App\Models\StoreDomain22211002112102118330.6421.211.560.01151.33291772215101.9263.5838.340.15112224App\Models\0120.67
App\Models\Theme33312003113103138390.7127.861.40.01254.631017102818101.361.3439.960.1541512315App\Models\0.01230.6
App\Models\ProductMedia22211002112102161359.210.5834.541.710.022101.52141127231698.9161.1937.720.15112224App\Models\0.01120.67
App\Models\User444130074461035529267.193.36897.750.30.095079.52134242524563292.0949.6442.450.291690.45169.456761.79677.79App\Models\0.02450.56
App\Models\WebhookDelivery22211002112102161257.360.6436.51.570.02290.14214111723169961.2937.720.15112224App\Models\0.01120.67
App\Models\Fulfillment33312003113103171056.470.7843.921.290.02272.613141910291999.1759.739.470.1541512315App\Models\0.02230.6
App\Models\ThemeSettings2221100211210210728.070.6718.721.50.01142.1128167261997.8361.83360.15112224App\Models\0021
App\Models\Checkout222110021121022717110.360.7886.221.280.045141.262251167211499.5560.5638.990.15112224App\Models\0.02720.22
App\Models\Payment33312003113103171158.810.741.171.430.02284.0131411010291999.0559.5839.470.1541512315App\Models\0.01430.43
App\Models\AnalyticsDaily333300031111022913107.311.08116.260.920.04699.0632611215382397.2855.9441.340.15160.7316.73482.250.2App\Models\0.01110.5
App\Models\Customer77615106114104251289.620.8273.331.220.034109.5471811116513590.6652.5138.140.1513.54.5724.531.5App\Models\0.01420.33
App\Models\ProductVariant444130041141042717110.360.7279.321.390.044153.5542311613352296.7856.2840.50.15911036440App\Models\0140.8
App\Models\ProductOptionValue111010011121016513.930.638.711.60022.29151441511105.0169.1435.870.1510.51.510.51.5App\Models\0021
App\Models\TaxSettings22211002112102161155.350.738.751.430.02279.072141107261995.7759.77360.15112224App\Models\0.01320.4
App\Models\ShippingZone22211002112102128360.7125.711.40.01150.42101772114102.9663.9738.990.15112224App\Models\0.01220.5
App\Models\Collection22211002112102131043.190.6126.391.640.01170.672111972114102.4163.4138.990.1540.674.6781.339.33App\Models\0.01520.29
App\Models\Page11110001111101108300.6419.291.560.01146.67191741410104.5467.7136.830.15011011App\Models\0.01310.25
App\Models\ShippingRate2221100211210214944.380.7533.281.330.01259.172121872114102.3263.3338.990.15112224App\Models\0.01420.33
App\Models\StoreSettings222110021121029623.260.716.291.430.01133.2427157251899.4662.9136.550.15112224App\Models\0021
App\Models\CustomerAddress22211002112102118330.6421.211.560.01151.33291772215101.9263.5838.340.15112224App\Models\0.01120.67
App\Models\StoreUser2222000322210210625.85251.70.50.01312.922824323208861.4626.540.2290.259.25180.518.5App\Models\0021
App\Models\FulfillmentLine222020021131017516.250.6310.161.60.01126.01251472215104.0765.7338.340.15112224App\Models\0.01120.67
App\Models\AnalyticsEvent11110001111101131144.970.626.981.670.01174.951121104161299.7264.7534.970.15011011App\Models\0.01210.33
App\Models\Scopes\StoreScope1110100222301113941.213.6148.350.280.01811.4549451131285.7164.8820.830.2290.759.7590.759.75App\Models\Scopes\0.01130.75
App\Models\Organization111010011121014480.5420016131341410108.5671.7336.830.1510.51.510.51.5App\Models\0021
App\Models\Concerns\BelongsToStore12220200433400214944.385221.890.20.01128.8868544221891.3660.6830.680.22160.216.2320.432.4App\Models\Concerns\0031
App\Exceptions\InsufficientInventoryException00000000101100000000000000004417117100.15000000App\Exceptions\0110.5
App\Exceptions\FulfillmentGuardException00000000101100000000000000004417117100.15000000App\Exceptions\0110.5
App\Exceptions\InvalidDiscountException777070082211072617106.271.27134.610.790.04783.97192150323252.7152.7100.1513.144.1472229App\Exceptions\0.01110.5
App\Exceptions\PaymentFailedException00000000101100000000000000004417117100.15000000App\Exceptions\0110.5
App\Policies\StorePolicy3330300311600320756.159.33524.040.110.02296.02614430181860.2360.2300.1512.53.537.510.5App\Policies\0021
App\Policies\Concerns\ChecksStoreRole122220004323001221176.115.83443.960.170.032513.05814561181777.4459.5817.850.2291.1310.13182.2520.25App\Policies\Concerns\0021
App\Livewire\Settings\TwoFactor99918001793101019531470.656.923258.340.140.1618167.982372526321168474.4438.136.340.571440.34144.3412963.081299.08App\Livewire\Settings\0061
App\Livewire\Settings\DeleteUserForm111010011131018518.58001.250.01023.2208053151299.3767.4431.940.15250.1725.17250.1725.17App\Livewire\Settings\0031
App\Livewire\Settings\TwoFactor\RecoveryCodes3331200644210119960.235.2313.190.190.021711.586134510362692.5756.1336.440.22160.0716.07480.248.2App\Livewire\Settings\TwoFactor\0021
App\Livewire\Settings\Password11101002222101261086.373259.110.330.031428.79224283232084.3357.7926.540.154904949049App\Livewire\Settings\0021
App\Livewire\Settings\Profile55505001063111053914148.496.941031.160.140.055721.3814255911524181.4948.8132.690.291440.23144.237201.15721.15App\Livewire\Settings\0031
App\Livewire\Settings\Appearance000000001011000000000000000154202.9417131.940.15000000App\Livewire\Settings\0011
App\Livewire\Storefront\Products\Show665050195341018431416.1510.784487.210.090.1424938.5922628231535254.1143.5510.560.292560.2256.215361.181537.18App\Livewire\Storefront\Products\0041
App\Livewire\Storefront\Home44422008537102251495.182.8266.510.360.031533.9911144107383183.7952.9430.850.431440.38144.385761.54577.54App\Livewire\Storefront\0061
App\Livewire\Storefront\Checkout\Show1111114700352599101300871932.8818.435565.050.050.641976105.057023012751413512152.1228.1923.921.019000.27900.2799002.949902.94App\Livewire\Storefront\Checkout\0061Probably bugged,
App\Livewire\Storefront\Checkout\Confirmation22202002113101141046.512.36109.620.420.02619.73311371161581.4262.5318.880.15250.2525.25500.550.5App\Livewire\Storefront\Checkout\0031
App\Livewire\Storefront\Search\Index33303004223102221278.873236.610.330.031326.29418392222080.5858.0722.510.15360.1436.141080.43108.43App\Livewire\Storefront\Search\0031
App\Livewire\Storefront\CartDrawer333030053331023415132.836.5863.420.150.044820.448265102292773.2953.5119.790.4540.334.3312113App\Livewire\Storefront\0031
App\Livewire\Storefront\Cart\Show5550500106351026323284.9810.2829300.10.0916327.7216477161525155.4144.7610.660.59490.4549.452452.25247.25App\Livewire\Storefront\Cart\0031
App\Livewire\Storefront\Account\Dashboard22202002114102171056.471.7598.830.570.02532.27314282171587.2861.9425.340.15640.1164.111280.22128.22App\Livewire\Storefront\Account\0041
App\Livewire\Storefront\Account\Auth\Login333030064361024019169.925.71970.950.180.065429.748325141333264.3351.0113.320.291690.14169.145070.43507.43App\Livewire\Storefront\Account\Auth\0051
App\Livewire\Storefront\Account\Auth\Register333030042261024521197.653.42675.320.290.073857.854413182323070.3251.4318.880.221210.08121.083630.25363.25App\Livewire\Storefront\Account\Auth\0051
App\Livewire\Storefront\Account\Addresses\Index55505006221210510135518.064.312234.120.230.17124120.139923325524767.3644.2523.110.221690.1169.18450.5845.5App\Livewire\Storefront\Account\Addresses\0041
App\Livewire\Storefront\Account\Orders\Index22202002114102151049.831.574.740.670.02433.22312282171587.6662.3225.340.15490.1349.13980.2598.25App\Livewire\Storefront\Account\Orders\0041
App\Livewire\Storefront\Account\Orders\Show22202002114101191370.312.25158.190.440.02931.254153102181685.3560.6724.690.15640.1764.171280.33128.33App\Livewire\Storefront\Account\Orders\0041
App\Livewire\Storefront\Collections\Index2220200211310211934.871.2944.830.780.01227.1229271151483.5364.0619.470.15360.1436.14720.2972.29App\Livewire\Storefront\Collections\0031
App\Livewire\Storefront\Collections\Show333030053331024120177.24.13730.950.240.064142.968334162323070.5251.6318.880.221210.11121.113630.33363.33App\Livewire\Storefront\Collections\0031
App\Livewire\Storefront\Pages\Show2220200211310116950.721.8694.190.540.02527.31313271171680.0161.6618.350.15250.2525.25500.550.5App\Livewire\Storefront\Pages\0031
App\Livewire\Storefront\Concerns\EnsuresStore11111000333300112733.694.67157.210.210.0197.2257432151391.464.626.80.22250.1725.17250.1725.17App\Livewire\Storefront\Concerns\0011
App\Livewire\Admin\Customers\Index222020021131023619152.933.87591.310.260.053339.557294152222078.756.1922.510.151000.09100.092000.18200.18App\Livewire\Admin\Customers\0031
App\Livewire\Admin\Customers\Show222020032231013519148.683.87574.890.260.053238.456294151161577.7558.8718.880.15250.2525.25500.550.5App\Livewire\Admin\Customers\0031
App\Livewire\Admin\Settings\Taxes333030075541026128293.252.88844.560.350.147101.8213483256352980.0750.1529.920.15250.1725.17750.575.5App\Livewire\Admin\Settings\0031
App\Livewire\Admin\Settings\Index333030031121024522200.671.9381.280.530.0721105.627382207342784.8452.5232.320.1590.259.25270.7527.75App\Livewire\Admin\Settings\0021
App\Livewire\Admin\Settings\Shipping7770700822710410344562.324.352446.10.230.19136129.2716874407686165.3741.5323.840.221960.16196.1613721.131373.13App\Livewire\Admin\Settings\0041
App\Livewire\Admin\Dashboard444130052261035028240.373.25781.20.310.084373.9611394249393084.7150.8433.870.151210.33121.334841.33485.33App\Livewire\Admin\0051
App\Livewire\Admin\Products\Index555050073251025620242.0392178.250.110.0812126.8914426144423871.4548.44230.291690.21169.218451.07846.07App\Livewire\Admin\Products\0031
App\Livewire\Admin\Products\Form33303002220114102181471005.3811.911964.030.080.3466584.494513674012746266.437.1929.210.51440.21144.214320.62432.62App\Livewire\Admin\Products\0041Too complex method code,
App\Livewire\Admin\Auth\Login3331200644121026030294.415.631656.080.180.19252.3415456243403768.5547.9720.580.362560.18256.187680.53768.53App\Livewire\Admin\Auth\0061
App\Livewire\Admin\Navigation\Index888080014761110515152860.779.648301.610.10.2946189.25271247457787160.5138.1322.380.297290.13729.1358321.045833.04App\Livewire\Admin\Navigation\0051Blob / God object,
App\Livewire\Admin\Discounts\Index33303003113102271296.793.83371.040.260.032125.25423393242183.1657.1226.040.15490.1349.131470.38147.38App\Livewire\Admin\Discounts\0031
App\Livewire\Admin\Discounts\Form333030014128310213246729.1110.217444.60.10.2441471.41359783811675669.5740.2129.360.43360.4836.481081.43109.43App\Livewire\Admin\Discounts\0031
App\Livewire\Admin\Orders\Index555050051131026221272.326.121665.980.160.099344.5110524175403575.1749.1326.040.151000.09100.095000.45500.45App\Livewire\Admin\Orders\0031
App\Livewire\Admin\Orders\Show1010100100022136910215940846.1913.8911756.860.070.2865360.92813173321019945.0334.2210.810.522890.22289.2228902.172892.17App\Livewire\Admin\Orders\0061
App\Livewire\Admin\Collections\Index22202002113102251597.673.64355.170.280.032026.865204112181684.3559.6724.690.15490.1349.13980.2598.25App\Livewire\Admin\Collections\0031
App\Livewire\Admin\Collections\Form555050018148810117745972.0614.9214502.050.070.3280665.16391388378706263.138.125.010.593240.14324.1416200.681620.68App\Livewire\Admin\Collections\0051
App\Livewire\Admin\Pages\Index333030031141033219135.933.47471.240.290.052639.216264152242277.2755.6421.620.15810.1381.132430.4243.4App\Livewire\Admin\Pages\0031
App\Livewire\Admin\Pages\Form3330300151310610212634641.0212.317894.050.080.2143952.0531957277524569.4542.5326.910.36360.3336.331081109App\Livewire\Admin\Pages\0041
App\Livewire\Admin\Apps\Index333030031161037124325.535.91920.640.170.1110755.1712594204332976.0550.3725.680.152560.16256.167680.47768.47App\Livewire\Admin\Apps\0041
App\Livewire\Admin\Themes\Index444040052291046124279.686.711876.820.150.0910441.681051519338357048.9221.080.221690.2169.26760.79676.79App\Livewire\Admin\Themes\0041
App\Livewire\Admin\Analytics\Index222020043331016229301.195.421631.470.180.19155.6110525242211977.3554.34230.15250.2525.25500.550.5App\Livewire\Admin\Analytics\0031
App\Livewire\Admin\Developers\Index555050051191047835400.082.03812.290.490.1345197.0611672338524474.3445.828.550.152890.08289.0814450.391445.39App\Livewire\Admin\Developers\0051
App\Livewire\Actions\Logout11101001113001334.750.52.382009.51121231411105.2772.4132.860.15160.216.2160.216.2App\Livewire\Actions\0.01120.67
App\Support\HandleGenerator222110054320025523248.88.752176.960.110.0812128.4315407160262651.8251.8200.52490.7549.75981.599.5App\Support\0.01320.4
App\Support\CartSession333030097450034913181.3215.172750.040.070.0615311.962326760343449.8449.8400.43490.6749.671472149App\Support\0.02430.43
App\Http\Middleware\ResolveStore33321001085120018326390.146.192415.130.160.1313463.0231525210525243.3543.3500.643240.56324.569721.68973.68App\Http\Middleware\0091
App\Http\Controllers\Controller1000000001000000000000000000154202.9417131.940.15000000App\Http\Controllers\0000
App\Actions\Fortify\ResetUserPassword111010011130119418000.890.01016090451510108.2569.2638.990.15250.3325.33250.3325.33App\Actions\Fortify\0031
App\Actions\Fortify\CreateNewUser1110100111401115638.771.454.280.710.01327.71141551510105.9266.9338.990.15250.3325.33250.3325.33App\Actions\Fortify\0031
App\Jobs\ExpireAbandonedCheckouts111010011130119520.9001.110.01023.2209050111167.967.900.15640.1164.11640.1164.11App\Jobs\0031
App\Jobs\CleanupAbandonedCarts111010011120115410001.60016050409972.0572.0500.153603636036App\Jobs\0021
App\Jobs\AggregateAnalytics2220200433701210040532.194.672483.570.210.18138114.0416844360272749.2849.2800.156760.02676.0213520.041352.04App\Jobs\0061
App\Jobs\CancelUnpaidBankTransferOrders1110100111301111833001.450.01048011080111166.5266.5200.15640.1164.11640.1164.11App\Jobs\0031
App\Jobs\ProcessMediaUpload222020021130129623.260.818.611.250.01129.0818150141465.2965.2900.1540.174.1780.338.33App\Jobs\0031
App\Jobs\DeliverWebhook33312001086801210853618.6211.397047.80.090.2139254.3218711427514470.6843.5327.150.363610.18361.1810830.551083.55App\Jobs\0.01170.88
App\Events\OrderRefunded11101001112001222002004020208878.0678.0600.15022022App\Events\0120.67
App\Events\OrderCancelled11101001111001110002000010108817117100.15011011App\Events\0110.5
App\Events\OrderCreated11101001111001110002000010108817117100.15011011App\Events\0.01210.33
App\Events\OrderPaid11101001111001110002000010108817117100.15011011App\Events\0.01310.25
App\Events\FulfillmentDelivered11101001111001110002000010108817117100.15011011App\Events\0110.5
App\Events\OrderFulfilled11101001111001110002000010108817117100.15011011App\Events\0.01210.33
App\Observers\ProductObserver4440400411400210315.85000.60.0109.51010030191963.5763.5700.1540.334.33161.3317.33App\Observers\0021
App\Listeners\DispatchOrderWebhooks55514005115002261293.21000.920.03086.040260120232356.3756.3700.1510.61.6538App\Listeners\0051
App\Services\WebhookService333030042230023217130.83392.40.330.042243.64283143221984.0857.0227.070.38360.6736.671082110App\Services\0.01230.6
App\Services\OrderService444040028251223003237751476.2314.3121121.450.070.491173103.185118610652817945.133.0512.051.410890.131089.1343560.54356.5App\Services\0.013130.81Too complex method code,Probably bugged,
App\Services\Payments\MockPaymentProvider4442200854150126929335.24.41474.880.230.118276.1814554256413575.8947.9627.930.1591.510.536642App\Services\Payments\0071
App\Services\CheckoutService12121239004130837003312591835.3815.2828042.560.070.611558120.13852277521017016043.3825.0318.352.1212250.351225.35147004.2514704.25App\Services\0.011160.94Blob / God object,Probably bugged,
App\Services\FulfillmentService4441300171471300311737609.5112.557650.350.080.242548.5626918298564869.5741.9427.630.661960.32196.327841.27785.27App\Services\0.01180.89
App\Services\TaxCalculator33303001210620028830431.8114.256153.240.070.1434230.3315710204343073.3247.9825.340.3614.175.17312.515.5App\Services\0120.67
App\Services\ThemeSettingsService333030053350023521153.734.06624.530.250.053537.849265169322391.1954.5836.610.22810.4781.472431.4244.4App\Services\0031
App\Services\InventoryService555050084411005781631239.2112234.860.030.16807.961761970454545.9345.9300.22250.525.51252.5127.5App\Services\0.01340.57
App\Services\NavigationService33303003115002271399.911.04104.070.960.03695.922251125221792.6859.0233.660.15640.3364.331921193App\Services\0031
App\Services\RefundService2220200111010100029237479.2714.66995.490.070.1638932.84236911261403957.345.1812.130.731690.39169.393380.79338.79App\Services\0190.9
App\Services\ProductService666150038331621004308571796.5316.830181.710.060.61677106.94682407501012911948.427.520.929000.18900.1854001.15401.1App\Services\0.01170.88Too complex method code,Probably bugged,
App\Services\ShippingCalculator44422003936127002213531220.0522.9127954.330.040.41155353.2572141134012867460.1232.7727.351.492890.72289.7211562.891158.89App\Services\0150.83Too complex method code,Probably bugged,
App\Services\AnalyticsService22202003226002311612422480.50.04146232821461812100.5261.5338.990.15360.8636.86721.7173.71App\Services\0.01140.8
App\Services\CartService888170027201026003295451620.136.9159796.30.030.54332243.89922031233013013028.7228.7201.297290.37729.3758322.935834.93App\Services\0071Probably bugged,
App\Services\PricingEngine222020017161611002191491072.4116.54177360.060.3698564.8462129103905959383800.641960.27196.273920.53392.53App\Services\0190.9Too complex method code,Probably bugged,
App\Services\DiscountService33303002321151900316542889.7332.6229018.960.030.3161227.285910616263807750.1535.3714.781.224840.32484.3214520.961452.96App\Services\0160.86Too complex method code,
App\Services\SearchService5551400191571300213945763.3712.59542.090.080.2553061.07391009367595265.840.3625.430.573610.55361.5518052.751807.75App\Services\0.01150.83
App\Services\VariantMatrixService3332100151310200112239644.8210.066488.490.10.2136064.0830927327706362.8639.3323.531.812890.22289.228670.67867.67App\Services\0120.67
App\Concerns\ProfileValidationRules13333000422200120963.42.29144.910.440.02827.7441627153116104.8960.8544.040.15160.7316.73482.250.2App\Concerns\0011
App\Concerns\PasswordValidationRules1222200021110028518.580.7513.931.330.01124.772614102212110.6767.4443.230.15112224App\Concerns\0011
App\ValueObjects\PaymentResult144404004111002148421.33560.750.01331.568260191960.660.600.1504.254.2501717App\ValueObjects\0.01210.33
App\ValueObjects\DiscountResult111101001110001334.75002009.5103033107114.2176.6937.520.15033033App\ValueObjects\0.01100
App\ValueObjects\PricingResult1222020021100022714102.81102.810.036102.8126113152611109.2363.0646.170.1512.53.5257App\ValueObjects\0.01100
App\ValueObjects\TaxLine12220200211000210523.221.1326.120.890.01120.64191431411100.4567.5832.860.1502.52.5055App\ValueObjects\0.01200
App\ValueObjects\RefundResult1222020021110027719.65119.6510.01119.6525250111168.0968.0900.15033066App\ValueObjects\0.01210.33
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + diff --git a/report/classes.js b/report/classes.js new file mode 100644 index 00000000..676ccbdc --- /dev/null +++ b/report/classes.js @@ -0,0 +1,11646 @@ +var classes = [ + { + "name": "App\\Auth\\CustomerUserProvider", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "retrieveById", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "retrieveByToken", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updateRememberToken", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "retrieveByCredentials", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "validateCredentials", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "rehashPasswordIfRequired", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "createModel", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "newModelQuery", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "constrainToCurrentStore", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 10, + "nbMethods": 10, + "nbMethodsPrivate": 3, + "nbMethodsPublic": 7, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 29, + "ccn": 20, + "ccnMethodMax": 8, + "externals": [ + "Illuminate\\Contracts\\Auth\\UserProvider", + "Illuminate\\Contracts\\Hashing\\Hasher", + "Illuminate\\Contracts\\Auth\\Authenticatable", + "Illuminate\\Support\\Str", + "Illuminate\\Support\\Str", + "Illuminate\\Contracts\\Auth\\Authenticatable", + "Illuminate\\Contracts\\Auth\\Authenticatable", + "Illuminate\\Database\\Eloquent\\Model", + "class", + "Illuminate\\Database\\Eloquent\\Builder", + "Illuminate\\Database\\Eloquent\\Builder" + ], + "parents": [], + "implements": [ + "Illuminate\\Contracts\\Auth\\UserProvider" + ], + "lcom": 3, + "length": 158, + "vocabulary": 33, + "volume": 797.01, + "difficulty": 19.88, + "effort": 15840.66, + "level": 0.05, + "bugs": 0.27, + "time": 880, + "intelligentContent": 40.1, + "number_operators": 52, + "number_operands": 106, + "number_operators_unique": 9, + "number_operands_unique": 24, + "cloc": 16, + "loc": 109, + "lloc": 93, + "mi": 62.02, + "mIwoC": 34.05, + "commentWeight": 27.97, + "kanDefect": 1.01, + "relativeStructuralComplexity": 484, + "relativeDataComplexity": 0.63, + "relativeSystemComplexity": 484.63, + "totalStructuralComplexity": 4840, + "totalDataComplexity": 6.26, + "totalSystemComplexity": 4846.26, + "package": "App\\Auth\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 7, + "instability": 0.88, + "violations": {} + }, + { + "name": "App\\Providers\\AppServiceProvider", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "register", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "boot", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "configureWebhookListeners", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "configureRateLimiters", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "configureCustomerAuthProvider", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "configureDefaults", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 6, + "nbMethods": 6, + "nbMethodsPrivate": 4, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 7, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Illuminate\\Support\\ServiceProvider", + "App\\Models\\Product", + "Illuminate\\Support\\Facades\\Event", + "Illuminate\\Support\\Facades\\Event", + "Illuminate\\Support\\Facades\\Event", + "Illuminate\\Cache\\RateLimiting\\Limit", + "Illuminate\\Support\\Facades\\RateLimiter", + "App\\Auth\\CustomerUserProvider", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Date", + "Illuminate\\Support\\Facades\\DB", + "Illuminate\\Validation\\Rules\\Password", + "Illuminate\\Validation\\Rules\\Password" + ], + "parents": [ + "Illuminate\\Support\\ServiceProvider" + ], + "implements": [], + "lcom": 2, + "length": 26, + "vocabulary": 16, + "volume": 104, + "difficulty": 1.71, + "effort": 178.29, + "level": 0.58, + "bugs": 0.03, + "time": 10, + "intelligentContent": 60.67, + "number_operators": 2, + "number_operands": 24, + "number_operators_unique": 2, + "number_operands_unique": 14, + "cloc": 13, + "loc": 53, + "lloc": 40, + "mi": 85.37, + "mIwoC": 50.66, + "commentWeight": 34.71, + "kanDefect": 0.15, + "relativeStructuralComplexity": 529, + "relativeDataComplexity": 0.04, + "relativeSystemComplexity": 529.04, + "totalStructuralComplexity": 3174, + "totalDataComplexity": 0.25, + "totalSystemComplexity": 3174.25, + "package": "App\\Providers\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 10, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Providers\\FortifyServiceProvider", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "register", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "boot", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "configureActions", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "configureViews", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "configureRateLimiting", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 3, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Support\\ServiceProvider", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Fortify", + "Illuminate\\Cache\\RateLimiting\\Limit", + "Illuminate\\Support\\Facades\\RateLimiter", + "Laravel\\Fortify\\Fortify", + "Illuminate\\Support\\Str", + "Illuminate\\Support\\Str", + "Illuminate\\Cache\\RateLimiting\\Limit", + "Illuminate\\Support\\Facades\\RateLimiter" + ], + "parents": [ + "Illuminate\\Support\\ServiceProvider" + ], + "implements": [], + "lcom": 2, + "length": 28, + "vocabulary": 18, + "volume": 116.76, + "difficulty": 2.3, + "effort": 268.54, + "level": 0.43, + "bugs": 0.04, + "time": 15, + "intelligentContent": 50.76, + "number_operators": 5, + "number_operands": 23, + "number_operators_unique": 3, + "number_operands_unique": 15, + "cloc": 16, + "loc": 54, + "lloc": 38, + "mi": 88.27, + "mIwoC": 50.93, + "commentWeight": 37.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 484, + "relativeDataComplexity": 0.09, + "relativeSystemComplexity": 484.09, + "totalStructuralComplexity": 2420, + "totalDataComplexity": 0.43, + "totalSystemComplexity": 2420.43, + "package": "App\\Providers\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 5, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Models\\OrderLine", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "order", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "product", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "variant", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "fulfillmentLines", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 24, + "vocabulary": 13, + "volume": 88.81, + "difficulty": 0.79, + "effort": 70.31, + "level": 1.26, + "bugs": 0.03, + "time": 4, + "intelligentContent": 112.18, + "number_operators": 5, + "number_operands": 19, + "number_operators_unique": 1, + "number_operands_unique": 12, + "cloc": 16, + "loc": 43, + "lloc": 27, + "mi": 95.52, + "mIwoC": 55, + "commentWeight": 40.52, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 1.67, + "relativeSystemComplexity": 5.67, + "totalStructuralComplexity": 20, + "totalDataComplexity": 8.33, + "totalSystemComplexity": 28.33, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 3, + "instability": 0.75, + "violations": {} + }, + { + "name": "App\\Models\\WebhookSubscription", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "appInstallation", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "deliveries", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 13, + "vocabulary": 11, + "volume": 44.97, + "difficulty": 0.55, + "effort": 24.73, + "level": 1.82, + "bugs": 0.01, + "time": 1, + "intelligentContent": 81.77, + "number_operators": 2, + "number_operands": 11, + "number_operators_unique": 1, + "number_operands_unique": 10, + "cloc": 7, + "loc": 22, + "lloc": 15, + "mi": 100.98, + "mIwoC": 62.64, + "commentWeight": 38.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 0.67, + "relativeSystemComplexity": 4.67, + "totalStructuralComplexity": 8, + "totalDataComplexity": 1.33, + "totalSystemComplexity": 9.33, + "package": "App\\Models\\", + "pageRank": 0.02, + "afferentCoupling": 3, + "efferentCoupling": 3, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Models\\ThemeFile", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "theme", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 10, + "vocabulary": 8, + "volume": 30, + "difficulty": 0.57, + "effort": 17.14, + "level": 1.75, + "bugs": 0.01, + "time": 1, + "intelligentContent": 52.5, + "number_operators": 2, + "number_operands": 8, + "number_operators_unique": 1, + "number_operands_unique": 7, + "cloc": 7, + "loc": 22, + "lloc": 15, + "mi": 102.21, + "mIwoC": 63.87, + "commentWeight": 38.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Models\\ProductOption", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "product", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "values", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 7, + "vocabulary": 5, + "volume": 16.25, + "difficulty": 0.63, + "effort": 10.16, + "level": 1.6, + "bugs": 0.01, + "time": 1, + "intelligentContent": 26.01, + "number_operators": 2, + "number_operands": 5, + "number_operators_unique": 1, + "number_operands_unique": 4, + "cloc": 7, + "loc": 22, + "lloc": 15, + "mi": 104.07, + "mIwoC": 65.73, + "commentWeight": 38.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 0.67, + "relativeSystemComplexity": 4.67, + "totalStructuralComplexity": 8, + "totalDataComplexity": 1.33, + "totalSystemComplexity": 9.33, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Models\\NavigationItem", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "menu", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "resolveUrl", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 30, + "vocabulary": 16, + "volume": 120, + "difficulty": 2.65, + "effort": 318.46, + "level": 0.38, + "bugs": 0.04, + "time": 18, + "intelligentContent": 45.22, + "number_operators": 7, + "number_operands": 23, + "number_operators_unique": 3, + "number_operands_unique": 13, + "cloc": 7, + "loc": 31, + "lloc": 24, + "mi": 88.64, + "mIwoC": 55.06, + "commentWeight": 33.57, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1.5, + "relativeSystemComplexity": 2.5, + "totalStructuralComplexity": 3, + "totalDataComplexity": 4.5, + "totalSystemComplexity": 7.5, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 2, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Models\\Refund", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "order", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "payment", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 12, + "vocabulary": 8, + "volume": 36, + "difficulty": 0.64, + "effort": 23.14, + "level": 1.56, + "bugs": 0.01, + "time": 1, + "intelligentContent": 56, + "number_operators": 3, + "number_operands": 9, + "number_operators_unique": 1, + "number_operands_unique": 7, + "cloc": 10, + "loc": 29, + "lloc": 19, + "mi": 100.54, + "mIwoC": 61.07, + "commentWeight": 39.47, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1.5, + "relativeSystemComplexity": 2.5, + "totalStructuralComplexity": 3, + "totalDataComplexity": 4.5, + "totalSystemComplexity": 7.5, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 2, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Models\\InventoryItem", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "variant", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "quantityAvailable", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 16, + "vocabulary": 9, + "volume": 50.72, + "difficulty": 1.71, + "effort": 86.95, + "level": 0.58, + "bugs": 0.02, + "time": 5, + "intelligentContent": 29.59, + "number_operators": 4, + "number_operands": 12, + "number_operators_unique": 2, + "number_operands_unique": 7, + "cloc": 7, + "loc": 26, + "lloc": 19, + "mi": 96.03, + "mIwoC": 60.03, + "commentWeight": 36, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1.5, + "relativeSystemComplexity": 2.5, + "totalStructuralComplexity": 3, + "totalDataComplexity": 4.5, + "totalSystemComplexity": 7.5, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 4, + "efferentCoupling": 2, + "instability": 0.33, + "violations": {} + }, + { + "name": "App\\Models\\NavigationMenu", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "items", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 1, + "length": 7, + "vocabulary": 7, + "volume": 19.65, + "difficulty": 0.5, + "effort": 9.83, + "level": 2, + "bugs": 0.01, + "time": 1, + "intelligentContent": 39.3, + "number_operators": 1, + "number_operands": 6, + "number_operators_unique": 1, + "number_operands_unique": 6, + "cloc": 4, + "loc": 14, + "lloc": 10, + "mi": 105.83, + "mIwoC": 69, + "commentWeight": 36.83, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 0.33, + "relativeSystemComplexity": 4.33, + "totalStructuralComplexity": 4, + "totalDataComplexity": 0.33, + "totalSystemComplexity": 4.33, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 2, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Models\\App", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "installations", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 11, + "vocabulary": 9, + "volume": 34.87, + "difficulty": 0.56, + "effort": 19.61, + "level": 1.78, + "bugs": 0.01, + "time": 1, + "intelligentContent": 61.99, + "number_operators": 2, + "number_operands": 9, + "number_operators_unique": 1, + "number_operands_unique": 8, + "cloc": 7, + "loc": 22, + "lloc": 15, + "mi": 101.75, + "mIwoC": 63.41, + "commentWeight": 38.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Models\\CartLine", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "cart", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "variant", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 1, + "length": 12, + "vocabulary": 9, + "volume": 38.04, + "difficulty": 0.63, + "effort": 23.77, + "level": 1.6, + "bugs": 0.01, + "time": 1, + "intelligentContent": 60.86, + "number_operators": 2, + "number_operands": 10, + "number_operators_unique": 1, + "number_operands_unique": 8, + "cloc": 7, + "loc": 22, + "lloc": 15, + "mi": 101.49, + "mIwoC": 63.15, + "commentWeight": 38.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Models\\AppInstallation", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "app", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "webhookSubscriptions", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 16, + "vocabulary": 11, + "volume": 55.35, + "difficulty": 0.65, + "effort": 35.98, + "level": 1.54, + "bugs": 0.02, + "time": 2, + "intelligentContent": 85.16, + "number_operators": 3, + "number_operands": 13, + "number_operators_unique": 1, + "number_operands_unique": 10, + "cloc": 10, + "loc": 31, + "lloc": 21, + "mi": 97.35, + "mIwoC": 58.82, + "commentWeight": 38.53, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 5, + "totalStructuralComplexity": 12, + "totalDataComplexity": 3, + "totalSystemComplexity": 15, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 3, + "instability": 0.75, + "violations": {} + }, + { + "name": "App\\Models\\Cart", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "lines", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "checkouts", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "incrementVersion", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 18, + "vocabulary": 12, + "volume": 64.53, + "difficulty": 2.17, + "effort": 139.81, + "level": 0.46, + "bugs": 0.02, + "time": 8, + "intelligentContent": 29.78, + "number_operators": 5, + "number_operands": 13, + "number_operators_unique": 3, + "number_operands_unique": 9, + "cloc": 10, + "loc": 32, + "lloc": 22, + "mi": 96, + "mIwoC": 57.91, + "commentWeight": 38.09, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1.5, + "relativeSystemComplexity": 2.5, + "totalStructuralComplexity": 4, + "totalDataComplexity": 6, + "totalSystemComplexity": 10, + "package": "App\\Models\\", + "pageRank": 0.04, + "afferentCoupling": 6, + "efferentCoupling": 2, + "instability": 0.25, + "violations": {} + }, + { + "name": "App\\Models\\Discount", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "isCurrentlyActive", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 9, + "ccn": 8, + "ccnMethodMax": 8, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 50, + "vocabulary": 22, + "volume": 222.97, + "difficulty": 5.81, + "effort": 1296.02, + "level": 0.17, + "bugs": 0.07, + "time": 72, + "intelligentContent": 38.36, + "number_operators": 19, + "number_operands": 31, + "number_operators_unique": 6, + "number_operands_unique": 16, + "cloc": 4, + "loc": 31, + "lloc": 27, + "mi": 77.67, + "mIwoC": 51.26, + "commentWeight": 26.41, + "kanDefect": 0.43, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 2, + "relativeSystemComplexity": 6, + "totalStructuralComplexity": 8, + "totalDataComplexity": 4, + "totalSystemComplexity": 12, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 3, + "efferentCoupling": 1, + "instability": 0.25, + "violations": {} + }, + { + "name": "App\\Models\\Product", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "variants", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "options", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "media", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "collections", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 25, + "vocabulary": 15, + "volume": 97.67, + "difficulty": 0.71, + "effort": 69.77, + "level": 1.4, + "bugs": 0.03, + "time": 4, + "intelligentContent": 136.74, + "number_operators": 5, + "number_operands": 20, + "number_operators_unique": 1, + "number_operands_unique": 14, + "cloc": 16, + "loc": 42, + "lloc": 26, + "mi": 95.92, + "mIwoC": 55.07, + "commentWeight": 40.85, + "kanDefect": 0.15, + "relativeStructuralComplexity": 9, + "relativeDataComplexity": 1.25, + "relativeSystemComplexity": 10.25, + "totalStructuralComplexity": 45, + "totalDataComplexity": 6.25, + "totalSystemComplexity": 51.25, + "package": "App\\Models\\", + "pageRank": 0.04, + "afferentCoupling": 10, + "efferentCoupling": 3, + "instability": 0.23, + "violations": {} + }, + { + "name": "App\\Models\\Order", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "customer", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "lines", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "payments", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "refunds", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "fulfillments", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "requiresShipping", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "refundedTotal", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "refundableAmount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 9, + "nbMethods": 9, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 8, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 13, + "ccn": 5, + "ccnMethodMax": 5, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 65, + "vocabulary": 31, + "volume": 322.02, + "difficulty": 5.76, + "effort": 1854.85, + "level": 0.17, + "bugs": 0.11, + "time": 103, + "intelligentContent": 55.91, + "number_operators": 17, + "number_operands": 48, + "number_operators_unique": 6, + "number_operands_unique": 25, + "cloc": 19, + "loc": 69, + "lloc": 50, + "mi": 81.02, + "mIwoC": 44.71, + "commentWeight": 36.32, + "kanDefect": 0.52, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 1.57, + "relativeSystemComplexity": 37.57, + "totalStructuralComplexity": 324, + "totalDataComplexity": 14.14, + "totalSystemComplexity": 338.14, + "package": "App\\Models\\", + "pageRank": 0.1, + "afferentCoupling": 20, + "efferentCoupling": 3, + "instability": 0.13, + "violations": {} + }, + { + "name": "App\\Models\\Store", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "organization", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "domains", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "users", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "settings", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasOne" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 5, + "length": 19, + "vocabulary": 11, + "volume": 65.73, + "difficulty": 0.7, + "effort": 46.01, + "level": 1.43, + "bugs": 0.02, + "time": 3, + "intelligentContent": 93.9, + "number_operators": 5, + "number_operands": 14, + "number_operators_unique": 1, + "number_operands_unique": 10, + "cloc": 16, + "loc": 42, + "lloc": 26, + "mi": 97.12, + "mIwoC": 56.27, + "commentWeight": 40.85, + "kanDefect": 0.15, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 0.71, + "relativeSystemComplexity": 36.71, + "totalStructuralComplexity": 180, + "totalDataComplexity": 3.57, + "totalSystemComplexity": 183.57, + "package": "App\\Models\\", + "pageRank": 0.06, + "afferentCoupling": 17, + "efferentCoupling": 5, + "instability": 0.23, + "violations": {} + }, + { + "name": "App\\Models\\StoreDomain", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "store", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 11, + "vocabulary": 8, + "volume": 33, + "difficulty": 0.64, + "effort": 21.21, + "level": 1.56, + "bugs": 0.01, + "time": 1, + "intelligentContent": 51.33, + "number_operators": 2, + "number_operands": 9, + "number_operators_unique": 1, + "number_operands_unique": 7, + "cloc": 7, + "loc": 22, + "lloc": 15, + "mi": 101.92, + "mIwoC": 63.58, + "commentWeight": 38.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Models\\Theme", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "files", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "settings", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasOne" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 13, + "vocabulary": 8, + "volume": 39, + "difficulty": 0.71, + "effort": 27.86, + "level": 1.4, + "bugs": 0.01, + "time": 2, + "intelligentContent": 54.6, + "number_operators": 3, + "number_operands": 10, + "number_operators_unique": 1, + "number_operands_unique": 7, + "cloc": 10, + "loc": 28, + "lloc": 18, + "mi": 101.3, + "mIwoC": 61.34, + "commentWeight": 39.96, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 5, + "totalStructuralComplexity": 12, + "totalDataComplexity": 3, + "totalSystemComplexity": 15, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 3, + "instability": 0.6, + "violations": {} + }, + { + "name": "App\\Models\\ProductMedia", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "product", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 16, + "vocabulary": 13, + "volume": 59.21, + "difficulty": 0.58, + "effort": 34.54, + "level": 1.71, + "bugs": 0.02, + "time": 2, + "intelligentContent": 101.5, + "number_operators": 2, + "number_operands": 14, + "number_operators_unique": 1, + "number_operands_unique": 12, + "cloc": 7, + "loc": 23, + "lloc": 16, + "mi": 98.91, + "mIwoC": 61.19, + "commentWeight": 37.72, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Models\\User", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "initials", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "stores", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "roleForStore", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 7, + "ccn": 4, + "ccnMethodMax": 4, + "externals": [ + "Illuminate\\Foundation\\Auth\\User", + "Illuminate\\Support\\Str", + "Illuminate\\Support\\Str", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany", + "App\\Models\\Store", + "App\\Enums\\StoreUserRole" + ], + "parents": [ + "Illuminate\\Foundation\\Auth\\User" + ], + "implements": [], + "lcom": 3, + "length": 55, + "vocabulary": 29, + "volume": 267.19, + "difficulty": 3.36, + "effort": 897.75, + "level": 0.3, + "bugs": 0.09, + "time": 50, + "intelligentContent": 79.52, + "number_operators": 13, + "number_operands": 42, + "number_operators_unique": 4, + "number_operands_unique": 25, + "cloc": 24, + "loc": 56, + "lloc": 32, + "mi": 92.09, + "mIwoC": 49.64, + "commentWeight": 42.45, + "kanDefect": 0.29, + "relativeStructuralComplexity": 169, + "relativeDataComplexity": 0.45, + "relativeSystemComplexity": 169.45, + "totalStructuralComplexity": 676, + "totalDataComplexity": 1.79, + "totalSystemComplexity": 677.79, + "package": "App\\Models\\", + "pageRank": 0.02, + "afferentCoupling": 4, + "efferentCoupling": 5, + "instability": 0.56, + "violations": {} + }, + { + "name": "App\\Models\\WebhookDelivery", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "subscription", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 16, + "vocabulary": 12, + "volume": 57.36, + "difficulty": 0.64, + "effort": 36.5, + "level": 1.57, + "bugs": 0.02, + "time": 2, + "intelligentContent": 90.14, + "number_operators": 2, + "number_operands": 14, + "number_operators_unique": 1, + "number_operands_unique": 11, + "cloc": 7, + "loc": 23, + "lloc": 16, + "mi": 99, + "mIwoC": 61.29, + "commentWeight": 37.72, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Models\\Fulfillment", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "order", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "lines", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 17, + "vocabulary": 10, + "volume": 56.47, + "difficulty": 0.78, + "effort": 43.92, + "level": 1.29, + "bugs": 0.02, + "time": 2, + "intelligentContent": 72.61, + "number_operators": 3, + "number_operands": 14, + "number_operators_unique": 1, + "number_operands_unique": 9, + "cloc": 10, + "loc": 29, + "lloc": 19, + "mi": 99.17, + "mIwoC": 59.7, + "commentWeight": 39.47, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 5, + "totalStructuralComplexity": 12, + "totalDataComplexity": 3, + "totalSystemComplexity": 15, + "package": "App\\Models\\", + "pageRank": 0.02, + "afferentCoupling": 2, + "efferentCoupling": 3, + "instability": 0.6, + "violations": {} + }, + { + "name": "App\\Models\\ThemeSettings", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "theme", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 10, + "vocabulary": 7, + "volume": 28.07, + "difficulty": 0.67, + "effort": 18.72, + "level": 1.5, + "bugs": 0.01, + "time": 1, + "intelligentContent": 42.11, + "number_operators": 2, + "number_operands": 8, + "number_operators_unique": 1, + "number_operands_unique": 6, + "cloc": 7, + "loc": 26, + "lloc": 19, + "mi": 97.83, + "mIwoC": 61.83, + "commentWeight": 36, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Models\\Checkout", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "cart", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 27, + "vocabulary": 17, + "volume": 110.36, + "difficulty": 0.78, + "effort": 86.22, + "level": 1.28, + "bugs": 0.04, + "time": 5, + "intelligentContent": 141.26, + "number_operators": 2, + "number_operands": 25, + "number_operators_unique": 1, + "number_operands_unique": 16, + "cloc": 7, + "loc": 21, + "lloc": 14, + "mi": 99.55, + "mIwoC": 60.56, + "commentWeight": 38.99, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0.02, + "afferentCoupling": 7, + "efferentCoupling": 2, + "instability": 0.22, + "violations": {} + }, + { + "name": "App\\Models\\Payment", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "order", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "refunds", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 3, + "length": 17, + "vocabulary": 11, + "volume": 58.81, + "difficulty": 0.7, + "effort": 41.17, + "level": 1.43, + "bugs": 0.02, + "time": 2, + "intelligentContent": 84.01, + "number_operators": 3, + "number_operands": 14, + "number_operators_unique": 1, + "number_operands_unique": 10, + "cloc": 10, + "loc": 29, + "lloc": 19, + "mi": 99.05, + "mIwoC": 59.58, + "commentWeight": 39.47, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 5, + "totalStructuralComplexity": 12, + "totalDataComplexity": 3, + "totalSystemComplexity": 15, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 4, + "efferentCoupling": 3, + "instability": 0.43, + "violations": {} + }, + { + "name": "App\\Models\\AnalyticsDaily", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "setKeysForSaveQuery", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "setKeysForSelectQuery", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 3, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 29, + "vocabulary": 13, + "volume": 107.31, + "difficulty": 1.08, + "effort": 116.26, + "level": 0.92, + "bugs": 0.04, + "time": 6, + "intelligentContent": 99.06, + "number_operators": 3, + "number_operands": 26, + "number_operators_unique": 1, + "number_operands_unique": 12, + "cloc": 15, + "loc": 38, + "lloc": 23, + "mi": 97.28, + "mIwoC": 55.94, + "commentWeight": 41.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 16, + "relativeDataComplexity": 0.73, + "relativeSystemComplexity": 16.73, + "totalStructuralComplexity": 48, + "totalDataComplexity": 2.2, + "totalSystemComplexity": 50.2, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 1, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Models\\Customer", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "getAuthPassword", + "role": "getter", + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "getAuthPasswordName", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "getAuthIdentifierName", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "addresses", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "orders", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "carts", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 7, + "nbMethods": 6, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 5, + "nbMethodsGetter": 1, + "nbMethodsSetters": 0, + "wmc": 6, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Foundation\\Auth\\User", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Foundation\\Auth\\User" + ], + "implements": [], + "lcom": 4, + "length": 25, + "vocabulary": 12, + "volume": 89.62, + "difficulty": 0.82, + "effort": 73.33, + "level": 1.22, + "bugs": 0.03, + "time": 4, + "intelligentContent": 109.54, + "number_operators": 7, + "number_operands": 18, + "number_operators_unique": 1, + "number_operands_unique": 11, + "cloc": 16, + "loc": 51, + "lloc": 35, + "mi": 90.66, + "mIwoC": 52.51, + "commentWeight": 38.14, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 3.5, + "relativeSystemComplexity": 4.5, + "totalStructuralComplexity": 7, + "totalDataComplexity": 24.5, + "totalSystemComplexity": 31.5, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 4, + "efferentCoupling": 2, + "instability": 0.33, + "violations": {} + }, + { + "name": "App\\Models\\ProductVariant", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "product", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "inventoryItem", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "optionValues", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\HasOne", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 4, + "length": 27, + "vocabulary": 17, + "volume": 110.36, + "difficulty": 0.72, + "effort": 79.32, + "level": 1.39, + "bugs": 0.04, + "time": 4, + "intelligentContent": 153.55, + "number_operators": 4, + "number_operands": 23, + "number_operators_unique": 1, + "number_operands_unique": 16, + "cloc": 13, + "loc": 35, + "lloc": 22, + "mi": 96.78, + "mIwoC": 56.28, + "commentWeight": 40.5, + "kanDefect": 0.15, + "relativeStructuralComplexity": 9, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 10, + "totalStructuralComplexity": 36, + "totalDataComplexity": 4, + "totalSystemComplexity": 40, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 4, + "instability": 0.8, + "violations": {} + }, + { + "name": "App\\Models\\ProductOptionValue", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "option", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 1, + "length": 6, + "vocabulary": 5, + "volume": 13.93, + "difficulty": 0.63, + "effort": 8.71, + "level": 1.6, + "bugs": 0, + "time": 0, + "intelligentContent": 22.29, + "number_operators": 1, + "number_operands": 5, + "number_operators_unique": 1, + "number_operands_unique": 4, + "cloc": 4, + "loc": 15, + "lloc": 11, + "mi": 105.01, + "mIwoC": 69.14, + "commentWeight": 35.87, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 0.5, + "relativeSystemComplexity": 1.5, + "totalStructuralComplexity": 1, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 1.5, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Models\\TaxSettings", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "store", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 16, + "vocabulary": 11, + "volume": 55.35, + "difficulty": 0.7, + "effort": 38.75, + "level": 1.43, + "bugs": 0.02, + "time": 2, + "intelligentContent": 79.07, + "number_operators": 2, + "number_operands": 14, + "number_operators_unique": 1, + "number_operands_unique": 10, + "cloc": 7, + "loc": 26, + "lloc": 19, + "mi": 95.77, + "mIwoC": 59.77, + "commentWeight": 36, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 3, + "efferentCoupling": 2, + "instability": 0.4, + "violations": {} + }, + { + "name": "App\\Models\\ShippingZone", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "rates", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 12, + "vocabulary": 8, + "volume": 36, + "difficulty": 0.71, + "effort": 25.71, + "level": 1.4, + "bugs": 0.01, + "time": 1, + "intelligentContent": 50.4, + "number_operators": 2, + "number_operands": 10, + "number_operators_unique": 1, + "number_operands_unique": 7, + "cloc": 7, + "loc": 21, + "lloc": 14, + "mi": 102.96, + "mIwoC": 63.97, + "commentWeight": 38.99, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 2, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Models\\Collection", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "products", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 13, + "vocabulary": 10, + "volume": 43.19, + "difficulty": 0.61, + "effort": 26.39, + "level": 1.64, + "bugs": 0.01, + "time": 1, + "intelligentContent": 70.67, + "number_operators": 2, + "number_operands": 11, + "number_operators_unique": 1, + "number_operands_unique": 9, + "cloc": 7, + "loc": 21, + "lloc": 14, + "mi": 102.41, + "mIwoC": 63.41, + "commentWeight": 38.99, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 0.67, + "relativeSystemComplexity": 4.67, + "totalStructuralComplexity": 8, + "totalDataComplexity": 1.33, + "totalSystemComplexity": 9.33, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 5, + "efferentCoupling": 2, + "instability": 0.29, + "violations": {} + }, + { + "name": "App\\Models\\Page", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 1, + "length": 10, + "vocabulary": 8, + "volume": 30, + "difficulty": 0.64, + "effort": 19.29, + "level": 1.56, + "bugs": 0.01, + "time": 1, + "intelligentContent": 46.67, + "number_operators": 1, + "number_operands": 9, + "number_operators_unique": 1, + "number_operands_unique": 7, + "cloc": 4, + "loc": 14, + "lloc": 10, + "mi": 104.54, + "mIwoC": 67.71, + "commentWeight": 36.83, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 1, + "totalStructuralComplexity": 0, + "totalDataComplexity": 1, + "totalSystemComplexity": 1, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 3, + "efferentCoupling": 1, + "instability": 0.25, + "violations": {} + }, + { + "name": "App\\Models\\ShippingRate", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "zone", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 14, + "vocabulary": 9, + "volume": 44.38, + "difficulty": 0.75, + "effort": 33.28, + "level": 1.33, + "bugs": 0.01, + "time": 2, + "intelligentContent": 59.17, + "number_operators": 2, + "number_operands": 12, + "number_operators_unique": 1, + "number_operands_unique": 8, + "cloc": 7, + "loc": 21, + "lloc": 14, + "mi": 102.32, + "mIwoC": 63.33, + "commentWeight": 38.99, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 4, + "efferentCoupling": 2, + "instability": 0.33, + "violations": {} + }, + { + "name": "App\\Models\\StoreSettings", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "store", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 9, + "vocabulary": 6, + "volume": 23.26, + "difficulty": 0.7, + "effort": 16.29, + "level": 1.43, + "bugs": 0.01, + "time": 1, + "intelligentContent": 33.24, + "number_operators": 2, + "number_operands": 7, + "number_operators_unique": 1, + "number_operands_unique": 5, + "cloc": 7, + "loc": 25, + "lloc": 18, + "mi": 99.46, + "mIwoC": 62.91, + "commentWeight": 36.55, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Models\\CustomerAddress", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "customer", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 2, + "length": 11, + "vocabulary": 8, + "volume": 33, + "difficulty": 0.64, + "effort": 21.21, + "level": 1.56, + "bugs": 0.01, + "time": 1, + "intelligentContent": 51.33, + "number_operators": 2, + "number_operands": 9, + "number_operators_unique": 1, + "number_operands_unique": 7, + "cloc": 7, + "loc": 22, + "lloc": 15, + "mi": 101.92, + "mIwoC": 63.58, + "commentWeight": 38.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Models\\StoreUser", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "booted", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 2, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Illuminate\\Database\\Eloquent\\Relations\\Pivot", + "static" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Relations\\Pivot" + ], + "implements": [], + "lcom": 2, + "length": 10, + "vocabulary": 6, + "volume": 25.85, + "difficulty": 2, + "effort": 51.7, + "level": 0.5, + "bugs": 0.01, + "time": 3, + "intelligentContent": 12.92, + "number_operators": 2, + "number_operands": 8, + "number_operators_unique": 2, + "number_operands_unique": 4, + "cloc": 3, + "loc": 23, + "lloc": 20, + "mi": 88, + "mIwoC": 61.46, + "commentWeight": 26.54, + "kanDefect": 0.22, + "relativeStructuralComplexity": 9, + "relativeDataComplexity": 0.25, + "relativeSystemComplexity": 9.25, + "totalStructuralComplexity": 18, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 18.5, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Models\\FulfillmentLine", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "fulfillment", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "orderLine", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 1, + "length": 7, + "vocabulary": 5, + "volume": 16.25, + "difficulty": 0.63, + "effort": 10.16, + "level": 1.6, + "bugs": 0.01, + "time": 1, + "intelligentContent": 26.01, + "number_operators": 2, + "number_operands": 5, + "number_operators_unique": 1, + "number_operands_unique": 4, + "cloc": 7, + "loc": 22, + "lloc": 15, + "mi": 104.07, + "mIwoC": 65.73, + "commentWeight": 38.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Models\\AnalyticsEvent", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "casts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 1, + "length": 13, + "vocabulary": 11, + "volume": 44.97, + "difficulty": 0.6, + "effort": 26.98, + "level": 1.67, + "bugs": 0.01, + "time": 1, + "intelligentContent": 74.95, + "number_operators": 1, + "number_operands": 12, + "number_operators_unique": 1, + "number_operands_unique": 10, + "cloc": 4, + "loc": 16, + "lloc": 12, + "mi": 99.72, + "mIwoC": 64.75, + "commentWeight": 34.97, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 1, + "totalStructuralComplexity": 0, + "totalDataComplexity": 1, + "totalSystemComplexity": 1, + "package": "App\\Models\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 1, + "instability": 0.33, + "violations": {} + }, + { + "name": "App\\Models\\Scopes\\StoreScope", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "apply", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Illuminate\\Database\\Eloquent\\Scope", + "Illuminate\\Database\\Eloquent\\Builder", + "Illuminate\\Database\\Eloquent\\Model" + ], + "parents": [], + "implements": [ + "Illuminate\\Database\\Eloquent\\Scope" + ], + "lcom": 1, + "length": 13, + "vocabulary": 9, + "volume": 41.21, + "difficulty": 3.6, + "effort": 148.35, + "level": 0.28, + "bugs": 0.01, + "time": 8, + "intelligentContent": 11.45, + "number_operators": 4, + "number_operands": 9, + "number_operators_unique": 4, + "number_operands_unique": 5, + "cloc": 1, + "loc": 13, + "lloc": 12, + "mi": 85.71, + "mIwoC": 64.88, + "commentWeight": 20.83, + "kanDefect": 0.22, + "relativeStructuralComplexity": 9, + "relativeDataComplexity": 0.75, + "relativeSystemComplexity": 9.75, + "totalStructuralComplexity": 9, + "totalDataComplexity": 0.75, + "totalSystemComplexity": 9.75, + "package": "App\\Models\\Scopes\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 3, + "instability": 0.75, + "violations": {} + }, + { + "name": "App\\Models\\Organization", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "stores", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Database\\Eloquent\\Model", + "Illuminate\\Database\\Eloquent\\Relations\\HasMany" + ], + "parents": [ + "Illuminate\\Database\\Eloquent\\Model" + ], + "implements": [], + "lcom": 1, + "length": 4, + "vocabulary": 4, + "volume": 8, + "difficulty": 0.5, + "effort": 4, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 16, + "number_operators": 1, + "number_operands": 3, + "number_operators_unique": 1, + "number_operands_unique": 3, + "cloc": 4, + "loc": 14, + "lloc": 10, + "mi": 108.56, + "mIwoC": 71.73, + "commentWeight": 36.83, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 0.5, + "relativeSystemComplexity": 1.5, + "totalStructuralComplexity": 1, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 1.5, + "package": "App\\Models\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Models\\Concerns\\BelongsToStore", + "interface": false, + "abstract": true, + "final": false, + "methods": [ + { + "name": "bootBelongsToStore", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "store", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 3, + "ccnMethodMax": 3, + "externals": [ + "App\\Models\\Scopes\\StoreScope", + "static", + "static", + "Illuminate\\Database\\Eloquent\\Relations\\BelongsTo" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 14, + "vocabulary": 9, + "volume": 44.38, + "difficulty": 5, + "effort": 221.89, + "level": 0.2, + "bugs": 0.01, + "time": 12, + "intelligentContent": 8.88, + "number_operators": 6, + "number_operands": 8, + "number_operators_unique": 5, + "number_operands_unique": 4, + "cloc": 4, + "loc": 22, + "lloc": 18, + "mi": 91.36, + "mIwoC": 60.68, + "commentWeight": 30.68, + "kanDefect": 0.22, + "relativeStructuralComplexity": 16, + "relativeDataComplexity": 0.2, + "relativeSystemComplexity": 16.2, + "totalStructuralComplexity": 32, + "totalDataComplexity": 0.4, + "totalSystemComplexity": 32.4, + "package": "App\\Models\\Concerns\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Exceptions\\InsufficientInventoryException", + "interface": false, + "abstract": false, + "final": false, + "methods": [], + "nbMethodsIncludingGettersSetters": 0, + "nbMethods": 0, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 0, + "ccn": 1, + "ccnMethodMax": 0, + "externals": [ + "RuntimeException" + ], + "parents": [ + "RuntimeException" + ], + "implements": [], + "lcom": 0, + "length": 0, + "vocabulary": 0, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 0, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 0, + "number_operators_unique": 0, + "number_operands_unique": 0, + "cloc": 0, + "loc": 4, + "lloc": 4, + "mi": 171, + "mIwoC": 171, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 0, + "relativeSystemComplexity": 0, + "totalStructuralComplexity": 0, + "totalDataComplexity": 0, + "totalSystemComplexity": 0, + "package": "App\\Exceptions\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 1, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Exceptions\\FulfillmentGuardException", + "interface": false, + "abstract": false, + "final": false, + "methods": [], + "nbMethodsIncludingGettersSetters": 0, + "nbMethods": 0, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 0, + "ccn": 1, + "ccnMethodMax": 0, + "externals": [ + "RuntimeException" + ], + "parents": [ + "RuntimeException" + ], + "implements": [], + "lcom": 0, + "length": 0, + "vocabulary": 0, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 0, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 0, + "number_operators_unique": 0, + "number_operands_unique": 0, + "cloc": 0, + "loc": 4, + "lloc": 4, + "mi": 171, + "mIwoC": 171, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 0, + "relativeSystemComplexity": 0, + "totalStructuralComplexity": 0, + "totalDataComplexity": 0, + "totalSystemComplexity": 0, + "package": "App\\Exceptions\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 1, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Exceptions\\InvalidDiscountException", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "notFound", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "expired", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "notYetActive", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "usageLimitReached", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "minimumNotMet", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "disabled", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 7, + "nbMethods": 7, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 7, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 8, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "RuntimeException" + ], + "parents": [ + "RuntimeException" + ], + "implements": [], + "lcom": 7, + "length": 26, + "vocabulary": 17, + "volume": 106.27, + "difficulty": 1.27, + "effort": 134.61, + "level": 0.79, + "bugs": 0.04, + "time": 7, + "intelligentContent": 83.9, + "number_operators": 7, + "number_operands": 19, + "number_operators_unique": 2, + "number_operands_unique": 15, + "cloc": 0, + "loc": 32, + "lloc": 32, + "mi": 52.71, + "mIwoC": 52.71, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 3.14, + "relativeSystemComplexity": 4.14, + "totalStructuralComplexity": 7, + "totalDataComplexity": 22, + "totalSystemComplexity": 29, + "package": "App\\Exceptions\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 1, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Exceptions\\PaymentFailedException", + "interface": false, + "abstract": false, + "final": false, + "methods": [], + "nbMethodsIncludingGettersSetters": 0, + "nbMethods": 0, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 0, + "ccn": 1, + "ccnMethodMax": 0, + "externals": [ + "RuntimeException" + ], + "parents": [ + "RuntimeException" + ], + "implements": [], + "lcom": 0, + "length": 0, + "vocabulary": 0, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 0, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 0, + "number_operators_unique": 0, + "number_operands_unique": 0, + "cloc": 0, + "loc": 4, + "lloc": 4, + "mi": 171, + "mIwoC": 171, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 0, + "relativeSystemComplexity": 0, + "totalStructuralComplexity": 0, + "totalDataComplexity": 0, + "totalSystemComplexity": 0, + "package": "App\\Exceptions\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 1, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Policies\\StorePolicy", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "view", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "update", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "delete", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Models\\User", + "App\\Models\\Store", + "App\\Models\\User", + "App\\Models\\Store", + "App\\Models\\User", + "App\\Models\\Store" + ], + "parents": [], + "implements": [], + "lcom": 3, + "length": 20, + "vocabulary": 7, + "volume": 56.15, + "difficulty": 9.33, + "effort": 524.04, + "level": 0.11, + "bugs": 0.02, + "time": 29, + "intelligentContent": 6.02, + "number_operators": 6, + "number_operands": 14, + "number_operators_unique": 4, + "number_operands_unique": 3, + "cloc": 0, + "loc": 18, + "lloc": 18, + "mi": 60.23, + "mIwoC": 60.23, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 2.5, + "relativeSystemComplexity": 3.5, + "totalStructuralComplexity": 3, + "totalDataComplexity": 7.5, + "totalSystemComplexity": 10.5, + "package": "App\\Policies\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Policies\\Concerns\\ChecksStoreRole", + "interface": false, + "abstract": true, + "final": false, + "methods": [ + { + "name": "getUserRole", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "hasMinRole", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 2, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 3, + "ccnMethodMax": 2, + "externals": [ + "App\\Models\\User", + "App\\Models\\User", + "App\\Enums\\StoreUserRole" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 22, + "vocabulary": 11, + "volume": 76.11, + "difficulty": 5.83, + "effort": 443.96, + "level": 0.17, + "bugs": 0.03, + "time": 25, + "intelligentContent": 13.05, + "number_operators": 8, + "number_operands": 14, + "number_operators_unique": 5, + "number_operands_unique": 6, + "cloc": 1, + "loc": 18, + "lloc": 17, + "mi": 77.44, + "mIwoC": 59.58, + "commentWeight": 17.85, + "kanDefect": 0.22, + "relativeStructuralComplexity": 9, + "relativeDataComplexity": 1.13, + "relativeSystemComplexity": 10.13, + "totalStructuralComplexity": 18, + "totalDataComplexity": 2.25, + "totalSystemComplexity": 20.25, + "package": "App\\Policies\\Concerns\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Settings\\TwoFactor", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "enable", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "loadSetupData", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "showVerificationIfNecessary", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "confirmTwoFactor", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "resetVerification", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "disable", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "closeModal", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "getModalConfigProperty", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 9, + "nbMethods": 9, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 8, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 17, + "ccn": 9, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "Laravel\\Fortify\\Actions\\DisableTwoFactorAuthentication", + "Laravel\\Fortify\\Features", + "Laravel\\Fortify\\Features", + "Laravel\\Fortify\\Fortify", + "Laravel\\Fortify\\Features", + "Laravel\\Fortify\\Features", + "Laravel\\Fortify\\Actions\\EnableTwoFactorAuthentication", + "Laravel\\Fortify\\Actions\\ConfirmTwoFactorAuthentication", + "Laravel\\Fortify\\Actions\\DisableTwoFactorAuthentication" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 95, + "vocabulary": 31, + "volume": 470.65, + "difficulty": 6.92, + "effort": 3258.34, + "level": 0.14, + "bugs": 0.16, + "time": 181, + "intelligentContent": 67.98, + "number_operators": 23, + "number_operands": 72, + "number_operators_unique": 5, + "number_operands_unique": 26, + "cloc": 32, + "loc": 116, + "lloc": 84, + "mi": 74.44, + "mIwoC": 38.1, + "commentWeight": 36.34, + "kanDefect": 0.57, + "relativeStructuralComplexity": 144, + "relativeDataComplexity": 0.34, + "relativeSystemComplexity": 144.34, + "totalStructuralComplexity": 1296, + "totalDataComplexity": 3.08, + "totalSystemComplexity": 1299.08, + "package": "App\\Livewire\\Settings\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 6, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Settings\\DeleteUserForm", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "deleteUser", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "App\\Livewire\\Actions\\Logout", + "Illuminate\\Support\\Facades\\Auth" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 8, + "vocabulary": 5, + "volume": 18.58, + "difficulty": 0, + "effort": 0, + "level": 1.25, + "bugs": 0.01, + "time": 0, + "intelligentContent": 23.22, + "number_operators": 0, + "number_operands": 8, + "number_operators_unique": 0, + "number_operands_unique": 5, + "cloc": 3, + "loc": 15, + "lloc": 12, + "mi": 99.37, + "mIwoC": 67.44, + "commentWeight": 31.94, + "kanDefect": 0.15, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.17, + "relativeSystemComplexity": 25.17, + "totalStructuralComplexity": 25, + "totalDataComplexity": 0.17, + "totalSystemComplexity": 25.17, + "package": "App\\Livewire\\Settings\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Settings\\TwoFactor\\RecoveryCodes", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "regenerateRecoveryCodes", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "loadRecoveryCodes", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 6, + "ccn": 4, + "ccnMethodMax": 4, + "externals": [ + "Livewire\\Component", + "Laravel\\Fortify\\Actions\\GenerateNewRecoveryCodes" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 19, + "vocabulary": 9, + "volume": 60.23, + "difficulty": 5.2, + "effort": 313.19, + "level": 0.19, + "bugs": 0.02, + "time": 17, + "intelligentContent": 11.58, + "number_operators": 6, + "number_operands": 13, + "number_operators_unique": 4, + "number_operands_unique": 5, + "cloc": 10, + "loc": 36, + "lloc": 26, + "mi": 92.57, + "mIwoC": 56.13, + "commentWeight": 36.44, + "kanDefect": 0.22, + "relativeStructuralComplexity": 16, + "relativeDataComplexity": 0.07, + "relativeSystemComplexity": 16.07, + "totalStructuralComplexity": 48, + "totalDataComplexity": 0.2, + "totalSystemComplexity": 48.2, + "package": "App\\Livewire\\Settings\\TwoFactor\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Settings\\Password", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "updatePassword", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Facades\\Auth" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 26, + "vocabulary": 10, + "volume": 86.37, + "difficulty": 3, + "effort": 259.11, + "level": 0.33, + "bugs": 0.03, + "time": 14, + "intelligentContent": 28.79, + "number_operators": 2, + "number_operands": 24, + "number_operators_unique": 2, + "number_operands_unique": 8, + "cloc": 3, + "loc": 23, + "lloc": 20, + "mi": 84.33, + "mIwoC": 57.79, + "commentWeight": 26.54, + "kanDefect": 0.15, + "relativeStructuralComplexity": 49, + "relativeDataComplexity": 0, + "relativeSystemComplexity": 49, + "totalStructuralComplexity": 49, + "totalDataComplexity": 0, + "totalSystemComplexity": 49, + "package": "App\\Livewire\\Settings\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Settings\\Profile", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updateProfileInformation", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "resendVerificationNotification", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "hasUnverifiedEmail", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "showDeleteUser", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 10, + "ccn": 6, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Session", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Auth" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 5, + "length": 39, + "vocabulary": 14, + "volume": 148.49, + "difficulty": 6.94, + "effort": 1031.16, + "level": 0.14, + "bugs": 0.05, + "time": 57, + "intelligentContent": 21.38, + "number_operators": 14, + "number_operands": 25, + "number_operators_unique": 5, + "number_operands_unique": 9, + "cloc": 11, + "loc": 52, + "lloc": 41, + "mi": 81.49, + "mIwoC": 48.81, + "commentWeight": 32.69, + "kanDefect": 0.29, + "relativeStructuralComplexity": 144, + "relativeDataComplexity": 0.23, + "relativeSystemComplexity": 144.23, + "totalStructuralComplexity": 720, + "totalDataComplexity": 1.15, + "totalSystemComplexity": 721.15, + "package": "App\\Livewire\\Settings\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Settings\\Appearance", + "interface": false, + "abstract": false, + "final": false, + "methods": [], + "nbMethodsIncludingGettersSetters": 0, + "nbMethods": 0, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 0, + "ccn": 1, + "ccnMethodMax": 0, + "externals": [ + "Livewire\\Component" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 0, + "length": 0, + "vocabulary": 0, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 0, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 0, + "number_operators_unique": 0, + "number_operands_unique": 0, + "cloc": 1, + "loc": 5, + "lloc": 4, + "mi": 202.94, + "mIwoC": 171, + "commentWeight": 31.94, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 0, + "relativeSystemComplexity": 0, + "totalStructuralComplexity": 0, + "totalDataComplexity": 0, + "totalSystemComplexity": 0, + "package": "App\\Livewire\\Settings\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 1, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Products\\Show", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "incrementQuantity", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "decrementQuantity", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "selectVariant", + "role": "setter", + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "addToCart", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 6, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 1, + "wmc": 9, + "ccn": 5, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "App\\Support\\CartSession", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Product" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 84, + "vocabulary": 31, + "volume": 416.15, + "difficulty": 10.78, + "effort": 4487.21, + "level": 0.09, + "bugs": 0.14, + "time": 249, + "intelligentContent": 38.59, + "number_operators": 22, + "number_operands": 62, + "number_operators_unique": 8, + "number_operands_unique": 23, + "cloc": 1, + "loc": 53, + "lloc": 52, + "mi": 54.11, + "mIwoC": 43.55, + "commentWeight": 10.56, + "kanDefect": 0.29, + "relativeStructuralComplexity": 256, + "relativeDataComplexity": 0.2, + "relativeSystemComplexity": 256.2, + "totalStructuralComplexity": 1536, + "totalDataComplexity": 1.18, + "totalSystemComplexity": 1537.18, + "package": "App\\Livewire\\Storefront\\Products\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Home", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "featuredCollections", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "recentProducts", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 2, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 8, + "ccn": 5, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "App\\Models\\Store", + "Illuminate\\Contracts\\View\\View", + "Illuminate\\Support\\Collection", + "App\\Models\\Collection", + "Illuminate\\Support\\Collection", + "App\\Models\\Product" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 25, + "vocabulary": 14, + "volume": 95.18, + "difficulty": 2.8, + "effort": 266.51, + "level": 0.36, + "bugs": 0.03, + "time": 15, + "intelligentContent": 33.99, + "number_operators": 11, + "number_operands": 14, + "number_operators_unique": 4, + "number_operands_unique": 10, + "cloc": 7, + "loc": 38, + "lloc": 31, + "mi": 83.79, + "mIwoC": 52.94, + "commentWeight": 30.85, + "kanDefect": 0.43, + "relativeStructuralComplexity": 144, + "relativeDataComplexity": 0.38, + "relativeSystemComplexity": 144.38, + "totalStructuralComplexity": 576, + "totalDataComplexity": 1.54, + "totalSystemComplexity": 577.54, + "package": "App\\Livewire\\Storefront\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 6, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Checkout\\Show", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "continueToShipping", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "continueToPayment", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "backToAddress", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "backToShipping", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "placeOrder", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "getOrCreateCheckout", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "shippingAddressPayload", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "addressToPayload", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "computeTotals", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 11, + "nbMethods": 11, + "nbMethodsPrivate": 4, + "nbMethodsPublic": 7, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 35, + "ccn": 25, + "ccnMethodMax": 9, + "externals": [ + "Livewire\\Component", + "App\\Support\\CartSession", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\Order", + "App\\Support\\CartSession", + "Illuminate\\Contracts\\View\\View", + "App\\Support\\CartSession", + "App\\Models\\Checkout", + "App\\Support\\CartSession" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 300, + "vocabulary": 87, + "volume": 1932.88, + "difficulty": 18.4, + "effort": 35565.05, + "level": 0.05, + "bugs": 0.64, + "time": 1976, + "intelligentContent": 105.05, + "number_operators": 70, + "number_operands": 230, + "number_operators_unique": 12, + "number_operands_unique": 75, + "cloc": 14, + "loc": 135, + "lloc": 121, + "mi": 52.12, + "mIwoC": 28.19, + "commentWeight": 23.92, + "kanDefect": 1.01, + "relativeStructuralComplexity": 900, + "relativeDataComplexity": 0.27, + "relativeSystemComplexity": 900.27, + "totalStructuralComplexity": 9900, + "totalDataComplexity": 2.94, + "totalSystemComplexity": 9902.94, + "package": "App\\Livewire\\Storefront\\Checkout\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 6, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Checkout\\Confirmation", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "App\\Models\\Order", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 14, + "vocabulary": 10, + "volume": 46.51, + "difficulty": 2.36, + "effort": 109.62, + "level": 0.42, + "bugs": 0.02, + "time": 6, + "intelligentContent": 19.73, + "number_operators": 3, + "number_operands": 11, + "number_operators_unique": 3, + "number_operands_unique": 7, + "cloc": 1, + "loc": 16, + "lloc": 15, + "mi": 81.42, + "mIwoC": 62.53, + "commentWeight": 18.88, + "kanDefect": 0.15, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.25, + "relativeSystemComplexity": 25.25, + "totalStructuralComplexity": 50, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 50.5, + "package": "App\\Livewire\\Storefront\\Checkout\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Search\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updatedQ", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Product" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 22, + "vocabulary": 12, + "volume": 78.87, + "difficulty": 3, + "effort": 236.61, + "level": 0.33, + "bugs": 0.03, + "time": 13, + "intelligentContent": 26.29, + "number_operators": 4, + "number_operands": 18, + "number_operators_unique": 3, + "number_operands_unique": 9, + "cloc": 2, + "loc": 22, + "lloc": 20, + "mi": 80.58, + "mIwoC": 58.07, + "commentWeight": 22.51, + "kanDefect": 0.15, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 0.14, + "relativeSystemComplexity": 36.14, + "totalStructuralComplexity": 108, + "totalDataComplexity": 0.43, + "totalSystemComplexity": 108.43, + "package": "App\\Livewire\\Storefront\\Search\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\CartDrawer", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "refreshCart", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 3, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Support\\CartSession" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 34, + "vocabulary": 15, + "volume": 132.83, + "difficulty": 6.5, + "effort": 863.42, + "level": 0.15, + "bugs": 0.04, + "time": 48, + "intelligentContent": 20.44, + "number_operators": 8, + "number_operands": 26, + "number_operators_unique": 5, + "number_operands_unique": 10, + "cloc": 2, + "loc": 29, + "lloc": 27, + "mi": 73.29, + "mIwoC": 53.51, + "commentWeight": 19.79, + "kanDefect": 0.45, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 0.33, + "relativeSystemComplexity": 4.33, + "totalStructuralComplexity": 12, + "totalDataComplexity": 1, + "totalSystemComplexity": 13, + "package": "App\\Livewire\\Storefront\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Cart\\Show", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updateQty", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "removeLine", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "applyDiscount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 10, + "ccn": 6, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "App\\Support\\CartSession", + "App\\Support\\CartSession", + "Illuminate\\Contracts\\View\\View", + "App\\Support\\CartSession" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 63, + "vocabulary": 23, + "volume": 284.98, + "difficulty": 10.28, + "effort": 2930, + "level": 0.1, + "bugs": 0.09, + "time": 163, + "intelligentContent": 27.72, + "number_operators": 16, + "number_operands": 47, + "number_operators_unique": 7, + "number_operands_unique": 16, + "cloc": 1, + "loc": 52, + "lloc": 51, + "mi": 55.41, + "mIwoC": 44.76, + "commentWeight": 10.66, + "kanDefect": 0.59, + "relativeStructuralComplexity": 49, + "relativeDataComplexity": 0.45, + "relativeSystemComplexity": 49.45, + "totalStructuralComplexity": 245, + "totalDataComplexity": 2.25, + "totalSystemComplexity": 247.25, + "package": "App\\Livewire\\Storefront\\Cart\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Account\\Dashboard", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\Order" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 17, + "vocabulary": 10, + "volume": 56.47, + "difficulty": 1.75, + "effort": 98.83, + "level": 0.57, + "bugs": 0.02, + "time": 5, + "intelligentContent": 32.27, + "number_operators": 3, + "number_operands": 14, + "number_operators_unique": 2, + "number_operands_unique": 8, + "cloc": 2, + "loc": 17, + "lloc": 15, + "mi": 87.28, + "mIwoC": 61.94, + "commentWeight": 25.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 64, + "relativeDataComplexity": 0.11, + "relativeSystemComplexity": 64.11, + "totalStructuralComplexity": 128, + "totalDataComplexity": 0.22, + "totalSystemComplexity": 128.22, + "package": "App\\Livewire\\Storefront\\Account\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Account\\Auth\\Login", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "login", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 6, + "ccn": 4, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\Customer", + "Illuminate\\Support\\Facades\\Hash", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 40, + "vocabulary": 19, + "volume": 169.92, + "difficulty": 5.71, + "effort": 970.95, + "level": 0.18, + "bugs": 0.06, + "time": 54, + "intelligentContent": 29.74, + "number_operators": 8, + "number_operands": 32, + "number_operators_unique": 5, + "number_operands_unique": 14, + "cloc": 1, + "loc": 33, + "lloc": 32, + "mi": 64.33, + "mIwoC": 51.01, + "commentWeight": 13.32, + "kanDefect": 0.29, + "relativeStructuralComplexity": 169, + "relativeDataComplexity": 0.14, + "relativeSystemComplexity": 169.14, + "totalStructuralComplexity": 507, + "totalDataComplexity": 0.43, + "totalSystemComplexity": 507.43, + "package": "App\\Livewire\\Storefront\\Account\\Auth\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 5, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Account\\Auth\\Register", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "register", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Validation\\Rule", + "App\\Models\\Customer", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 45, + "vocabulary": 21, + "volume": 197.65, + "difficulty": 3.42, + "effort": 675.32, + "level": 0.29, + "bugs": 0.07, + "time": 38, + "intelligentContent": 57.85, + "number_operators": 4, + "number_operands": 41, + "number_operators_unique": 3, + "number_operands_unique": 18, + "cloc": 2, + "loc": 32, + "lloc": 30, + "mi": 70.32, + "mIwoC": 51.43, + "commentWeight": 18.88, + "kanDefect": 0.22, + "relativeStructuralComplexity": 121, + "relativeDataComplexity": 0.08, + "relativeSystemComplexity": 121.08, + "totalStructuralComplexity": 363, + "totalDataComplexity": 0.25, + "totalSystemComplexity": 363.25, + "package": "App\\Livewire\\Storefront\\Account\\Auth\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 5, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Account\\Addresses\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "addAddress", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "makeDefault", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "deleteAddress", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 6, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\CustomerAddress", + "App\\Models\\CustomerAddress", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\CustomerAddress", + "App\\Models\\CustomerAddress", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\CustomerAddress", + "Illuminate\\Contracts\\View\\View", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\CustomerAddress" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 5, + "length": 101, + "vocabulary": 35, + "volume": 518.06, + "difficulty": 4.31, + "effort": 2234.12, + "level": 0.23, + "bugs": 0.17, + "time": 124, + "intelligentContent": 120.13, + "number_operators": 9, + "number_operands": 92, + "number_operators_unique": 3, + "number_operands_unique": 32, + "cloc": 5, + "loc": 52, + "lloc": 47, + "mi": 67.36, + "mIwoC": 44.25, + "commentWeight": 23.11, + "kanDefect": 0.22, + "relativeStructuralComplexity": 169, + "relativeDataComplexity": 0.1, + "relativeSystemComplexity": 169.1, + "totalStructuralComplexity": 845, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 845.5, + "package": "App\\Livewire\\Storefront\\Account\\Addresses\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Account\\Orders\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\Order" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 15, + "vocabulary": 10, + "volume": 49.83, + "difficulty": 1.5, + "effort": 74.74, + "level": 0.67, + "bugs": 0.02, + "time": 4, + "intelligentContent": 33.22, + "number_operators": 3, + "number_operands": 12, + "number_operators_unique": 2, + "number_operands_unique": 8, + "cloc": 2, + "loc": 17, + "lloc": 15, + "mi": 87.66, + "mIwoC": 62.32, + "commentWeight": 25.34, + "kanDefect": 0.15, + "relativeStructuralComplexity": 49, + "relativeDataComplexity": 0.13, + "relativeSystemComplexity": 49.13, + "totalStructuralComplexity": 98, + "totalDataComplexity": 0.25, + "totalSystemComplexity": 98.25, + "package": "App\\Livewire\\Storefront\\Account\\Orders\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Account\\Orders\\Show", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\Order", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 19, + "vocabulary": 13, + "volume": 70.31, + "difficulty": 2.25, + "effort": 158.19, + "level": 0.44, + "bugs": 0.02, + "time": 9, + "intelligentContent": 31.25, + "number_operators": 4, + "number_operands": 15, + "number_operators_unique": 3, + "number_operands_unique": 10, + "cloc": 2, + "loc": 18, + "lloc": 16, + "mi": 85.35, + "mIwoC": 60.67, + "commentWeight": 24.69, + "kanDefect": 0.15, + "relativeStructuralComplexity": 64, + "relativeDataComplexity": 0.17, + "relativeSystemComplexity": 64.17, + "totalStructuralComplexity": 128, + "totalDataComplexity": 0.33, + "totalSystemComplexity": 128.33, + "package": "App\\Livewire\\Storefront\\Account\\Orders\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Collections\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Collection" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 11, + "vocabulary": 9, + "volume": 34.87, + "difficulty": 1.29, + "effort": 44.83, + "level": 0.78, + "bugs": 0.01, + "time": 2, + "intelligentContent": 27.12, + "number_operators": 2, + "number_operands": 9, + "number_operators_unique": 2, + "number_operands_unique": 7, + "cloc": 1, + "loc": 15, + "lloc": 14, + "mi": 83.53, + "mIwoC": 64.06, + "commentWeight": 19.47, + "kanDefect": 0.15, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 0.14, + "relativeSystemComplexity": 36.14, + "totalStructuralComplexity": 72, + "totalDataComplexity": 0.29, + "totalSystemComplexity": 72.29, + "package": "App\\Livewire\\Storefront\\Collections\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Collections\\Show", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updatedSort", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 3, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Collection" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 41, + "vocabulary": 20, + "volume": 177.2, + "difficulty": 4.13, + "effort": 730.95, + "level": 0.24, + "bugs": 0.06, + "time": 41, + "intelligentContent": 42.96, + "number_operators": 8, + "number_operands": 33, + "number_operators_unique": 4, + "number_operands_unique": 16, + "cloc": 2, + "loc": 32, + "lloc": 30, + "mi": 70.52, + "mIwoC": 51.63, + "commentWeight": 18.88, + "kanDefect": 0.22, + "relativeStructuralComplexity": 121, + "relativeDataComplexity": 0.11, + "relativeSystemComplexity": 121.11, + "totalStructuralComplexity": 363, + "totalDataComplexity": 0.33, + "totalSystemComplexity": 363.33, + "package": "App\\Livewire\\Storefront\\Collections\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Pages\\Show", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Page" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 16, + "vocabulary": 9, + "volume": 50.72, + "difficulty": 1.86, + "effort": 94.19, + "level": 0.54, + "bugs": 0.02, + "time": 5, + "intelligentContent": 27.31, + "number_operators": 3, + "number_operands": 13, + "number_operators_unique": 2, + "number_operands_unique": 7, + "cloc": 1, + "loc": 17, + "lloc": 16, + "mi": 80.01, + "mIwoC": 61.66, + "commentWeight": 18.35, + "kanDefect": 0.15, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.25, + "relativeSystemComplexity": 25.25, + "totalStructuralComplexity": 50, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 50.5, + "package": "App\\Livewire\\Storefront\\Pages\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Storefront\\Concerns\\EnsuresStore", + "interface": false, + "abstract": true, + "final": false, + "methods": [ + { + "name": "ensureCurrentStore", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 3, + "ccnMethodMax": 3, + "externals": [ + "App\\Models\\Store", + "App\\Models\\Store", + "App\\Models\\Store" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 12, + "vocabulary": 7, + "volume": 33.69, + "difficulty": 4.67, + "effort": 157.21, + "level": 0.21, + "bugs": 0.01, + "time": 9, + "intelligentContent": 7.22, + "number_operators": 5, + "number_operands": 7, + "number_operators_unique": 4, + "number_operands_unique": 3, + "cloc": 2, + "loc": 15, + "lloc": 13, + "mi": 91.4, + "mIwoC": 64.6, + "commentWeight": 26.8, + "kanDefect": 0.22, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.17, + "relativeSystemComplexity": 25.17, + "totalStructuralComplexity": 25, + "totalDataComplexity": 0.17, + "totalSystemComplexity": 25.17, + "package": "App\\Livewire\\Storefront\\Concerns\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 1, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Customers\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "updatingSearch", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Customer" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 36, + "vocabulary": 19, + "volume": 152.93, + "difficulty": 3.87, + "effort": 591.31, + "level": 0.26, + "bugs": 0.05, + "time": 33, + "intelligentContent": 39.55, + "number_operators": 7, + "number_operands": 29, + "number_operators_unique": 4, + "number_operands_unique": 15, + "cloc": 2, + "loc": 22, + "lloc": 20, + "mi": 78.7, + "mIwoC": 56.19, + "commentWeight": 22.51, + "kanDefect": 0.15, + "relativeStructuralComplexity": 100, + "relativeDataComplexity": 0.09, + "relativeSystemComplexity": 100.09, + "totalStructuralComplexity": 200, + "totalDataComplexity": 0.18, + "totalSystemComplexity": 200.18, + "package": "App\\Livewire\\Admin\\Customers\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Customers\\Show", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "App\\Models\\Customer", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 35, + "vocabulary": 19, + "volume": 148.68, + "difficulty": 3.87, + "effort": 574.89, + "level": 0.26, + "bugs": 0.05, + "time": 32, + "intelligentContent": 38.45, + "number_operators": 6, + "number_operands": 29, + "number_operators_unique": 4, + "number_operands_unique": 15, + "cloc": 1, + "loc": 16, + "lloc": 15, + "mi": 77.75, + "mIwoC": 58.87, + "commentWeight": 18.88, + "kanDefect": 0.15, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.25, + "relativeSystemComplexity": 25.25, + "totalStructuralComplexity": 50, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 50.5, + "package": "App\\Livewire\\Admin\\Customers\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Settings\\Taxes", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "save", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 7, + "ccn": 5, + "ccnMethodMax": 5, + "externals": [ + "Livewire\\Component", + "App\\Models\\TaxSettings", + "App\\Models\\TaxSettings", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 61, + "vocabulary": 28, + "volume": 293.25, + "difficulty": 2.88, + "effort": 844.56, + "level": 0.35, + "bugs": 0.1, + "time": 47, + "intelligentContent": 101.82, + "number_operators": 13, + "number_operands": 48, + "number_operators_unique": 3, + "number_operands_unique": 25, + "cloc": 6, + "loc": 35, + "lloc": 29, + "mi": 80.07, + "mIwoC": 50.15, + "commentWeight": 29.92, + "kanDefect": 0.15, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.17, + "relativeSystemComplexity": 25.17, + "totalStructuralComplexity": 75, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 75.5, + "package": "App\\Livewire\\Admin\\Settings\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Settings\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "save", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 45, + "vocabulary": 22, + "volume": 200.67, + "difficulty": 1.9, + "effort": 381.28, + "level": 0.53, + "bugs": 0.07, + "time": 21, + "intelligentContent": 105.62, + "number_operators": 7, + "number_operands": 38, + "number_operators_unique": 2, + "number_operands_unique": 20, + "cloc": 7, + "loc": 34, + "lloc": 27, + "mi": 84.84, + "mIwoC": 52.52, + "commentWeight": 32.32, + "kanDefect": 0.15, + "relativeStructuralComplexity": 9, + "relativeDataComplexity": 0.25, + "relativeSystemComplexity": 9.25, + "totalStructuralComplexity": 27, + "totalDataComplexity": 0.75, + "totalSystemComplexity": 27.75, + "package": "App\\Livewire\\Admin\\Settings\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Settings\\Shipping", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "openZoneModal", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "createZone", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "deleteZone", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "openRateModal", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "createRate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "deleteRate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 7, + "nbMethods": 7, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 7, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 8, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "App\\Models\\ShippingZone", + "App\\Models\\ShippingZone", + "App\\Models\\ShippingRate", + "App\\Models\\ShippingRate", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\ShippingZone" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 4, + "length": 103, + "vocabulary": 44, + "volume": 562.32, + "difficulty": 4.35, + "effort": 2446.1, + "level": 0.23, + "bugs": 0.19, + "time": 136, + "intelligentContent": 129.27, + "number_operators": 16, + "number_operands": 87, + "number_operators_unique": 4, + "number_operands_unique": 40, + "cloc": 7, + "loc": 68, + "lloc": 61, + "mi": 65.37, + "mIwoC": 41.53, + "commentWeight": 23.84, + "kanDefect": 0.22, + "relativeStructuralComplexity": 196, + "relativeDataComplexity": 0.16, + "relativeSystemComplexity": 196.16, + "totalStructuralComplexity": 1372, + "totalDataComplexity": 1.13, + "totalSystemComplexity": 1373.13, + "package": "App\\Livewire\\Admin\\Settings\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Dashboard", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "kpis", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "recentOrders", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "periodStart", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "App\\Models\\Order", + "Illuminate\\Support\\Collection", + "App\\Models\\Order", + "DateTimeInterface", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 3, + "length": 50, + "vocabulary": 28, + "volume": 240.37, + "difficulty": 3.25, + "effort": 781.2, + "level": 0.31, + "bugs": 0.08, + "time": 43, + "intelligentContent": 73.96, + "number_operators": 11, + "number_operands": 39, + "number_operators_unique": 4, + "number_operands_unique": 24, + "cloc": 9, + "loc": 39, + "lloc": 30, + "mi": 84.71, + "mIwoC": 50.84, + "commentWeight": 33.87, + "kanDefect": 0.15, + "relativeStructuralComplexity": 121, + "relativeDataComplexity": 0.33, + "relativeSystemComplexity": 121.33, + "totalStructuralComplexity": 484, + "totalDataComplexity": 1.33, + "totalSystemComplexity": 485.33, + "package": "App\\Livewire\\Admin\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 5, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Products\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "updatingSearch", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updatingStatusFilter", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "bulkArchive", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "bulkDelete", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 7, + "ccn": 3, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "App\\Models\\Product", + "App\\Models\\Product", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Product" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 56, + "vocabulary": 20, + "volume": 242.03, + "difficulty": 9, + "effort": 2178.25, + "level": 0.11, + "bugs": 0.08, + "time": 121, + "intelligentContent": 26.89, + "number_operators": 14, + "number_operands": 42, + "number_operators_unique": 6, + "number_operands_unique": 14, + "cloc": 4, + "loc": 42, + "lloc": 38, + "mi": 71.45, + "mIwoC": 48.44, + "commentWeight": 23, + "kanDefect": 0.29, + "relativeStructuralComplexity": 169, + "relativeDataComplexity": 0.21, + "relativeSystemComplexity": 169.21, + "totalStructuralComplexity": 845, + "totalDataComplexity": 1.07, + "totalSystemComplexity": 846.07, + "package": "App\\Livewire\\Admin\\Products\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Products\\Form", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "save", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 22, + "ccn": 20, + "ccnMethodMax": 11, + "externals": [ + "Livewire\\Component", + "App\\Services\\ProductService", + "Illuminate\\Contracts\\View\\View", + "App\\Enums\\ProductStatus" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 181, + "vocabulary": 47, + "volume": 1005.38, + "difficulty": 11.9, + "effort": 11964.03, + "level": 0.08, + "bugs": 0.34, + "time": 665, + "intelligentContent": 84.49, + "number_operators": 45, + "number_operands": 136, + "number_operators_unique": 7, + "number_operands_unique": 40, + "cloc": 12, + "loc": 74, + "lloc": 62, + "mi": 66.4, + "mIwoC": 37.19, + "commentWeight": 29.21, + "kanDefect": 0.5, + "relativeStructuralComplexity": 144, + "relativeDataComplexity": 0.21, + "relativeSystemComplexity": 144.21, + "totalStructuralComplexity": 432, + "totalDataComplexity": 0.62, + "totalSystemComplexity": 432.62, + "package": "App\\Livewire\\Admin\\Products\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Auth\\Login", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "login", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "throttleKey", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 6, + "ccn": 4, + "ccnMethodMax": 4, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Facades\\RateLimiter", + "Illuminate\\Support\\Facades\\RateLimiter", + "Illuminate\\Validation\\ValidationException", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\RateLimiter", + "Illuminate\\Validation\\ValidationException", + "Illuminate\\Support\\Facades\\RateLimiter", + "Illuminate\\Support\\Facades\\Session", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Session", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 60, + "vocabulary": 30, + "volume": 294.41, + "difficulty": 5.63, + "effort": 1656.08, + "level": 0.18, + "bugs": 0.1, + "time": 92, + "intelligentContent": 52.34, + "number_operators": 15, + "number_operands": 45, + "number_operators_unique": 6, + "number_operands_unique": 24, + "cloc": 3, + "loc": 40, + "lloc": 37, + "mi": 68.55, + "mIwoC": 47.97, + "commentWeight": 20.58, + "kanDefect": 0.36, + "relativeStructuralComplexity": 256, + "relativeDataComplexity": 0.18, + "relativeSystemComplexity": 256.18, + "totalStructuralComplexity": 768, + "totalDataComplexity": 0.53, + "totalSystemComplexity": 768.53, + "package": "App\\Livewire\\Admin\\Auth\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 6, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Navigation\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "createMenu", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "deleteMenu", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "openItemModal", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "closeItemModal", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "addItem", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "deleteItem", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "moveItem", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 8, + "nbMethods": 8, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 8, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 14, + "ccn": 7, + "ccnMethodMax": 6, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Str", + "App\\Models\\NavigationMenu", + "App\\Models\\NavigationMenu", + "App\\Models\\NavigationMenu", + "App\\Models\\NavigationItem", + "App\\Models\\NavigationItem", + "App\\Models\\NavigationItem", + "App\\Models\\NavigationItem", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\NavigationMenu" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 5, + "length": 151, + "vocabulary": 52, + "volume": 860.77, + "difficulty": 9.64, + "effort": 8301.61, + "level": 0.1, + "bugs": 0.29, + "time": 461, + "intelligentContent": 89.25, + "number_operators": 27, + "number_operands": 124, + "number_operators_unique": 7, + "number_operands_unique": 45, + "cloc": 7, + "loc": 78, + "lloc": 71, + "mi": 60.51, + "mIwoC": 38.13, + "commentWeight": 22.38, + "kanDefect": 0.29, + "relativeStructuralComplexity": 729, + "relativeDataComplexity": 0.13, + "relativeSystemComplexity": 729.13, + "totalStructuralComplexity": 5832, + "totalDataComplexity": 1.04, + "totalSystemComplexity": 5833.04, + "package": "App\\Livewire\\Admin\\Navigation\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 5, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Discounts\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "updatingStatusFilter", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updatingTypeFilter", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Discount" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 27, + "vocabulary": 12, + "volume": 96.79, + "difficulty": 3.83, + "effort": 371.04, + "level": 0.26, + "bugs": 0.03, + "time": 21, + "intelligentContent": 25.25, + "number_operators": 4, + "number_operands": 23, + "number_operators_unique": 3, + "number_operands_unique": 9, + "cloc": 3, + "loc": 24, + "lloc": 21, + "mi": 83.16, + "mIwoC": 57.12, + "commentWeight": 26.04, + "kanDefect": 0.15, + "relativeStructuralComplexity": 49, + "relativeDataComplexity": 0.13, + "relativeSystemComplexity": 49.13, + "totalStructuralComplexity": 147, + "totalDataComplexity": 0.38, + "totalSystemComplexity": 147.38, + "package": "App\\Livewire\\Admin\\Discounts\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Discounts\\Form", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "save", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 14, + "ccn": 12, + "ccnMethodMax": 8, + "externals": [ + "Livewire\\Component", + "App\\Models\\Discount", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 132, + "vocabulary": 46, + "volume": 729.11, + "difficulty": 10.21, + "effort": 7444.6, + "level": 0.1, + "bugs": 0.24, + "time": 414, + "intelligentContent": 71.41, + "number_operators": 35, + "number_operands": 97, + "number_operators_unique": 8, + "number_operands_unique": 38, + "cloc": 11, + "loc": 67, + "lloc": 56, + "mi": 69.57, + "mIwoC": 40.21, + "commentWeight": 29.36, + "kanDefect": 0.43, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 0.48, + "relativeSystemComplexity": 36.48, + "totalStructuralComplexity": 108, + "totalDataComplexity": 1.43, + "totalSystemComplexity": 109.43, + "package": "App\\Livewire\\Admin\\Discounts\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Orders\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "updatingSearch", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updatingStatusFilter", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updatingFinancialFilter", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updatingFulfillmentFilter", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Order" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 62, + "vocabulary": 21, + "volume": 272.32, + "difficulty": 6.12, + "effort": 1665.98, + "level": 0.16, + "bugs": 0.09, + "time": 93, + "intelligentContent": 44.51, + "number_operators": 10, + "number_operands": 52, + "number_operators_unique": 4, + "number_operands_unique": 17, + "cloc": 5, + "loc": 40, + "lloc": 35, + "mi": 75.17, + "mIwoC": 49.13, + "commentWeight": 26.04, + "kanDefect": 0.15, + "relativeStructuralComplexity": 100, + "relativeDataComplexity": 0.09, + "relativeSystemComplexity": 100.09, + "totalStructuralComplexity": 500, + "totalDataComplexity": 0.45, + "totalSystemComplexity": 500.45, + "package": "App\\Livewire\\Admin\\Orders\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Orders\\Show", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "openFulfillModal", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "openRefundModal", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "createFulfillment", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "markShipped", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "markDelivered", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "createRefund", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "confirmBankTransfer", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "cancelOrder", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 10, + "nbMethods": 10, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 10, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 22, + "ccn": 13, + "ccnMethodMax": 6, + "externals": [ + "Livewire\\Component", + "App\\Models\\Order", + "App\\Services\\FulfillmentService", + "App\\Services\\FulfillmentService", + "App\\Services\\FulfillmentService", + "App\\Services\\RefundService", + "App\\Services\\OrderService", + "App\\Services\\OrderService", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 159, + "vocabulary": 40, + "volume": 846.19, + "difficulty": 13.89, + "effort": 11756.86, + "level": 0.07, + "bugs": 0.28, + "time": 653, + "intelligentContent": 60.9, + "number_operators": 28, + "number_operands": 131, + "number_operators_unique": 7, + "number_operands_unique": 33, + "cloc": 2, + "loc": 101, + "lloc": 99, + "mi": 45.03, + "mIwoC": 34.22, + "commentWeight": 10.81, + "kanDefect": 0.52, + "relativeStructuralComplexity": 289, + "relativeDataComplexity": 0.22, + "relativeSystemComplexity": 289.22, + "totalStructuralComplexity": 2890, + "totalDataComplexity": 2.17, + "totalSystemComplexity": 2892.17, + "package": "App\\Livewire\\Admin\\Orders\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 6, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Collections\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "updatingSearch", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Collection" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 25, + "vocabulary": 15, + "volume": 97.67, + "difficulty": 3.64, + "effort": 355.17, + "level": 0.28, + "bugs": 0.03, + "time": 20, + "intelligentContent": 26.86, + "number_operators": 5, + "number_operands": 20, + "number_operators_unique": 4, + "number_operands_unique": 11, + "cloc": 2, + "loc": 18, + "lloc": 16, + "mi": 84.35, + "mIwoC": 59.67, + "commentWeight": 24.69, + "kanDefect": 0.15, + "relativeStructuralComplexity": 49, + "relativeDataComplexity": 0.13, + "relativeSystemComplexity": 49.13, + "totalStructuralComplexity": 98, + "totalDataComplexity": 0.25, + "totalSystemComplexity": 98.25, + "package": "App\\Livewire\\Admin\\Collections\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Collections\\Form", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "addProduct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "removeProduct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "save", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 18, + "ccn": 14, + "ccnMethodMax": 8, + "externals": [ + "Livewire\\Component", + "App\\Support\\HandleGenerator", + "App\\Support\\HandleGenerator", + "App\\Models\\Collection", + "App\\Support\\HandleGenerator", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Product", + "App\\Models\\Product" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 177, + "vocabulary": 45, + "volume": 972.06, + "difficulty": 14.92, + "effort": 14502.05, + "level": 0.07, + "bugs": 0.32, + "time": 806, + "intelligentContent": 65.16, + "number_operators": 39, + "number_operands": 138, + "number_operators_unique": 8, + "number_operands_unique": 37, + "cloc": 8, + "loc": 70, + "lloc": 62, + "mi": 63.1, + "mIwoC": 38.1, + "commentWeight": 25.01, + "kanDefect": 0.59, + "relativeStructuralComplexity": 324, + "relativeDataComplexity": 0.14, + "relativeSystemComplexity": 324.14, + "totalStructuralComplexity": 1620, + "totalDataComplexity": 0.68, + "totalSystemComplexity": 1620.68, + "package": "App\\Livewire\\Admin\\Collections\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 5, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Pages\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "updatingSearch", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "delete", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "App\\Models\\Page", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Page" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 3, + "length": 32, + "vocabulary": 19, + "volume": 135.93, + "difficulty": 3.47, + "effort": 471.24, + "level": 0.29, + "bugs": 0.05, + "time": 26, + "intelligentContent": 39.21, + "number_operators": 6, + "number_operands": 26, + "number_operators_unique": 4, + "number_operands_unique": 15, + "cloc": 2, + "loc": 24, + "lloc": 22, + "mi": 77.27, + "mIwoC": 55.64, + "commentWeight": 21.62, + "kanDefect": 0.15, + "relativeStructuralComplexity": 81, + "relativeDataComplexity": 0.13, + "relativeSystemComplexity": 81.13, + "totalStructuralComplexity": 243, + "totalDataComplexity": 0.4, + "totalSystemComplexity": 243.4, + "package": "App\\Livewire\\Admin\\Pages\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Pages\\Form", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "save", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 15, + "ccn": 13, + "ccnMethodMax": 10, + "externals": [ + "Livewire\\Component", + "App\\Support\\HandleGenerator", + "App\\Support\\HandleGenerator", + "App\\Models\\Page", + "App\\Support\\HandleGenerator", + "Illuminate\\Contracts\\View\\View" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 2, + "length": 126, + "vocabulary": 34, + "volume": 641.02, + "difficulty": 12.31, + "effort": 7894.05, + "level": 0.08, + "bugs": 0.21, + "time": 439, + "intelligentContent": 52.05, + "number_operators": 31, + "number_operands": 95, + "number_operators_unique": 7, + "number_operands_unique": 27, + "cloc": 7, + "loc": 52, + "lloc": 45, + "mi": 69.45, + "mIwoC": 42.53, + "commentWeight": 26.91, + "kanDefect": 0.36, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 0.33, + "relativeSystemComplexity": 36.33, + "totalStructuralComplexity": 108, + "totalDataComplexity": 1, + "totalSystemComplexity": 109, + "package": "App\\Livewire\\Admin\\Pages\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Apps\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "install", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "uninstall", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "App\\Models\\AppInstallation", + "App\\Models\\AppInstallation", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\AppInstallation", + "App\\Models\\App" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 3, + "length": 71, + "vocabulary": 24, + "volume": 325.53, + "difficulty": 5.9, + "effort": 1920.64, + "level": 0.17, + "bugs": 0.11, + "time": 107, + "intelligentContent": 55.17, + "number_operators": 12, + "number_operands": 59, + "number_operators_unique": 4, + "number_operands_unique": 20, + "cloc": 4, + "loc": 33, + "lloc": 29, + "mi": 76.05, + "mIwoC": 50.37, + "commentWeight": 25.68, + "kanDefect": 0.15, + "relativeStructuralComplexity": 256, + "relativeDataComplexity": 0.16, + "relativeSystemComplexity": 256.16, + "totalStructuralComplexity": 768, + "totalDataComplexity": 0.47, + "totalSystemComplexity": 768.47, + "package": "App\\Livewire\\Admin\\Apps\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Themes\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "publish", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "duplicate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "delete", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Livewire\\Component", + "App\\Models\\Theme", + "App\\Models\\Theme", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Theme", + "App\\Models\\Theme", + "App\\Models\\Theme", + "Illuminate\\Contracts\\View\\View", + "App\\Models\\Theme" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 4, + "length": 61, + "vocabulary": 24, + "volume": 279.68, + "difficulty": 6.71, + "effort": 1876.82, + "level": 0.15, + "bugs": 0.09, + "time": 104, + "intelligentContent": 41.68, + "number_operators": 10, + "number_operands": 51, + "number_operators_unique": 5, + "number_operands_unique": 19, + "cloc": 3, + "loc": 38, + "lloc": 35, + "mi": 70, + "mIwoC": 48.92, + "commentWeight": 21.08, + "kanDefect": 0.22, + "relativeStructuralComplexity": 169, + "relativeDataComplexity": 0.2, + "relativeSystemComplexity": 169.2, + "totalStructuralComplexity": 676, + "totalDataComplexity": 0.79, + "totalSystemComplexity": 676.79, + "package": "App\\Livewire\\Admin\\Themes\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 4, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Analytics\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "mount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 3, + "ccnMethodMax": 3, + "externals": [ + "Livewire\\Component", + "Illuminate\\Contracts\\View\\View", + "App\\Services\\AnalyticsService" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 1, + "length": 62, + "vocabulary": 29, + "volume": 301.19, + "difficulty": 5.42, + "effort": 1631.47, + "level": 0.18, + "bugs": 0.1, + "time": 91, + "intelligentContent": 55.61, + "number_operators": 10, + "number_operands": 52, + "number_operators_unique": 5, + "number_operands_unique": 24, + "cloc": 2, + "loc": 21, + "lloc": 19, + "mi": 77.35, + "mIwoC": 54.34, + "commentWeight": 23, + "kanDefect": 0.15, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.25, + "relativeSystemComplexity": 25.25, + "totalStructuralComplexity": 50, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 50.5, + "package": "App\\Livewire\\Admin\\Analytics\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Admin\\Developers\\Index", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "createToken", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "revokeToken", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "createWebhook", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "deleteWebhook", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "render", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Livewire\\Component", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Str", + "App\\Models\\WebhookSubscription", + "App\\Models\\WebhookSubscription", + "Illuminate\\Contracts\\View\\View", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\WebhookSubscription" + ], + "parents": [ + "Livewire\\Component" + ], + "implements": [], + "lcom": 4, + "length": 78, + "vocabulary": 35, + "volume": 400.08, + "difficulty": 2.03, + "effort": 812.29, + "level": 0.49, + "bugs": 0.13, + "time": 45, + "intelligentContent": 197.06, + "number_operators": 11, + "number_operands": 67, + "number_operators_unique": 2, + "number_operands_unique": 33, + "cloc": 8, + "loc": 52, + "lloc": 44, + "mi": 74.34, + "mIwoC": 45.8, + "commentWeight": 28.55, + "kanDefect": 0.15, + "relativeStructuralComplexity": 289, + "relativeDataComplexity": 0.08, + "relativeSystemComplexity": 289.08, + "totalStructuralComplexity": 1445, + "totalDataComplexity": 0.39, + "totalSystemComplexity": 1445.39, + "package": "App\\Livewire\\Admin\\Developers\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 5, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Livewire\\Actions\\Logout", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__invoke", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Support\\Facades\\Auth", + "Illuminate\\Support\\Facades\\Session", + "Illuminate\\Support\\Facades\\Session" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 3, + "vocabulary": 3, + "volume": 4.75, + "difficulty": 0.5, + "effort": 2.38, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 9.51, + "number_operators": 1, + "number_operands": 2, + "number_operators_unique": 1, + "number_operands_unique": 2, + "cloc": 3, + "loc": 14, + "lloc": 11, + "mi": 105.27, + "mIwoC": 72.41, + "commentWeight": 32.86, + "kanDefect": 0.15, + "relativeStructuralComplexity": 16, + "relativeDataComplexity": 0.2, + "relativeSystemComplexity": 16.2, + "totalStructuralComplexity": 16, + "totalDataComplexity": 0.2, + "totalSystemComplexity": 16.2, + "package": "App\\Livewire\\Actions\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Support\\HandleGenerator", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "generate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "exists", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 4, + "ccnMethodMax": 3, + "externals": [ + "Illuminate\\Support\\Str", + "Illuminate\\Support\\Facades\\DB" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 55, + "vocabulary": 23, + "volume": 248.8, + "difficulty": 8.75, + "effort": 2176.96, + "level": 0.11, + "bugs": 0.08, + "time": 121, + "intelligentContent": 28.43, + "number_operators": 15, + "number_operands": 40, + "number_operators_unique": 7, + "number_operands_unique": 16, + "cloc": 0, + "loc": 26, + "lloc": 26, + "mi": 51.82, + "mIwoC": 51.82, + "commentWeight": 0, + "kanDefect": 0.52, + "relativeStructuralComplexity": 49, + "relativeDataComplexity": 0.75, + "relativeSystemComplexity": 49.75, + "totalStructuralComplexity": 98, + "totalDataComplexity": 1.5, + "totalSystemComplexity": 99.5, + "package": "App\\Support\\", + "pageRank": 0.01, + "afferentCoupling": 3, + "efferentCoupling": 2, + "instability": 0.4, + "violations": {} + }, + { + "name": "App\\Support\\CartSession", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "getOrCreate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "current", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "clear", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 9, + "ccn": 7, + "ccnMethodMax": 4, + "externals": [ + "App\\Models\\Cart", + "App\\Models\\Store", + "App\\Models\\Cart", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\Cart" + ], + "parents": [], + "implements": [], + "lcom": 3, + "length": 49, + "vocabulary": 13, + "volume": 181.32, + "difficulty": 15.17, + "effort": 2750.04, + "level": 0.07, + "bugs": 0.06, + "time": 153, + "intelligentContent": 11.96, + "number_operators": 23, + "number_operands": 26, + "number_operators_unique": 7, + "number_operands_unique": 6, + "cloc": 0, + "loc": 34, + "lloc": 34, + "mi": 49.84, + "mIwoC": 49.84, + "commentWeight": 0, + "kanDefect": 0.43, + "relativeStructuralComplexity": 49, + "relativeDataComplexity": 0.67, + "relativeSystemComplexity": 49.67, + "totalStructuralComplexity": 147, + "totalDataComplexity": 2, + "totalSystemComplexity": 149, + "package": "App\\Support\\", + "pageRank": 0.02, + "afferentCoupling": 4, + "efferentCoupling": 3, + "instability": 0.43, + "violations": {} + }, + { + "name": "App\\Http\\Middleware\\ResolveStore", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "handle", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "resolveFromHost", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "resolveFromSession", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 2, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 10, + "ccn": 8, + "ccnMethodMax": 5, + "externals": [ + "Symfony\\Component\\HttpFoundation\\Response", + "Illuminate\\Http\\Request", + "Closure", + "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException", + "Symfony\\Component\\HttpKernel\\Exception\\HttpException", + "Illuminate\\Http\\Request", + "App\\Models\\StoreDomain", + "Illuminate\\Support\\Facades\\Cache", + "App\\Models\\Store", + "Illuminate\\Http\\Request", + "Illuminate\\Support\\Facades\\Auth", + "App\\Models\\Store" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 83, + "vocabulary": 26, + "volume": 390.14, + "difficulty": 6.19, + "effort": 2415.13, + "level": 0.16, + "bugs": 0.13, + "time": 134, + "intelligentContent": 63.02, + "number_operators": 31, + "number_operands": 52, + "number_operators_unique": 5, + "number_operands_unique": 21, + "cloc": 0, + "loc": 52, + "lloc": 52, + "mi": 43.35, + "mIwoC": 43.35, + "commentWeight": 0, + "kanDefect": 0.64, + "relativeStructuralComplexity": 324, + "relativeDataComplexity": 0.56, + "relativeSystemComplexity": 324.56, + "totalStructuralComplexity": 972, + "totalDataComplexity": 1.68, + "totalSystemComplexity": 973.68, + "package": "App\\Http\\Middleware\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 9, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Http\\Controllers\\Controller", + "interface": false, + "abstract": true, + "final": false, + "methods": [], + "nbMethodsIncludingGettersSetters": 0, + "nbMethods": 0, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 0, + "ccn": 1, + "ccnMethodMax": 0, + "externals": [], + "parents": [], + "implements": [], + "lcom": 0, + "length": 0, + "vocabulary": 0, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 0, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 0, + "number_operators_unique": 0, + "number_operands_unique": 0, + "cloc": 1, + "loc": 5, + "lloc": 4, + "mi": 202.94, + "mIwoC": 171, + "commentWeight": 31.94, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 0, + "relativeSystemComplexity": 0, + "totalStructuralComplexity": 0, + "totalDataComplexity": 0, + "totalSystemComplexity": 0, + "package": "App\\Http\\Controllers\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 0, + "instability": 0, + "violations": {} + }, + { + "name": "App\\Actions\\Fortify\\ResetUserPassword", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "reset", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Laravel\\Fortify\\Contracts\\ResetsUserPasswords", + "App\\Models\\User", + "Illuminate\\Support\\Facades\\Validator" + ], + "parents": [], + "implements": [ + "Laravel\\Fortify\\Contracts\\ResetsUserPasswords" + ], + "lcom": 1, + "length": 9, + "vocabulary": 4, + "volume": 18, + "difficulty": 0, + "effort": 0, + "level": 0.89, + "bugs": 0.01, + "time": 0, + "intelligentContent": 16, + "number_operators": 0, + "number_operands": 9, + "number_operators_unique": 0, + "number_operands_unique": 4, + "cloc": 5, + "loc": 15, + "lloc": 10, + "mi": 108.25, + "mIwoC": 69.26, + "commentWeight": 38.99, + "kanDefect": 0.15, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.33, + "relativeSystemComplexity": 25.33, + "totalStructuralComplexity": 25, + "totalDataComplexity": 0.33, + "totalSystemComplexity": 25.33, + "package": "App\\Actions\\Fortify\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Actions\\Fortify\\CreateNewUser", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "create", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Laravel\\Fortify\\Contracts\\CreatesNewUsers", + "App\\Models\\User", + "Illuminate\\Support\\Facades\\Validator", + "App\\Models\\User" + ], + "parents": [], + "implements": [ + "Laravel\\Fortify\\Contracts\\CreatesNewUsers" + ], + "lcom": 1, + "length": 15, + "vocabulary": 6, + "volume": 38.77, + "difficulty": 1.4, + "effort": 54.28, + "level": 0.71, + "bugs": 0.01, + "time": 3, + "intelligentContent": 27.7, + "number_operators": 1, + "number_operands": 14, + "number_operators_unique": 1, + "number_operands_unique": 5, + "cloc": 5, + "loc": 15, + "lloc": 10, + "mi": 105.92, + "mIwoC": 66.93, + "commentWeight": 38.99, + "kanDefect": 0.15, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.33, + "relativeSystemComplexity": 25.33, + "totalStructuralComplexity": 25, + "totalDataComplexity": 0.33, + "totalSystemComplexity": 25.33, + "package": "App\\Actions\\Fortify\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Jobs\\ExpireAbandonedCheckouts", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "handle", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue", + "App\\Services\\CheckoutService", + "App\\Models\\Checkout" + ], + "parents": [], + "implements": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue" + ], + "lcom": 1, + "length": 9, + "vocabulary": 5, + "volume": 20.9, + "difficulty": 0, + "effort": 0, + "level": 1.11, + "bugs": 0.01, + "time": 0, + "intelligentContent": 23.22, + "number_operators": 0, + "number_operands": 9, + "number_operators_unique": 0, + "number_operands_unique": 5, + "cloc": 0, + "loc": 11, + "lloc": 11, + "mi": 67.9, + "mIwoC": 67.9, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 64, + "relativeDataComplexity": 0.11, + "relativeSystemComplexity": 64.11, + "totalStructuralComplexity": 64, + "totalDataComplexity": 0.11, + "totalSystemComplexity": 64.11, + "package": "App\\Jobs\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Jobs\\CleanupAbandonedCarts", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "handle", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue", + "App\\Models\\Cart" + ], + "parents": [], + "implements": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue" + ], + "lcom": 1, + "length": 5, + "vocabulary": 4, + "volume": 10, + "difficulty": 0, + "effort": 0, + "level": 1.6, + "bugs": 0, + "time": 0, + "intelligentContent": 16, + "number_operators": 0, + "number_operands": 5, + "number_operators_unique": 0, + "number_operands_unique": 4, + "cloc": 0, + "loc": 9, + "lloc": 9, + "mi": 72.05, + "mIwoC": 72.05, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 0, + "relativeSystemComplexity": 36, + "totalStructuralComplexity": 36, + "totalDataComplexity": 0, + "totalSystemComplexity": 36, + "package": "App\\Jobs\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Jobs\\AggregateAnalytics", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "handle", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 3, + "ccnMethodMax": 3, + "externals": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue", + "Carbon\\CarbonImmutable", + "Carbon\\CarbonImmutable", + "App\\Models\\Store", + "App\\Models\\Order", + "App\\Models\\AnalyticsEvent", + "Illuminate\\Support\\Facades\\DB" + ], + "parents": [], + "implements": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue" + ], + "lcom": 2, + "length": 100, + "vocabulary": 40, + "volume": 532.19, + "difficulty": 4.67, + "effort": 2483.57, + "level": 0.21, + "bugs": 0.18, + "time": 138, + "intelligentContent": 114.04, + "number_operators": 16, + "number_operands": 84, + "number_operators_unique": 4, + "number_operands_unique": 36, + "cloc": 0, + "loc": 27, + "lloc": 27, + "mi": 49.28, + "mIwoC": 49.28, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 676, + "relativeDataComplexity": 0.02, + "relativeSystemComplexity": 676.02, + "totalStructuralComplexity": 1352, + "totalDataComplexity": 0.04, + "totalSystemComplexity": 1352.04, + "package": "App\\Jobs\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 6, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Jobs\\CancelUnpaidBankTransferOrders", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "handle", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue", + "App\\Services\\OrderService", + "App\\Models\\Order" + ], + "parents": [], + "implements": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue" + ], + "lcom": 1, + "length": 11, + "vocabulary": 8, + "volume": 33, + "difficulty": 0, + "effort": 0, + "level": 1.45, + "bugs": 0.01, + "time": 0, + "intelligentContent": 48, + "number_operators": 0, + "number_operands": 11, + "number_operators_unique": 0, + "number_operands_unique": 8, + "cloc": 0, + "loc": 11, + "lloc": 11, + "mi": 66.52, + "mIwoC": 66.52, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 64, + "relativeDataComplexity": 0.11, + "relativeSystemComplexity": 64.11, + "totalStructuralComplexity": 64, + "totalDataComplexity": 0.11, + "totalSystemComplexity": 64.11, + "package": "App\\Jobs\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Jobs\\ProcessMediaUpload", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "handle", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue", + "App\\Models\\ProductMedia", + "Illuminate\\Support\\Facades\\Log" + ], + "parents": [], + "implements": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue" + ], + "lcom": 2, + "length": 9, + "vocabulary": 6, + "volume": 23.26, + "difficulty": 0.8, + "effort": 18.61, + "level": 1.25, + "bugs": 0.01, + "time": 1, + "intelligentContent": 29.08, + "number_operators": 1, + "number_operands": 8, + "number_operators_unique": 1, + "number_operands_unique": 5, + "cloc": 0, + "loc": 14, + "lloc": 14, + "mi": 65.29, + "mIwoC": 65.29, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 0.17, + "relativeSystemComplexity": 4.17, + "totalStructuralComplexity": 8, + "totalDataComplexity": 0.33, + "totalSystemComplexity": 8.33, + "package": "App\\Jobs\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Jobs\\DeliverWebhook", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "handle", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "recordFailure", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 10, + "ccn": 8, + "ccnMethodMax": 6, + "externals": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue", + "App\\Models\\WebhookSubscription", + "App\\Services\\WebhookService", + "Illuminate\\Support\\Str", + "App\\Models\\WebhookDelivery", + "Illuminate\\Support\\Facades\\Http", + "RuntimeException", + "App\\Models\\WebhookSubscription" + ], + "parents": [], + "implements": [ + "Illuminate\\Contracts\\Queue\\ShouldQueue" + ], + "lcom": 2, + "length": 108, + "vocabulary": 53, + "volume": 618.62, + "difficulty": 11.39, + "effort": 7047.8, + "level": 0.09, + "bugs": 0.21, + "time": 392, + "intelligentContent": 54.3, + "number_operators": 21, + "number_operands": 87, + "number_operators_unique": 11, + "number_operands_unique": 42, + "cloc": 7, + "loc": 51, + "lloc": 44, + "mi": 70.68, + "mIwoC": 43.53, + "commentWeight": 27.15, + "kanDefect": 0.36, + "relativeStructuralComplexity": 361, + "relativeDataComplexity": 0.18, + "relativeSystemComplexity": 361.18, + "totalStructuralComplexity": 1083, + "totalDataComplexity": 0.55, + "totalSystemComplexity": 1083.55, + "package": "App\\Jobs\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 7, + "instability": 0.88, + "violations": {} + }, + { + "name": "App\\Events\\OrderRefunded", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Models\\Order", + "App\\Models\\Refund" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 2, + "vocabulary": 2, + "volume": 2, + "difficulty": 0, + "effort": 0, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 4, + "number_operators": 0, + "number_operands": 2, + "number_operators_unique": 0, + "number_operands_unique": 2, + "cloc": 0, + "loc": 8, + "lloc": 8, + "mi": 78.06, + "mIwoC": 78.06, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 2, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 0, + "totalDataComplexity": 2, + "totalSystemComplexity": 2, + "package": "App\\Events\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Events\\OrderCancelled", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Models\\Order" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 1, + "vocabulary": 1, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 1, + "number_operators_unique": 0, + "number_operands_unique": 1, + "cloc": 0, + "loc": 8, + "lloc": 8, + "mi": 171, + "mIwoC": 171, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 1, + "totalStructuralComplexity": 0, + "totalDataComplexity": 1, + "totalSystemComplexity": 1, + "package": "App\\Events\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 1, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Events\\OrderCreated", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Models\\Order" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 1, + "vocabulary": 1, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 1, + "number_operators_unique": 0, + "number_operands_unique": 1, + "cloc": 0, + "loc": 8, + "lloc": 8, + "mi": 171, + "mIwoC": 171, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 1, + "totalStructuralComplexity": 0, + "totalDataComplexity": 1, + "totalSystemComplexity": 1, + "package": "App\\Events\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 1, + "instability": 0.33, + "violations": {} + }, + { + "name": "App\\Events\\OrderPaid", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Models\\Order" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 1, + "vocabulary": 1, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 1, + "number_operators_unique": 0, + "number_operands_unique": 1, + "cloc": 0, + "loc": 8, + "lloc": 8, + "mi": 171, + "mIwoC": 171, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 1, + "totalStructuralComplexity": 0, + "totalDataComplexity": 1, + "totalSystemComplexity": 1, + "package": "App\\Events\\", + "pageRank": 0.01, + "afferentCoupling": 3, + "efferentCoupling": 1, + "instability": 0.25, + "violations": {} + }, + { + "name": "App\\Events\\FulfillmentDelivered", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Models\\Fulfillment" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 1, + "vocabulary": 1, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 1, + "number_operators_unique": 0, + "number_operands_unique": 1, + "cloc": 0, + "loc": 8, + "lloc": 8, + "mi": 171, + "mIwoC": 171, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 1, + "totalStructuralComplexity": 0, + "totalDataComplexity": 1, + "totalSystemComplexity": 1, + "package": "App\\Events\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 1, + "instability": 0.5, + "violations": {} + }, + { + "name": "App\\Events\\OrderFulfilled", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Models\\Order" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 1, + "vocabulary": 1, + "volume": 0, + "difficulty": 0, + "effort": 0, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 0, + "number_operators": 0, + "number_operands": 1, + "number_operators_unique": 0, + "number_operands_unique": 1, + "cloc": 0, + "loc": 8, + "lloc": 8, + "mi": 171, + "mIwoC": 171, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 1, + "totalStructuralComplexity": 0, + "totalDataComplexity": 1, + "totalSystemComplexity": 1, + "package": "App\\Events\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 1, + "instability": 0.33, + "violations": {} + }, + { + "name": "App\\Observers\\ProductObserver", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "created", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updated", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "deleted", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Services\\SearchService", + "App\\Models\\Product", + "App\\Models\\Product", + "App\\Models\\Product" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 10, + "vocabulary": 3, + "volume": 15.85, + "difficulty": 0, + "effort": 0, + "level": 0.6, + "bugs": 0.01, + "time": 0, + "intelligentContent": 9.51, + "number_operators": 0, + "number_operands": 10, + "number_operators_unique": 0, + "number_operands_unique": 3, + "cloc": 0, + "loc": 19, + "lloc": 19, + "mi": 63.57, + "mIwoC": 63.57, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 4, + "relativeDataComplexity": 0.33, + "relativeSystemComplexity": 4.33, + "totalStructuralComplexity": 16, + "totalDataComplexity": 1.33, + "totalSystemComplexity": 17.33, + "package": "App\\Observers\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 2, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Listeners\\DispatchOrderWebhooks", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "handleCreated", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "handlePaid", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "handleFulfilled", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "dispatch", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Services\\WebhookService", + "App\\Events\\OrderCreated", + "App\\Events\\OrderPaid", + "App\\Events\\OrderFulfilled", + "App\\Models\\Order" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 26, + "vocabulary": 12, + "volume": 93.21, + "difficulty": 0, + "effort": 0, + "level": 0.92, + "bugs": 0.03, + "time": 0, + "intelligentContent": 86.04, + "number_operators": 0, + "number_operands": 26, + "number_operators_unique": 0, + "number_operands_unique": 12, + "cloc": 0, + "loc": 23, + "lloc": 23, + "mi": 56.37, + "mIwoC": 56.37, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 0.6, + "relativeSystemComplexity": 1.6, + "totalStructuralComplexity": 5, + "totalDataComplexity": 3, + "totalSystemComplexity": 8, + "package": "App\\Listeners\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 5, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Services\\WebhookService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "dispatch", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "sign", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "verify", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "App\\Models\\Store", + "App\\Models\\WebhookSubscription", + "App\\Jobs\\DeliverWebhook" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 32, + "vocabulary": 17, + "volume": 130.8, + "difficulty": 3, + "effort": 392.4, + "level": 0.33, + "bugs": 0.04, + "time": 22, + "intelligentContent": 43.6, + "number_operators": 4, + "number_operands": 28, + "number_operators_unique": 3, + "number_operands_unique": 14, + "cloc": 3, + "loc": 22, + "lloc": 19, + "mi": 84.08, + "mIwoC": 57.02, + "commentWeight": 27.07, + "kanDefect": 0.38, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 0.67, + "relativeSystemComplexity": 36.67, + "totalStructuralComplexity": 108, + "totalDataComplexity": 2, + "totalSystemComplexity": 110, + "package": "App\\Services\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 3, + "instability": 0.6, + "violations": {} + }, + { + "name": "App\\Services\\OrderService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "createFromCheckout", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "generateOrderNumber", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "cancel", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "confirmBankTransferPayment", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 28, + "ccn": 25, + "ccnMethodMax": 12, + "externals": [ + "App\\Models\\Order", + "App\\Models\\Checkout", + "App\\Models\\Store", + "App\\Models\\Order", + "App\\Models\\OrderLine", + "App\\Events\\OrderCreated", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Store", + "App\\Models\\Order", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Order", + "App\\Enums\\FulfillmentStatus", + "DomainException", + "App\\Models\\InventoryItem", + "App\\Events\\OrderCancelled", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Order", + "App\\Enums\\PaymentMethod", + "App\\Enums\\FinancialStatus", + "DomainException", + "App\\Models\\InventoryItem", + "App\\Events\\OrderPaid", + "Illuminate\\Support\\Facades\\DB" + ], + "parents": [], + "implements": [], + "lcom": 3, + "length": 237, + "vocabulary": 75, + "volume": 1476.23, + "difficulty": 14.31, + "effort": 21121.45, + "level": 0.07, + "bugs": 0.49, + "time": 1173, + "intelligentContent": 103.18, + "number_operators": 51, + "number_operands": 186, + "number_operators_unique": 10, + "number_operands_unique": 65, + "cloc": 2, + "loc": 81, + "lloc": 79, + "mi": 45.1, + "mIwoC": 33.05, + "commentWeight": 12.05, + "kanDefect": 1.4, + "relativeStructuralComplexity": 1089, + "relativeDataComplexity": 0.13, + "relativeSystemComplexity": 1089.13, + "totalStructuralComplexity": 4356, + "totalDataComplexity": 0.5, + "totalSystemComplexity": 4356.5, + "package": "App\\Services\\", + "pageRank": 0.01, + "afferentCoupling": 3, + "efferentCoupling": 13, + "instability": 0.81, + "violations": {} + }, + { + "name": "App\\Services\\Payments\\MockPaymentProvider", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "charge", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "refund", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "chargeCreditCard", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "generateId", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 2, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 8, + "ccn": 5, + "ccnMethodMax": 4, + "externals": [ + "App\\Contracts\\PaymentProvider", + "App\\ValueObjects\\PaymentResult", + "App\\Models\\Checkout", + "App\\Enums\\PaymentMethod", + "App\\ValueObjects\\PaymentResult", + "App\\ValueObjects\\PaymentResult", + "App\\ValueObjects\\RefundResult", + "App\\Models\\Payment", + "Illuminate\\Support\\Str", + "App\\ValueObjects\\RefundResult", + "App\\ValueObjects\\PaymentResult", + "App\\ValueObjects\\PaymentResult", + "App\\ValueObjects\\PaymentResult", + "App\\ValueObjects\\PaymentResult", + "Illuminate\\Support\\Str" + ], + "parents": [], + "implements": [ + "App\\Contracts\\PaymentProvider" + ], + "lcom": 2, + "length": 69, + "vocabulary": 29, + "volume": 335.2, + "difficulty": 4.4, + "effort": 1474.88, + "level": 0.23, + "bugs": 0.11, + "time": 82, + "intelligentContent": 76.18, + "number_operators": 14, + "number_operands": 55, + "number_operators_unique": 4, + "number_operands_unique": 25, + "cloc": 6, + "loc": 41, + "lloc": 35, + "mi": 75.89, + "mIwoC": 47.96, + "commentWeight": 27.93, + "kanDefect": 0.15, + "relativeStructuralComplexity": 9, + "relativeDataComplexity": 1.5, + "relativeSystemComplexity": 10.5, + "totalStructuralComplexity": 36, + "totalDataComplexity": 6, + "totalSystemComplexity": 42, + "package": "App\\Services\\Payments\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 7, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Services\\CheckoutService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "start", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "setAddress", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "setShippingMethod", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "selectPaymentMethod", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "applyDiscount", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "recalculate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "expire", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "complete", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "releaseReservedInventory", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "commitReservedInventory", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "assertTransitionAllowed", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 12, + "nbMethods": 12, + "nbMethodsPrivate": 3, + "nbMethodsPublic": 9, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 41, + "ccn": 30, + "ccnMethodMax": 8, + "externals": [ + "App\\Services\\PricingEngine", + "App\\Services\\InventoryService", + "App\\Services\\OrderService", + "App\\Contracts\\PaymentProvider", + "App\\Models\\Checkout", + "App\\Models\\Cart", + "App\\Models\\Checkout", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "App\\Models\\ShippingRate", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "DomainException", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Checkout", + "App\\Models\\Checkout", + "DomainException", + "App\\Enums\\PaymentMethod", + "App\\Exceptions\\PaymentFailedException", + "App\\Models\\Payment", + "App\\Events\\OrderPaid", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Checkout", + "App\\Models\\Order", + "App\\Models\\InventoryItem", + "App\\Models\\Checkout", + "App\\Enums\\CheckoutStatus", + "DomainException" + ], + "parents": [], + "implements": [], + "lcom": 3, + "length": 312, + "vocabulary": 59, + "volume": 1835.38, + "difficulty": 15.28, + "effort": 28042.56, + "level": 0.07, + "bugs": 0.61, + "time": 1558, + "intelligentContent": 120.13, + "number_operators": 85, + "number_operands": 227, + "number_operators_unique": 7, + "number_operands_unique": 52, + "cloc": 10, + "loc": 170, + "lloc": 160, + "mi": 43.38, + "mIwoC": 25.03, + "commentWeight": 18.35, + "kanDefect": 2.12, + "relativeStructuralComplexity": 1225, + "relativeDataComplexity": 0.35, + "relativeSystemComplexity": 1225.35, + "totalStructuralComplexity": 14700, + "totalDataComplexity": 4.25, + "totalSystemComplexity": 14704.25, + "package": "App\\Services\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 16, + "instability": 0.94, + "violations": {} + }, + { + "name": "App\\Services\\FulfillmentService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "create", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "markAsShipped", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "markAsDelivered", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updateOrderFulfillmentStatus", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 17, + "ccn": 14, + "ccnMethodMax": 7, + "externals": [ + "App\\Models\\Fulfillment", + "App\\Models\\Order", + "App\\Enums\\FinancialStatus", + "App\\Exceptions\\FulfillmentGuardException", + "App\\Models\\Fulfillment", + "App\\Models\\FulfillmentLine", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Fulfillment", + "App\\Models\\Fulfillment", + "App\\Events\\FulfillmentDelivered", + "App\\Models\\Order", + "App\\Models\\FulfillmentLine", + "App\\Events\\OrderFulfilled" + ], + "parents": [], + "implements": [], + "lcom": 3, + "length": 117, + "vocabulary": 37, + "volume": 609.51, + "difficulty": 12.55, + "effort": 7650.35, + "level": 0.08, + "bugs": 0.2, + "time": 425, + "intelligentContent": 48.56, + "number_operators": 26, + "number_operands": 91, + "number_operators_unique": 8, + "number_operands_unique": 29, + "cloc": 8, + "loc": 56, + "lloc": 48, + "mi": 69.57, + "mIwoC": 41.94, + "commentWeight": 27.63, + "kanDefect": 0.66, + "relativeStructuralComplexity": 196, + "relativeDataComplexity": 0.32, + "relativeSystemComplexity": 196.32, + "totalStructuralComplexity": 784, + "totalDataComplexity": 1.27, + "totalSystemComplexity": 785.27, + "package": "App\\Services\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 8, + "instability": 0.89, + "violations": {} + }, + { + "name": "App\\Services\\TaxCalculator", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "addExclusive", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "extractInclusive", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "calculate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 12, + "ccn": 10, + "ccnMethodMax": 6, + "externals": [ + "App\\Models\\TaxSettings", + "App\\ValueObjects\\TaxLine" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 88, + "vocabulary": 30, + "volume": 431.81, + "difficulty": 14.25, + "effort": 6153.24, + "level": 0.07, + "bugs": 0.14, + "time": 342, + "intelligentContent": 30.3, + "number_operators": 31, + "number_operands": 57, + "number_operators_unique": 10, + "number_operands_unique": 20, + "cloc": 4, + "loc": 34, + "lloc": 30, + "mi": 73.32, + "mIwoC": 47.98, + "commentWeight": 25.34, + "kanDefect": 0.36, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 4.17, + "relativeSystemComplexity": 5.17, + "totalStructuralComplexity": 3, + "totalDataComplexity": 12.5, + "totalSystemComplexity": 15.5, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Services\\ThemeSettingsService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "forStore", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "defaultSettings", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "forgetStore", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 5, + "ccn": 3, + "ccnMethodMax": 3, + "externals": [ + "App\\Models\\Store", + "App\\Models\\Theme", + "Illuminate\\Support\\Facades\\Cache", + "App\\Models\\Store", + "Illuminate\\Support\\Facades\\Cache" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 35, + "vocabulary": 21, + "volume": 153.73, + "difficulty": 4.06, + "effort": 624.53, + "level": 0.25, + "bugs": 0.05, + "time": 35, + "intelligentContent": 37.84, + "number_operators": 9, + "number_operands": 26, + "number_operators_unique": 5, + "number_operands_unique": 16, + "cloc": 9, + "loc": 32, + "lloc": 23, + "mi": 91.19, + "mIwoC": 54.58, + "commentWeight": 36.61, + "kanDefect": 0.22, + "relativeStructuralComplexity": 81, + "relativeDataComplexity": 0.47, + "relativeSystemComplexity": 81.47, + "totalStructuralComplexity": 243, + "totalDataComplexity": 1.4, + "totalSystemComplexity": 244.4, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Services\\InventoryService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "checkAvailability", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "reserve", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "release", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "commit", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "restock", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 8, + "ccn": 4, + "ccnMethodMax": 4, + "externals": [ + "App\\Models\\InventoryItem", + "App\\Models\\InventoryItem", + "App\\Enums\\InventoryPolicy", + "App\\Exceptions\\InsufficientInventoryException", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\InventoryItem", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\InventoryItem", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\InventoryItem", + "Illuminate\\Support\\Facades\\DB" + ], + "parents": [], + "implements": [], + "lcom": 5, + "length": 78, + "vocabulary": 16, + "volume": 312, + "difficulty": 39.21, + "effort": 12234.86, + "level": 0.03, + "bugs": 0.1, + "time": 680, + "intelligentContent": 7.96, + "number_operators": 17, + "number_operands": 61, + "number_operators_unique": 9, + "number_operands_unique": 7, + "cloc": 0, + "loc": 45, + "lloc": 45, + "mi": 45.93, + "mIwoC": 45.93, + "commentWeight": 0, + "kanDefect": 0.22, + "relativeStructuralComplexity": 25, + "relativeDataComplexity": 0.5, + "relativeSystemComplexity": 25.5, + "totalStructuralComplexity": 125, + "totalDataComplexity": 2.5, + "totalSystemComplexity": 127.5, + "package": "App\\Services\\", + "pageRank": 0.01, + "afferentCoupling": 3, + "efferentCoupling": 4, + "instability": 0.57, + "violations": {} + }, + { + "name": "App\\Services\\NavigationService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "buildTree", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "resolveUrl", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "forgetMenu", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Models\\NavigationMenu", + "Illuminate\\Support\\Facades\\Cache", + "App\\Models\\NavigationItem", + "App\\Models\\NavigationMenu", + "Illuminate\\Support\\Facades\\Cache" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 27, + "vocabulary": 13, + "volume": 99.91, + "difficulty": 1.04, + "effort": 104.07, + "level": 0.96, + "bugs": 0.03, + "time": 6, + "intelligentContent": 95.92, + "number_operators": 2, + "number_operands": 25, + "number_operators_unique": 1, + "number_operands_unique": 12, + "cloc": 5, + "loc": 22, + "lloc": 17, + "mi": 92.68, + "mIwoC": 59.02, + "commentWeight": 33.66, + "kanDefect": 0.15, + "relativeStructuralComplexity": 64, + "relativeDataComplexity": 0.33, + "relativeSystemComplexity": 64.33, + "totalStructuralComplexity": 192, + "totalDataComplexity": 1, + "totalSystemComplexity": 193, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 3, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Services\\RefundService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "create", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 11, + "ccn": 10, + "ccnMethodMax": 10, + "externals": [ + "App\\Contracts\\PaymentProvider", + "App\\Services\\InventoryService", + "App\\Models\\Refund", + "App\\Models\\Order", + "App\\Models\\Payment", + "InvalidArgumentException", + "App\\Models\\Refund", + "App\\Models\\InventoryItem", + "App\\Events\\OrderRefunded", + "Illuminate\\Support\\Facades\\DB" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 92, + "vocabulary": 37, + "volume": 479.27, + "difficulty": 14.6, + "effort": 6995.49, + "level": 0.07, + "bugs": 0.16, + "time": 389, + "intelligentContent": 32.84, + "number_operators": 23, + "number_operands": 69, + "number_operators_unique": 11, + "number_operands_unique": 26, + "cloc": 1, + "loc": 40, + "lloc": 39, + "mi": 57.3, + "mIwoC": 45.18, + "commentWeight": 12.13, + "kanDefect": 0.73, + "relativeStructuralComplexity": 169, + "relativeDataComplexity": 0.39, + "relativeSystemComplexity": 169.39, + "totalStructuralComplexity": 338, + "totalDataComplexity": 0.79, + "totalSystemComplexity": 338.79, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 9, + "instability": 0.9, + "violations": {} + }, + { + "name": "App\\Services\\ProductService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "create", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "update", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "transitionStatus", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "delete", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "syncOptions", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 6, + "nbMethods": 6, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 5, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 38, + "ccn": 33, + "ccnMethodMax": 16, + "externals": [ + "App\\Services\\VariantMatrixService", + "App\\Models\\Product", + "App\\Models\\Store", + "App\\Support\\HandleGenerator", + "App\\Support\\HandleGenerator", + "App\\Models\\Product", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Product", + "App\\Models\\Product", + "App\\Support\\HandleGenerator", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Product", + "App\\Enums\\ProductStatus", + "App\\Enums\\ProductStatus", + "InvalidArgumentException", + "App\\Models\\Product", + "App\\Enums\\ProductStatus", + "InvalidArgumentException", + "App\\Models\\Product", + "Illuminate\\Support\\Facades\\DB", + "Illuminate\\Support\\Facades\\DB" + ], + "parents": [], + "implements": [], + "lcom": 4, + "length": 308, + "vocabulary": 57, + "volume": 1796.53, + "difficulty": 16.8, + "effort": 30181.71, + "level": 0.06, + "bugs": 0.6, + "time": 1677, + "intelligentContent": 106.94, + "number_operators": 68, + "number_operands": 240, + "number_operators_unique": 7, + "number_operands_unique": 50, + "cloc": 10, + "loc": 129, + "lloc": 119, + "mi": 48.4, + "mIwoC": 27.5, + "commentWeight": 20.9, + "kanDefect": 2, + "relativeStructuralComplexity": 900, + "relativeDataComplexity": 0.18, + "relativeSystemComplexity": 900.18, + "totalStructuralComplexity": 5400, + "totalDataComplexity": 1.1, + "totalSystemComplexity": 5401.1, + "package": "App\\Services\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 7, + "instability": 0.88, + "violations": {} + }, + { + "name": "App\\Services\\ShippingCalculator", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "getAvailableRates", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "calculate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "weightRate", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "priceRate", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 2, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 39, + "ccn": 36, + "ccnMethodMax": 12, + "externals": [ + "Illuminate\\Database\\Eloquent\\Collection", + "App\\Models\\Store", + "App\\Models\\ShippingZone", + "Illuminate\\Database\\Eloquent\\Collection", + "App\\Models\\ShippingRate", + "App\\Models\\ShippingRate", + "App\\Models\\Cart" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 213, + "vocabulary": 53, + "volume": 1220.05, + "difficulty": 22.91, + "effort": 27954.33, + "level": 0.04, + "bugs": 0.41, + "time": 1553, + "intelligentContent": 53.25, + "number_operators": 72, + "number_operands": 141, + "number_operators_unique": 13, + "number_operands_unique": 40, + "cloc": 12, + "loc": 86, + "lloc": 74, + "mi": 60.12, + "mIwoC": 32.77, + "commentWeight": 27.35, + "kanDefect": 1.49, + "relativeStructuralComplexity": 289, + "relativeDataComplexity": 0.72, + "relativeSystemComplexity": 289.72, + "totalStructuralComplexity": 1156, + "totalDataComplexity": 2.89, + "totalSystemComplexity": 1158.89, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 5, + "instability": 0.83, + "violations": {} + }, + { + "name": "App\\Services\\AnalyticsService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "track", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "getDailyMetrics", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 3, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "App\\Models\\AnalyticsEvent", + "App\\Models\\Store", + "App\\Models\\AnalyticsEvent", + "Illuminate\\Support\\Collection", + "App\\Models\\Store", + "App\\Models\\AnalyticsDaily" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 31, + "vocabulary": 16, + "volume": 124, + "difficulty": 2, + "effort": 248, + "level": 0.5, + "bugs": 0.04, + "time": 14, + "intelligentContent": 62, + "number_operators": 3, + "number_operands": 28, + "number_operators_unique": 2, + "number_operands_unique": 14, + "cloc": 6, + "loc": 18, + "lloc": 12, + "mi": 100.52, + "mIwoC": 61.53, + "commentWeight": 38.99, + "kanDefect": 0.15, + "relativeStructuralComplexity": 36, + "relativeDataComplexity": 0.86, + "relativeSystemComplexity": 36.86, + "totalStructuralComplexity": 72, + "totalDataComplexity": 1.71, + "totalSystemComplexity": 73.71, + "package": "App\\Services\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 4, + "instability": 0.8, + "violations": {} + }, + { + "name": "App\\Services\\CartService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "create", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "getOrCreateForSession", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "addLine", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "updateLineQuantity", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "removeLine", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "mergeOnLogin", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "touchVersion", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 8, + "nbMethods": 8, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 7, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 27, + "ccn": 20, + "ccnMethodMax": 10, + "externals": [ + "App\\Services\\InventoryService", + "App\\Models\\Cart", + "App\\Models\\Store", + "App\\Models\\Cart", + "App\\Models\\Cart", + "App\\Models\\Store", + "App\\Models\\Cart", + "App\\Models\\CartLine", + "App\\Models\\Cart", + "InvalidArgumentException", + "App\\Models\\ProductVariant", + "RuntimeException", + "RuntimeException", + "RuntimeException", + "RuntimeException", + "App\\Models\\CartLine", + "App\\Models\\CartLine", + "App\\Models\\Cart", + "InvalidArgumentException", + "App\\Models\\ProductVariant", + "RuntimeException", + "App\\Models\\Cart", + "App\\Models\\Cart", + "App\\Models\\Cart", + "App\\Models\\Cart", + "App\\Models\\Cart" + ], + "parents": [], + "implements": [], + "lcom": 3, + "length": 295, + "vocabulary": 45, + "volume": 1620.1, + "difficulty": 36.91, + "effort": 59796.3, + "level": 0.03, + "bugs": 0.54, + "time": 3322, + "intelligentContent": 43.89, + "number_operators": 92, + "number_operands": 203, + "number_operators_unique": 12, + "number_operands_unique": 33, + "cloc": 0, + "loc": 130, + "lloc": 130, + "mi": 28.72, + "mIwoC": 28.72, + "commentWeight": 0, + "kanDefect": 1.29, + "relativeStructuralComplexity": 729, + "relativeDataComplexity": 0.37, + "relativeSystemComplexity": 729.37, + "totalStructuralComplexity": 5832, + "totalDataComplexity": 2.93, + "totalSystemComplexity": 5834.93, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 7, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Services\\PricingEngine", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "calculate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 17, + "ccn": 16, + "ccnMethodMax": 16, + "externals": [ + "App\\Services\\DiscountService", + "App\\Services\\ShippingCalculator", + "App\\Services\\TaxCalculator", + "App\\ValueObjects\\PricingResult", + "App\\Models\\Checkout", + "App\\ValueObjects\\PricingResult", + "App\\Models\\Store", + "App\\Models\\ShippingRate", + "App\\Models\\TaxSettings", + "App\\ValueObjects\\TaxLine", + "App\\ValueObjects\\PricingResult" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 191, + "vocabulary": 49, + "volume": 1072.41, + "difficulty": 16.54, + "effort": 17736, + "level": 0.06, + "bugs": 0.36, + "time": 985, + "intelligentContent": 64.84, + "number_operators": 62, + "number_operands": 129, + "number_operators_unique": 10, + "number_operands_unique": 39, + "cloc": 0, + "loc": 59, + "lloc": 59, + "mi": 38, + "mIwoC": 38, + "commentWeight": 0, + "kanDefect": 0.64, + "relativeStructuralComplexity": 196, + "relativeDataComplexity": 0.27, + "relativeSystemComplexity": 196.27, + "totalStructuralComplexity": 392, + "totalDataComplexity": 0.53, + "totalSystemComplexity": 392.53, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 9, + "instability": 0.9, + "violations": {} + }, + { + "name": "App\\Services\\DiscountService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "validate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "calculate", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "recordUsage", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 3, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 23, + "ccn": 21, + "ccnMethodMax": 15, + "externals": [ + "App\\Models\\Discount", + "App\\Models\\Store", + "App\\Models\\Cart", + "App\\Models\\Discount", + "App\\Exceptions\\InvalidDiscountException", + "App\\Exceptions\\InvalidDiscountException", + "App\\Exceptions\\InvalidDiscountException", + "App\\Exceptions\\InvalidDiscountException", + "App\\Exceptions\\InvalidDiscountException", + "App\\Exceptions\\InvalidDiscountException", + "App\\Exceptions\\InvalidDiscountException", + "App\\ValueObjects\\DiscountResult", + "App\\Models\\Discount", + "App\\ValueObjects\\DiscountResult", + "App\\ValueObjects\\DiscountResult", + "App\\ValueObjects\\DiscountResult", + "App\\ValueObjects\\DiscountResult", + "App\\Models\\Discount", + "Illuminate\\Support\\Facades\\DB" + ], + "parents": [], + "implements": [], + "lcom": 3, + "length": 165, + "vocabulary": 42, + "volume": 889.73, + "difficulty": 32.62, + "effort": 29018.96, + "level": 0.03, + "bugs": 0.3, + "time": 1612, + "intelligentContent": 27.28, + "number_operators": 59, + "number_operands": 106, + "number_operators_unique": 16, + "number_operands_unique": 26, + "cloc": 3, + "loc": 80, + "lloc": 77, + "mi": 50.15, + "mIwoC": 35.37, + "commentWeight": 14.78, + "kanDefect": 1.22, + "relativeStructuralComplexity": 484, + "relativeDataComplexity": 0.32, + "relativeSystemComplexity": 484.32, + "totalStructuralComplexity": 1452, + "totalDataComplexity": 0.96, + "totalSystemComplexity": 1452.96, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 6, + "instability": 0.86, + "violations": {} + }, + { + "name": "App\\Services\\SearchService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "syncProduct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "removeProduct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "search", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "autocomplete", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "buildFtsQuery", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 5, + "nbMethods": 5, + "nbMethodsPrivate": 1, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 19, + "ccn": 15, + "ccnMethodMax": 7, + "externals": [ + "App\\Models\\Product", + "Illuminate\\Support\\Facades\\DB", + "Illuminate\\Support\\Facades\\DB", + "Illuminate\\Contracts\\Pagination\\LengthAwarePaginator", + "App\\Models\\Store", + "App\\Models\\Product", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Product", + "App\\Models\\Product", + "Illuminate\\Support\\Collection", + "App\\Models\\Store", + "Illuminate\\Support\\Facades\\DB", + "App\\Models\\Product" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 139, + "vocabulary": 45, + "volume": 763.37, + "difficulty": 12.5, + "effort": 9542.09, + "level": 0.08, + "bugs": 0.25, + "time": 530, + "intelligentContent": 61.07, + "number_operators": 39, + "number_operands": 100, + "number_operators_unique": 9, + "number_operands_unique": 36, + "cloc": 7, + "loc": 59, + "lloc": 52, + "mi": 65.8, + "mIwoC": 40.36, + "commentWeight": 25.43, + "kanDefect": 0.57, + "relativeStructuralComplexity": 361, + "relativeDataComplexity": 0.55, + "relativeSystemComplexity": 361.55, + "totalStructuralComplexity": 1805, + "totalDataComplexity": 2.75, + "totalSystemComplexity": 1807.75, + "package": "App\\Services\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 5, + "instability": 0.83, + "violations": {} + }, + { + "name": "App\\Services\\VariantMatrixService", + "interface": false, + "abstract": false, + "final": false, + "methods": [ + { + "name": "rebuildMatrix", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "cartesian", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "keyFor", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 2, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 15, + "ccn": 13, + "ccnMethodMax": 10, + "externals": [ + "App\\Models\\Product", + "Illuminate\\Support\\Facades\\DB" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 122, + "vocabulary": 39, + "volume": 644.82, + "difficulty": 10.06, + "effort": 6488.49, + "level": 0.1, + "bugs": 0.21, + "time": 360, + "intelligentContent": 64.08, + "number_operators": 30, + "number_operands": 92, + "number_operators_unique": 7, + "number_operands_unique": 32, + "cloc": 7, + "loc": 70, + "lloc": 63, + "mi": 62.86, + "mIwoC": 39.33, + "commentWeight": 23.53, + "kanDefect": 1.81, + "relativeStructuralComplexity": 289, + "relativeDataComplexity": 0.22, + "relativeSystemComplexity": 289.22, + "totalStructuralComplexity": 867, + "totalDataComplexity": 0.67, + "totalSystemComplexity": 867.67, + "package": "App\\Services\\", + "pageRank": 0, + "afferentCoupling": 1, + "efferentCoupling": 2, + "instability": 0.67, + "violations": {} + }, + { + "name": "App\\Concerns\\ProfileValidationRules", + "interface": false, + "abstract": true, + "final": false, + "methods": [ + { + "name": "profileRules", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "nameRules", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "emailRules", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 3, + "nbMethods": 3, + "nbMethodsPrivate": 3, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 2, + "ccnMethodMax": 2, + "externals": [ + "Illuminate\\Validation\\Rule", + "Illuminate\\Validation\\Rule" + ], + "parents": [], + "implements": [], + "lcom": 1, + "length": 20, + "vocabulary": 9, + "volume": 63.4, + "difficulty": 2.29, + "effort": 144.91, + "level": 0.44, + "bugs": 0.02, + "time": 8, + "intelligentContent": 27.74, + "number_operators": 4, + "number_operands": 16, + "number_operators_unique": 2, + "number_operands_unique": 7, + "cloc": 15, + "loc": 31, + "lloc": 16, + "mi": 104.89, + "mIwoC": 60.85, + "commentWeight": 44.04, + "kanDefect": 0.15, + "relativeStructuralComplexity": 16, + "relativeDataComplexity": 0.73, + "relativeSystemComplexity": 16.73, + "totalStructuralComplexity": 48, + "totalDataComplexity": 2.2, + "totalSystemComplexity": 50.2, + "package": "App\\Concerns\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 1, + "instability": 1, + "violations": {} + }, + { + "name": "App\\Concerns\\PasswordValidationRules", + "interface": false, + "abstract": true, + "final": false, + "methods": [ + { + "name": "passwordRules", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "currentPasswordRules", + "role": null, + "public": false, + "private": true, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 2, + "nbMethodsPublic": 0, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "Illuminate\\Validation\\Rules\\Password" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 8, + "vocabulary": 5, + "volume": 18.58, + "difficulty": 0.75, + "effort": 13.93, + "level": 1.33, + "bugs": 0.01, + "time": 1, + "intelligentContent": 24.77, + "number_operators": 2, + "number_operands": 6, + "number_operators_unique": 1, + "number_operands_unique": 4, + "cloc": 10, + "loc": 22, + "lloc": 12, + "mi": 110.67, + "mIwoC": 67.44, + "commentWeight": 43.23, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 1, + "relativeSystemComplexity": 2, + "totalStructuralComplexity": 2, + "totalDataComplexity": 2, + "totalSystemComplexity": 4, + "package": "App\\Concerns\\", + "pageRank": 0, + "afferentCoupling": 0, + "efferentCoupling": 1, + "instability": 1, + "violations": {} + }, + { + "name": "App\\ValueObjects\\PaymentResult", + "interface": false, + "abstract": false, + "final": true, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "successful", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "pending", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "failed", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 4, + "nbMethods": 4, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 4, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 4, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Enums\\PaymentStatus" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 14, + "vocabulary": 8, + "volume": 42, + "difficulty": 1.33, + "effort": 56, + "level": 0.75, + "bugs": 0.01, + "time": 3, + "intelligentContent": 31.5, + "number_operators": 6, + "number_operands": 8, + "number_operators_unique": 2, + "number_operands_unique": 6, + "cloc": 0, + "loc": 19, + "lloc": 19, + "mi": 60.6, + "mIwoC": 60.6, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 4.25, + "relativeSystemComplexity": 4.25, + "totalStructuralComplexity": 0, + "totalDataComplexity": 17, + "totalSystemComplexity": 17, + "package": "App\\ValueObjects\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 1, + "instability": 0.33, + "violations": {} + }, + { + "name": "App\\ValueObjects\\DiscountResult", + "interface": false, + "abstract": false, + "final": true, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 1, + "nbMethods": 1, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 1, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 1, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [], + "parents": [], + "implements": [], + "lcom": 1, + "length": 3, + "vocabulary": 3, + "volume": 4.75, + "difficulty": 0, + "effort": 0, + "level": 2, + "bugs": 0, + "time": 0, + "intelligentContent": 9.51, + "number_operators": 0, + "number_operands": 3, + "number_operators_unique": 0, + "number_operands_unique": 3, + "cloc": 3, + "loc": 10, + "lloc": 7, + "mi": 114.21, + "mIwoC": 76.69, + "commentWeight": 37.52, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 3, + "relativeSystemComplexity": 3, + "totalStructuralComplexity": 0, + "totalDataComplexity": 3, + "totalSystemComplexity": 3, + "package": "App\\ValueObjects\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 0, + "instability": 0, + "violations": {} + }, + { + "name": "App\\ValueObjects\\PricingResult", + "interface": false, + "abstract": false, + "final": true, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "toArray", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [], + "parents": [], + "implements": [], + "lcom": 2, + "length": 27, + "vocabulary": 14, + "volume": 102.8, + "difficulty": 1, + "effort": 102.8, + "level": 1, + "bugs": 0.03, + "time": 6, + "intelligentContent": 102.8, + "number_operators": 1, + "number_operands": 26, + "number_operators_unique": 1, + "number_operands_unique": 13, + "cloc": 15, + "loc": 26, + "lloc": 11, + "mi": 109.23, + "mIwoC": 63.06, + "commentWeight": 46.17, + "kanDefect": 0.15, + "relativeStructuralComplexity": 1, + "relativeDataComplexity": 2.5, + "relativeSystemComplexity": 3.5, + "totalStructuralComplexity": 2, + "totalDataComplexity": 5, + "totalSystemComplexity": 7, + "package": "App\\ValueObjects\\", + "pageRank": 0.01, + "afferentCoupling": 1, + "efferentCoupling": 0, + "instability": 0, + "violations": {} + }, + { + "name": "App\\ValueObjects\\TaxLine", + "interface": false, + "abstract": false, + "final": true, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "toArray", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [], + "parents": [], + "implements": [], + "lcom": 2, + "length": 10, + "vocabulary": 5, + "volume": 23.22, + "difficulty": 1.13, + "effort": 26.12, + "level": 0.89, + "bugs": 0.01, + "time": 1, + "intelligentContent": 20.64, + "number_operators": 1, + "number_operands": 9, + "number_operators_unique": 1, + "number_operands_unique": 4, + "cloc": 3, + "loc": 14, + "lloc": 11, + "mi": 100.45, + "mIwoC": 67.58, + "commentWeight": 32.86, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 2.5, + "relativeSystemComplexity": 2.5, + "totalStructuralComplexity": 0, + "totalDataComplexity": 5, + "totalSystemComplexity": 5, + "package": "App\\ValueObjects\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 0, + "instability": 0, + "violations": {} + }, + { + "name": "App\\ValueObjects\\RefundResult", + "interface": false, + "abstract": false, + "final": true, + "methods": [ + { + "name": "__construct", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + }, + { + "name": "successful", + "role": null, + "public": true, + "private": false, + "_type": "Hal\\Metric\\FunctionMetric" + } + ], + "nbMethodsIncludingGettersSetters": 2, + "nbMethods": 2, + "nbMethodsPrivate": 0, + "nbMethodsPublic": 2, + "nbMethodsGetter": 0, + "nbMethodsSetters": 0, + "wmc": 2, + "ccn": 1, + "ccnMethodMax": 1, + "externals": [ + "App\\Enums\\RefundStatus" + ], + "parents": [], + "implements": [], + "lcom": 2, + "length": 7, + "vocabulary": 7, + "volume": 19.65, + "difficulty": 1, + "effort": 19.65, + "level": 1, + "bugs": 0.01, + "time": 1, + "intelligentContent": 19.65, + "number_operators": 2, + "number_operands": 5, + "number_operators_unique": 2, + "number_operands_unique": 5, + "cloc": 0, + "loc": 11, + "lloc": 11, + "mi": 68.09, + "mIwoC": 68.09, + "commentWeight": 0, + "kanDefect": 0.15, + "relativeStructuralComplexity": 0, + "relativeDataComplexity": 3, + "relativeSystemComplexity": 3, + "totalStructuralComplexity": 0, + "totalDataComplexity": 6, + "totalSystemComplexity": 6, + "package": "App\\ValueObjects\\", + "pageRank": 0.01, + "afferentCoupling": 2, + "efferentCoupling": 1, + "instability": 0.33, + "violations": {} + } +] \ No newline at end of file diff --git a/report/complexity.html b/report/complexity.html new file mode 100644 index 00000000..b546acb5 --- /dev/null +++ b/report/complexity.html @@ -0,0 +1,5262 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + +
+
+
+
Average weighted method count by class (CC)
+
+ 6.35
+
+
+
+
+
Average cyclomatic complexity by class
+
+ 4.27
+
+
+
+
+
Average relative System complexity
+
+ 112.57
+
+
+
+
+
Average bugs by class(Halstead)
+
+ 0.08
+
+
+
+
+
average defects by class (Kan)
+
+ 0.3
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassWMCClass cycl.Max method cycl.Relative system complexityRelative data complexityRelative structural complexityBugsDefects
App\Auth\CustomerUserProvider + + 29 + + + 20 + + + 8 + + + 484.63 + + + 0.63 + + + 484 + + + 0.27 + + + 1.01 +
App\Providers\AppServiceProvider + + 7 + + + 2 + + + 2 + + + 529.04 + + + 0.04 + + + 529 + + + 0.03 + + + 0.15 +
App\Providers\FortifyServiceProvider + + 5 + + + 1 + + + 1 + + + 484.09 + + + 0.09 + + + 484 + + + 0.04 + + + 0.15 +
App\Models\OrderLine + + 5 + + + 1 + + + 1 + + + 5.67 + + + 1.67 + + + 4 + + + 0.03 + + + 0.15 +
App\Models\WebhookSubscription + + 2 + + + 1 + + + 1 + + + 4.67 + + + 0.67 + + + 4 + + + 0.01 + + + 0.15 +
App\Models\ThemeFile + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\ProductOption + + 2 + + + 1 + + + 1 + + + 4.67 + + + 0.67 + + + 4 + + + 0.01 + + + 0.15 +
App\Models\NavigationItem + + 4 + + + 2 + + + 2 + + + 2.5 + + + 1.5 + + + 1 + + + 0.04 + + + 0.15 +
App\Models\Refund + + 3 + + + 1 + + + 1 + + + 2.5 + + + 1.5 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\InventoryItem + + 3 + + + 1 + + + 1 + + + 2.5 + + + 1.5 + + + 1 + + + 0.02 + + + 0.15 +
App\Models\NavigationMenu + + 1 + + + 1 + + + 1 + + + 4.33 + + + 0.33 + + + 4 + + + 0.01 + + + 0.15 +
App\Models\App + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\CartLine + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\AppInstallation + + 3 + + + 1 + + + 1 + + + 5 + + + 1 + + + 4 + + + 0.02 + + + 0.15 +
App\Models\Cart + + 4 + + + 1 + + + 1 + + + 2.5 + + + 1.5 + + + 1 + + + 0.02 + + + 0.15 +
App\Models\Discount + + 9 + + + 8 + + + 8 + + + 6 + + + 2 + + + 4 + + + 0.07 + + + 0.43 +
App\Models\Product + + 5 + + + 1 + + + 1 + + + 10.25 + + + 1.25 + + + 9 + + + 0.03 + + + 0.15 +
App\Models\Order + + 13 + + + 5 + + + 5 + + + 37.57 + + + 1.57 + + + 36 + + + 0.11 + + + 0.52 +
App\Models\Store + + 5 + + + 1 + + + 1 + + + 36.71 + + + 0.71 + + + 36 + + + 0.02 + + + 0.15 +
App\Models\StoreDomain + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\Theme + + 3 + + + 1 + + + 1 + + + 5 + + + 1 + + + 4 + + + 0.01 + + + 0.15 +
App\Models\ProductMedia + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.02 + + + 0.15 +
App\Models\User + + 7 + + + 4 + + + 4 + + + 169.45 + + + 0.45 + + + 169 + + + 0.09 + + + 0.29 +
App\Models\WebhookDelivery + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.02 + + + 0.15 +
App\Models\Fulfillment + + 3 + + + 1 + + + 1 + + + 5 + + + 1 + + + 4 + + + 0.02 + + + 0.15 +
App\Models\ThemeSettings + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\Checkout + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.04 + + + 0.15 +
App\Models\Payment + + 3 + + + 1 + + + 1 + + + 5 + + + 1 + + + 4 + + + 0.02 + + + 0.15 +
App\Models\AnalyticsDaily + + 3 + + + 1 + + + 1 + + + 16.73 + + + 0.73 + + + 16 + + + 0.04 + + + 0.15 +
App\Models\Customer + + 6 + + + 1 + + + 1 + + + 4.5 + + + 3.5 + + + 1 + + + 0.03 + + + 0.15 +
App\Models\ProductVariant + + 4 + + + 1 + + + 1 + + + 10 + + + 1 + + + 9 + + + 0.04 + + + 0.15 +
App\Models\ProductOptionValue + + 1 + + + 1 + + + 1 + + + 1.5 + + + 0.5 + + + 1 + + + 0 + + + 0.15 +
App\Models\TaxSettings + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.02 + + + 0.15 +
App\Models\ShippingZone + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\Collection + + 2 + + + 1 + + + 1 + + + 4.67 + + + 0.67 + + + 4 + + + 0.01 + + + 0.15 +
App\Models\Page + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + 0.01 + + + 0.15 +
App\Models\ShippingRate + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\StoreSettings + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\CustomerAddress + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\StoreUser + + 3 + + + 2 + + + 2 + + + 9.25 + + + 0.25 + + + 9 + + + 0.01 + + + 0.22 +
App\Models\FulfillmentLine + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\Models\AnalyticsEvent + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + 0.01 + + + 0.15 +
App\Models\Scopes\StoreScope + + 2 + + + 2 + + + 2 + + + 9.75 + + + 0.75 + + + 9 + + + 0.01 + + + 0.22 +
App\Models\Organization + + 1 + + + 1 + + + 1 + + + 1.5 + + + 0.5 + + + 1 + + + 0 + + + 0.15 +
App\Models\Concerns\BelongsToStore + + 4 + + + 3 + + + 3 + + + 16.2 + + + 0.2 + + + 16 + + + 0.01 + + + 0.22 +
App\Exceptions\InsufficientInventoryException + + 0 + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0.15 +
App\Exceptions\FulfillmentGuardException + + 0 + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0.15 +
App\Exceptions\InvalidDiscountException + + 8 + + + 2 + + + 2 + + + 4.14 + + + 3.14 + + + 1 + + + 0.04 + + + 0.15 +
App\Exceptions\PaymentFailedException + + 0 + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0.15 +
App\Policies\StorePolicy + + 3 + + + 1 + + + 1 + + + 3.5 + + + 2.5 + + + 1 + + + 0.02 + + + 0.15 +
App\Policies\Concerns\ChecksStoreRole + + 4 + + + 3 + + + 2 + + + 10.13 + + + 1.13 + + + 9 + + + 0.03 + + + 0.22 +
App\Livewire\Settings\TwoFactor + + 17 + + + 9 + + + 3 + + + 144.34 + + + 0.34 + + + 144 + + + 0.16 + + + 0.57 +
App\Livewire\Settings\DeleteUserForm + + 1 + + + 1 + + + 1 + + + 25.17 + + + 0.17 + + + 25 + + + 0.01 + + + 0.15 +
App\Livewire\Settings\TwoFactor\RecoveryCodes + + 6 + + + 4 + + + 4 + + + 16.07 + + + 0.07 + + + 16 + + + 0.02 + + + 0.22 +
App\Livewire\Settings\Password + + 2 + + + 2 + + + 2 + + + 49 + + + 0 + + + 49 + + + 0.03 + + + 0.15 +
App\Livewire\Settings\Profile + + 10 + + + 6 + + + 3 + + + 144.23 + + + 0.23 + + + 144 + + + 0.05 + + + 0.29 +
App\Livewire\Settings\Appearance + + 0 + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0.15 +
App\Livewire\Storefront\Products\Show + + 9 + + + 5 + + + 3 + + + 256.2 + + + 0.2 + + + 256 + + + 0.14 + + + 0.29 +
App\Livewire\Storefront\Home + + 8 + + + 5 + + + 3 + + + 144.38 + + + 0.38 + + + 144 + + + 0.03 + + + 0.43 +
App\Livewire\Storefront\Checkout\Show + + 35 + + + 25 + + + 9 + + + 900.27 + + + 0.27 + + + 900 + + + 0.64 + + + 1.01 +
App\Livewire\Storefront\Checkout\Confirmation + + 2 + + + 1 + + + 1 + + + 25.25 + + + 0.25 + + + 25 + + + 0.02 + + + 0.15 +
App\Livewire\Storefront\Search\Index + + 4 + + + 2 + + + 2 + + + 36.14 + + + 0.14 + + + 36 + + + 0.03 + + + 0.15 +
App\Livewire\Storefront\CartDrawer + + 5 + + + 3 + + + 3 + + + 4.33 + + + 0.33 + + + 4 + + + 0.04 + + + 0.45 +
App\Livewire\Storefront\Cart\Show + + 10 + + + 6 + + + 3 + + + 49.45 + + + 0.45 + + + 49 + + + 0.09 + + + 0.59 +
App\Livewire\Storefront\Account\Dashboard + + 2 + + + 1 + + + 1 + + + 64.11 + + + 0.11 + + + 64 + + + 0.02 + + + 0.15 +
App\Livewire\Storefront\Account\Auth\Login + + 6 + + + 4 + + + 3 + + + 169.14 + + + 0.14 + + + 169 + + + 0.06 + + + 0.29 +
App\Livewire\Storefront\Account\Auth\Register + + 4 + + + 2 + + + 2 + + + 121.08 + + + 0.08 + + + 121 + + + 0.07 + + + 0.22 +
App\Livewire\Storefront\Account\Addresses\Index + + 6 + + + 2 + + + 2 + + + 169.1 + + + 0.1 + + + 169 + + + 0.17 + + + 0.22 +
App\Livewire\Storefront\Account\Orders\Index + + 2 + + + 1 + + + 1 + + + 49.13 + + + 0.13 + + + 49 + + + 0.02 + + + 0.15 +
App\Livewire\Storefront\Account\Orders\Show + + 2 + + + 1 + + + 1 + + + 64.17 + + + 0.17 + + + 64 + + + 0.02 + + + 0.15 +
App\Livewire\Storefront\Collections\Index + + 2 + + + 1 + + + 1 + + + 36.14 + + + 0.14 + + + 36 + + + 0.01 + + + 0.15 +
App\Livewire\Storefront\Collections\Show + + 5 + + + 3 + + + 3 + + + 121.11 + + + 0.11 + + + 121 + + + 0.06 + + + 0.22 +
App\Livewire\Storefront\Pages\Show + + 2 + + + 1 + + + 1 + + + 25.25 + + + 0.25 + + + 25 + + + 0.02 + + + 0.15 +
App\Livewire\Storefront\Concerns\EnsuresStore + + 3 + + + 3 + + + 3 + + + 25.17 + + + 0.17 + + + 25 + + + 0.01 + + + 0.22 +
App\Livewire\Admin\Customers\Index + + 2 + + + 1 + + + 1 + + + 100.09 + + + 0.09 + + + 100 + + + 0.05 + + + 0.15 +
App\Livewire\Admin\Customers\Show + + 3 + + + 2 + + + 2 + + + 25.25 + + + 0.25 + + + 25 + + + 0.05 + + + 0.15 +
App\Livewire\Admin\Settings\Taxes + + 7 + + + 5 + + + 5 + + + 25.17 + + + 0.17 + + + 25 + + + 0.1 + + + 0.15 +
App\Livewire\Admin\Settings\Index + + 3 + + + 1 + + + 1 + + + 9.25 + + + 0.25 + + + 9 + + + 0.07 + + + 0.15 +
App\Livewire\Admin\Settings\Shipping + + 8 + + + 2 + + + 2 + + + 196.16 + + + 0.16 + + + 196 + + + 0.19 + + + 0.22 +
App\Livewire\Admin\Dashboard + + 5 + + + 2 + + + 2 + + + 121.33 + + + 0.33 + + + 121 + + + 0.08 + + + 0.15 +
App\Livewire\Admin\Products\Index + + 7 + + + 3 + + + 2 + + + 169.21 + + + 0.21 + + + 169 + + + 0.08 + + + 0.29 +
App\Livewire\Admin\Products\Form + + 22 + + + 20 + + + 11 + + + 144.21 + + + 0.21 + + + 144 + + + 0.34 + + + 0.5 +
App\Livewire\Admin\Auth\Login + + 6 + + + 4 + + + 4 + + + 256.18 + + + 0.18 + + + 256 + + + 0.1 + + + 0.36 +
App\Livewire\Admin\Navigation\Index + + 14 + + + 7 + + + 6 + + + 729.13 + + + 0.13 + + + 729 + + + 0.29 + + + 0.29 +
App\Livewire\Admin\Discounts\Index + + 3 + + + 1 + + + 1 + + + 49.13 + + + 0.13 + + + 49 + + + 0.03 + + + 0.15 +
App\Livewire\Admin\Discounts\Form + + 14 + + + 12 + + + 8 + + + 36.48 + + + 0.48 + + + 36 + + + 0.24 + + + 0.43 +
App\Livewire\Admin\Orders\Index + + 5 + + + 1 + + + 1 + + + 100.09 + + + 0.09 + + + 100 + + + 0.09 + + + 0.15 +
App\Livewire\Admin\Orders\Show + + 22 + + + 13 + + + 6 + + + 289.22 + + + 0.22 + + + 289 + + + 0.28 + + + 0.52 +
App\Livewire\Admin\Collections\Index + + 2 + + + 1 + + + 1 + + + 49.13 + + + 0.13 + + + 49 + + + 0.03 + + + 0.15 +
App\Livewire\Admin\Collections\Form + + 18 + + + 14 + + + 8 + + + 324.14 + + + 0.14 + + + 324 + + + 0.32 + + + 0.59 +
App\Livewire\Admin\Pages\Index + + 3 + + + 1 + + + 1 + + + 81.13 + + + 0.13 + + + 81 + + + 0.05 + + + 0.15 +
App\Livewire\Admin\Pages\Form + + 15 + + + 13 + + + 10 + + + 36.33 + + + 0.33 + + + 36 + + + 0.21 + + + 0.36 +
App\Livewire\Admin\Apps\Index + + 3 + + + 1 + + + 1 + + + 256.16 + + + 0.16 + + + 256 + + + 0.11 + + + 0.15 +
App\Livewire\Admin\Themes\Index + + 5 + + + 2 + + + 2 + + + 169.2 + + + 0.2 + + + 169 + + + 0.09 + + + 0.22 +
App\Livewire\Admin\Analytics\Index + + 4 + + + 3 + + + 3 + + + 25.25 + + + 0.25 + + + 25 + + + 0.1 + + + 0.15 +
App\Livewire\Admin\Developers\Index + + 5 + + + 1 + + + 1 + + + 289.08 + + + 0.08 + + + 289 + + + 0.13 + + + 0.15 +
App\Livewire\Actions\Logout + + 1 + + + 1 + + + 1 + + + 16.2 + + + 0.2 + + + 16 + + + 0 + + + 0.15 +
App\Support\HandleGenerator + + 5 + + + 4 + + + 3 + + + 49.75 + + + 0.75 + + + 49 + + + 0.08 + + + 0.52 +
App\Support\CartSession + + 9 + + + 7 + + + 4 + + + 49.67 + + + 0.67 + + + 49 + + + 0.06 + + + 0.43 +
App\Http\Middleware\ResolveStore + + 10 + + + 8 + + + 5 + + + 324.56 + + + 0.56 + + + 324 + + + 0.13 + + + 0.64 +
App\Http\Controllers\Controller + + 0 + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0.15 +
App\Actions\Fortify\ResetUserPassword + + 1 + + + 1 + + + 1 + + + 25.33 + + + 0.33 + + + 25 + + + 0.01 + + + 0.15 +
App\Actions\Fortify\CreateNewUser + + 1 + + + 1 + + + 1 + + + 25.33 + + + 0.33 + + + 25 + + + 0.01 + + + 0.15 +
App\Jobs\ExpireAbandonedCheckouts + + 1 + + + 1 + + + 1 + + + 64.11 + + + 0.11 + + + 64 + + + 0.01 + + + 0.15 +
App\Jobs\CleanupAbandonedCarts + + 1 + + + 1 + + + 1 + + + 36 + + + 0 + + + 36 + + + 0 + + + 0.15 +
App\Jobs\AggregateAnalytics + + 4 + + + 3 + + + 3 + + + 676.02 + + + 0.02 + + + 676 + + + 0.18 + + + 0.15 +
App\Jobs\CancelUnpaidBankTransferOrders + + 1 + + + 1 + + + 1 + + + 64.11 + + + 0.11 + + + 64 + + + 0.01 + + + 0.15 +
App\Jobs\ProcessMediaUpload + + 2 + + + 1 + + + 1 + + + 4.17 + + + 0.17 + + + 4 + + + 0.01 + + + 0.15 +
App\Jobs\DeliverWebhook + + 10 + + + 8 + + + 6 + + + 361.18 + + + 0.18 + + + 361 + + + 0.21 + + + 0.36 +
App\Events\OrderRefunded + + 1 + + + 1 + + + 1 + + + 2 + + + 2 + + + 0 + + + 0 + + + 0.15 +
App\Events\OrderCancelled + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + 0 + + + 0.15 +
App\Events\OrderCreated + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + 0 + + + 0.15 +
App\Events\OrderPaid + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + 0 + + + 0.15 +
App\Events\FulfillmentDelivered + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + 0 + + + 0.15 +
App\Events\OrderFulfilled + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + 0 + + + 0.15 +
App\Observers\ProductObserver + + 4 + + + 1 + + + 1 + + + 4.33 + + + 0.33 + + + 4 + + + 0.01 + + + 0.15 +
App\Listeners\DispatchOrderWebhooks + + 5 + + + 1 + + + 1 + + + 1.6 + + + 0.6 + + + 1 + + + 0.03 + + + 0.15 +
App\Services\WebhookService + + 4 + + + 2 + + + 2 + + + 36.67 + + + 0.67 + + + 36 + + + 0.04 + + + 0.38 +
App\Services\OrderService + + 28 + + + 25 + + + 12 + + + 1089.13 + + + 0.13 + + + 1089 + + + 0.49 + + + 1.4 +
App\Services\Payments\MockPaymentProvider + + 8 + + + 5 + + + 4 + + + 10.5 + + + 1.5 + + + 9 + + + 0.11 + + + 0.15 +
App\Services\CheckoutService + + 41 + + + 30 + + + 8 + + + 1225.35 + + + 0.35 + + + 1225 + + + 0.61 + + + 2.12 +
App\Services\FulfillmentService + + 17 + + + 14 + + + 7 + + + 196.32 + + + 0.32 + + + 196 + + + 0.2 + + + 0.66 +
App\Services\TaxCalculator + + 12 + + + 10 + + + 6 + + + 5.17 + + + 4.17 + + + 1 + + + 0.14 + + + 0.36 +
App\Services\ThemeSettingsService + + 5 + + + 3 + + + 3 + + + 81.47 + + + 0.47 + + + 81 + + + 0.05 + + + 0.22 +
App\Services\InventoryService + + 8 + + + 4 + + + 4 + + + 25.5 + + + 0.5 + + + 25 + + + 0.1 + + + 0.22 +
App\Services\NavigationService + + 3 + + + 1 + + + 1 + + + 64.33 + + + 0.33 + + + 64 + + + 0.03 + + + 0.15 +
App\Services\RefundService + + 11 + + + 10 + + + 10 + + + 169.39 + + + 0.39 + + + 169 + + + 0.16 + + + 0.73 +
App\Services\ProductService + + 38 + + + 33 + + + 16 + + + 900.18 + + + 0.18 + + + 900 + + + 0.6 + + + 2 +
App\Services\ShippingCalculator + + 39 + + + 36 + + + 12 + + + 289.72 + + + 0.72 + + + 289 + + + 0.41 + + + 1.49 +
App\Services\AnalyticsService + + 3 + + + 2 + + + 2 + + + 36.86 + + + 0.86 + + + 36 + + + 0.04 + + + 0.15 +
App\Services\CartService + + 27 + + + 20 + + + 10 + + + 729.37 + + + 0.37 + + + 729 + + + 0.54 + + + 1.29 +
App\Services\PricingEngine + + 17 + + + 16 + + + 16 + + + 196.27 + + + 0.27 + + + 196 + + + 0.36 + + + 0.64 +
App\Services\DiscountService + + 23 + + + 21 + + + 15 + + + 484.32 + + + 0.32 + + + 484 + + + 0.3 + + + 1.22 +
App\Services\SearchService + + 19 + + + 15 + + + 7 + + + 361.55 + + + 0.55 + + + 361 + + + 0.25 + + + 0.57 +
App\Services\VariantMatrixService + + 15 + + + 13 + + + 10 + + + 289.22 + + + 0.22 + + + 289 + + + 0.21 + + + 1.81 +
App\Concerns\ProfileValidationRules + + 4 + + + 2 + + + 2 + + + 16.73 + + + 0.73 + + + 16 + + + 0.02 + + + 0.15 +
App\Concerns\PasswordValidationRules + + 2 + + + 1 + + + 1 + + + 2 + + + 1 + + + 1 + + + 0.01 + + + 0.15 +
App\ValueObjects\PaymentResult + + 4 + + + 1 + + + 1 + + + 4.25 + + + 4.25 + + + 0 + + + 0.01 + + + 0.15 +
App\ValueObjects\DiscountResult + + 1 + + + 1 + + + 1 + + + 3 + + + 3 + + + 0 + + + 0 + + + 0.15 +
App\ValueObjects\PricingResult + + 2 + + + 1 + + + 1 + + + 3.5 + + + 2.5 + + + 1 + + + 0.03 + + + 0.15 +
App\ValueObjects\TaxLine + + 2 + + + 1 + + + 1 + + + 2.5 + + + 2.5 + + + 0 + + + 0.01 + + + 0.15 +
App\ValueObjects\RefundResult + + 2 + + + 1 + + + 1 + + + 3 + + + 3 + + + 0 + + + 0.01 + + + 0.15 +
+
+
+
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + diff --git a/report/composer.html b/report/composer.html new file mode 100644 index 00000000..72487354 --- /dev/null +++ b/report/composer.html @@ -0,0 +1,230 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ +
No composer.json file found in this project
+ + + +
+ + + + + + + + + + + + + + + + + + diff --git a/report/coupling.html b/report/coupling.html new file mode 100644 index 00000000..2227a9a7 --- /dev/null +++ b/report/coupling.html @@ -0,0 +1,2963 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + +
+
+
+

Coupling

+ +
+ Afferent coupling (AC) is the number of classes affected by given class. +
Efferent coupling (EC) is the number of classes from which given class receives + effects. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassAfferent couplingEfferent couplingInstabilityClassRank
App\Auth\CustomerUserProvider + + 1 + + + 7 + + + 0.88 + + + 0 +
App\Providers\AppServiceProvider + + 0 + + + 10 + + + 1 + + + 0 +
App\Providers\FortifyServiceProvider + + 0 + + + 5 + + + 1 + + + 0 +
App\Models\OrderLine + + 1 + + + 3 + + + 0.75 + + + 0 +
App\Models\WebhookSubscription + + 3 + + + 3 + + + 0.5 + + + 0.02 +
App\Models\ThemeFile + + 0 + + + 2 + + + 1 + + + 0 +
App\Models\ProductOption + + 0 + + + 3 + + + 1 + + + 0 +
App\Models\NavigationItem + + 2 + + + 2 + + + 0.5 + + + 0.01 +
App\Models\Refund + + 2 + + + 2 + + + 0.5 + + + 0.01 +
App\Models\InventoryItem + + 4 + + + 2 + + + 0.33 + + + 0.01 +
App\Models\NavigationMenu + + 2 + + + 2 + + + 0.5 + + + 0.01 +
App\Models\App + + 1 + + + 2 + + + 0.67 + + + 0 +
App\Models\CartLine + + 1 + + + 2 + + + 0.67 + + + 0 +
App\Models\AppInstallation + + 1 + + + 3 + + + 0.75 + + + 0.01 +
App\Models\Cart + + 6 + + + 2 + + + 0.25 + + + 0.04 +
App\Models\Discount + + 3 + + + 1 + + + 0.25 + + + 0.01 +
App\Models\Product + + 10 + + + 3 + + + 0.23 + + + 0.04 +
App\Models\Order + + 20 + + + 3 + + + 0.13 + + + 0.1 +
App\Models\Store + + 17 + + + 5 + + + 0.23 + + + 0.06 +
App\Models\StoreDomain + + 1 + + + 2 + + + 0.67 + + + 0 +
App\Models\Theme + + 2 + + + 3 + + + 0.6 + + + 0.01 +
App\Models\ProductMedia + + 1 + + + 2 + + + 0.67 + + + 0.01 +
App\Models\User + + 4 + + + 5 + + + 0.56 + + + 0.02 +
App\Models\WebhookDelivery + + 1 + + + 2 + + + 0.67 + + + 0.01 +
App\Models\Fulfillment + + 2 + + + 3 + + + 0.6 + + + 0.02 +
App\Models\ThemeSettings + + 0 + + + 2 + + + 1 + + + 0 +
App\Models\Checkout + + 7 + + + 2 + + + 0.22 + + + 0.02 +
App\Models\Payment + + 4 + + + 3 + + + 0.43 + + + 0.01 +
App\Models\AnalyticsDaily + + 1 + + + 1 + + + 0.5 + + + 0.01 +
App\Models\Customer + + 4 + + + 2 + + + 0.33 + + + 0.01 +
App\Models\ProductVariant + + 1 + + + 4 + + + 0.8 + + + 0 +
App\Models\ProductOptionValue + + 0 + + + 2 + + + 1 + + + 0 +
App\Models\TaxSettings + + 3 + + + 2 + + + 0.4 + + + 0.01 +
App\Models\ShippingZone + + 2 + + + 2 + + + 0.5 + + + 0.01 +
App\Models\Collection + + 5 + + + 2 + + + 0.29 + + + 0.01 +
App\Models\Page + + 3 + + + 1 + + + 0.25 + + + 0.01 +
App\Models\ShippingRate + + 4 + + + 2 + + + 0.33 + + + 0.01 +
App\Models\StoreSettings + + 0 + + + 2 + + + 1 + + + 0 +
App\Models\CustomerAddress + + 1 + + + 2 + + + 0.67 + + + 0.01 +
App\Models\StoreUser + + 0 + + + 2 + + + 1 + + + 0 +
App\Models\FulfillmentLine + + 1 + + + 2 + + + 0.67 + + + 0.01 +
App\Models\AnalyticsEvent + + 2 + + + 1 + + + 0.33 + + + 0.01 +
App\Models\Scopes\StoreScope + + 1 + + + 3 + + + 0.75 + + + 0.01 +
App\Models\Organization + + 0 + + + 2 + + + 1 + + + 0 +
App\Models\Concerns\BelongsToStore + + 0 + + + 3 + + + 1 + + + 0 +
App\Exceptions\InsufficientInventoryException + + 1 + + + 1 + + + 0.5 + + + 0 +
App\Exceptions\FulfillmentGuardException + + 1 + + + 1 + + + 0.5 + + + 0 +
App\Exceptions\InvalidDiscountException + + 1 + + + 1 + + + 0.5 + + + 0.01 +
App\Exceptions\PaymentFailedException + + 1 + + + 1 + + + 0.5 + + + 0 +
App\Policies\StorePolicy + + 0 + + + 2 + + + 1 + + + 0 +
App\Policies\Concerns\ChecksStoreRole + + 0 + + + 2 + + + 1 + + + 0 +
App\Livewire\Settings\TwoFactor + + 0 + + + 6 + + + 1 + + + 0 +
App\Livewire\Settings\DeleteUserForm + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Settings\TwoFactor\RecoveryCodes + + 0 + + + 2 + + + 1 + + + 0 +
App\Livewire\Settings\Password + + 0 + + + 2 + + + 1 + + + 0 +
App\Livewire\Settings\Profile + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Settings\Appearance + + 0 + + + 1 + + + 1 + + + 0 +
App\Livewire\Storefront\Products\Show + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Storefront\Home + + 0 + + + 6 + + + 1 + + + 0 +
App\Livewire\Storefront\Checkout\Show + + 0 + + + 6 + + + 1 + + + 0 +
App\Livewire\Storefront\Checkout\Confirmation + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Storefront\Search\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Storefront\CartDrawer + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Storefront\Cart\Show + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Storefront\Account\Dashboard + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Storefront\Account\Auth\Login + + 0 + + + 5 + + + 1 + + + 0 +
App\Livewire\Storefront\Account\Auth\Register + + 0 + + + 5 + + + 1 + + + 0 +
App\Livewire\Storefront\Account\Addresses\Index + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Storefront\Account\Orders\Index + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Storefront\Account\Orders\Show + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Storefront\Collections\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Storefront\Collections\Show + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Storefront\Pages\Show + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Storefront\Concerns\EnsuresStore + + 0 + + + 1 + + + 1 + + + 0 +
App\Livewire\Admin\Customers\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Customers\Show + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Settings\Taxes + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Settings\Index + + 0 + + + 2 + + + 1 + + + 0 +
App\Livewire\Admin\Settings\Shipping + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Admin\Dashboard + + 0 + + + 5 + + + 1 + + + 0 +
App\Livewire\Admin\Products\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Products\Form + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Admin\Auth\Login + + 0 + + + 6 + + + 1 + + + 0 +
App\Livewire\Admin\Navigation\Index + + 0 + + + 5 + + + 1 + + + 0 +
App\Livewire\Admin\Discounts\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Discounts\Form + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Orders\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Orders\Show + + 0 + + + 6 + + + 1 + + + 0 +
App\Livewire\Admin\Collections\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Collections\Form + + 0 + + + 5 + + + 1 + + + 0 +
App\Livewire\Admin\Pages\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Pages\Form + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Admin\Apps\Index + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Admin\Themes\Index + + 0 + + + 4 + + + 1 + + + 0 +
App\Livewire\Admin\Analytics\Index + + 0 + + + 3 + + + 1 + + + 0 +
App\Livewire\Admin\Developers\Index + + 0 + + + 5 + + + 1 + + + 0 +
App\Livewire\Actions\Logout + + 1 + + + 2 + + + 0.67 + + + 0.01 +
App\Support\HandleGenerator + + 3 + + + 2 + + + 0.4 + + + 0.01 +
App\Support\CartSession + + 4 + + + 3 + + + 0.43 + + + 0.02 +
App\Http\Middleware\ResolveStore + + 0 + + + 9 + + + 1 + + + 0 +
App\Http\Controllers\Controller + + 0 + + + 0 + + + 0 + + + 0 +
App\Actions\Fortify\ResetUserPassword + + 0 + + + 3 + + + 1 + + + 0 +
App\Actions\Fortify\CreateNewUser + + 0 + + + 3 + + + 1 + + + 0 +
App\Jobs\ExpireAbandonedCheckouts + + 0 + + + 3 + + + 1 + + + 0 +
App\Jobs\CleanupAbandonedCarts + + 0 + + + 2 + + + 1 + + + 0 +
App\Jobs\AggregateAnalytics + + 0 + + + 6 + + + 1 + + + 0 +
App\Jobs\CancelUnpaidBankTransferOrders + + 0 + + + 3 + + + 1 + + + 0 +
App\Jobs\ProcessMediaUpload + + 0 + + + 3 + + + 1 + + + 0 +
App\Jobs\DeliverWebhook + + 1 + + + 7 + + + 0.88 + + + 0.01 +
App\Events\OrderRefunded + + 1 + + + 2 + + + 0.67 + + + 0 +
App\Events\OrderCancelled + + 1 + + + 1 + + + 0.5 + + + 0 +
App\Events\OrderCreated + + 2 + + + 1 + + + 0.33 + + + 0.01 +
App\Events\OrderPaid + + 3 + + + 1 + + + 0.25 + + + 0.01 +
App\Events\FulfillmentDelivered + + 1 + + + 1 + + + 0.5 + + + 0 +
App\Events\OrderFulfilled + + 2 + + + 1 + + + 0.33 + + + 0.01 +
App\Observers\ProductObserver + + 0 + + + 2 + + + 1 + + + 0 +
App\Listeners\DispatchOrderWebhooks + + 0 + + + 5 + + + 1 + + + 0 +
App\Services\WebhookService + + 2 + + + 3 + + + 0.6 + + + 0.01 +
App\Services\OrderService + + 3 + + + 13 + + + 0.81 + + + 0.01 +
App\Services\Payments\MockPaymentProvider + + 0 + + + 7 + + + 1 + + + 0 +
App\Services\CheckoutService + + 1 + + + 16 + + + 0.94 + + + 0.01 +
App\Services\FulfillmentService + + 1 + + + 8 + + + 0.89 + + + 0.01 +
App\Services\TaxCalculator + + 1 + + + 2 + + + 0.67 + + + 0 +
App\Services\ThemeSettingsService + + 0 + + + 3 + + + 1 + + + 0 +
App\Services\InventoryService + + 3 + + + 4 + + + 0.57 + + + 0.01 +
App\Services\NavigationService + + 0 + + + 3 + + + 1 + + + 0 +
App\Services\RefundService + + 1 + + + 9 + + + 0.9 + + + 0 +
App\Services\ProductService + + 1 + + + 7 + + + 0.88 + + + 0.01 +
App\Services\ShippingCalculator + + 1 + + + 5 + + + 0.83 + + + 0 +
App\Services\AnalyticsService + + 1 + + + 4 + + + 0.8 + + + 0.01 +
App\Services\CartService + + 0 + + + 7 + + + 1 + + + 0 +
App\Services\PricingEngine + + 1 + + + 9 + + + 0.9 + + + 0 +
App\Services\DiscountService + + 1 + + + 6 + + + 0.86 + + + 0 +
App\Services\SearchService + + 1 + + + 5 + + + 0.83 + + + 0.01 +
App\Services\VariantMatrixService + + 1 + + + 2 + + + 0.67 + + + 0 +
App\Concerns\ProfileValidationRules + + 0 + + + 1 + + + 1 + + + 0 +
App\Concerns\PasswordValidationRules + + 0 + + + 1 + + + 1 + + + 0 +
App\ValueObjects\PaymentResult + + 2 + + + 1 + + + 0.33 + + + 0.01 +
App\ValueObjects\DiscountResult + + 1 + + + 0 + + + 0 + + + 0.01 +
App\ValueObjects\PricingResult + + 1 + + + 0 + + + 0 + + + 0.01 +
App\ValueObjects\TaxLine + + 2 + + + 0 + + + 0 + + + 0.01 +
App\ValueObjects\RefundResult + + 2 + + + 1 + + + 0.33 + + + 0.01 +
+ +
+
+ +
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/report/css/clusterize.css b/report/css/clusterize.css new file mode 100644 index 00000000..5db98df4 --- /dev/null +++ b/report/css/clusterize.css @@ -0,0 +1,37 @@ +/* max-height - the only parameter in this file that needs to be edited. + * Change it to suit your needs. The rest is recommended to leave as is. + */ +.clusterize-scroll{ + max-height: 200px; + overflow: auto; +} + +/** + * Avoid vertical margins for extra tags + * Necessary for correct calculations when rows have nonzero vertical margins + */ +.clusterize-extra-row{ + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +/* By default extra tag .clusterize-keep-parity added to keep parity of rows. + * Useful when used :nth-child(even/odd) + */ +.clusterize-extra-row.clusterize-keep-parity{ + display: none; +} + +/* During initialization clusterize adds tabindex to force the browser to keep focus + * on the scrolling list, see issue #11 + * Outline removes default browser's borders for focused elements. + */ +.clusterize-content{ + outline: 0; +} + +/* Centering message that appears when no data provided + */ +.clusterize-no-data td{ + text-align: center; +} \ No newline at end of file diff --git a/report/css/material-icons.css b/report/css/material-icons.css new file mode 100644 index 00000000..bf3707e5 --- /dev/null +++ b/report/css/material-icons.css @@ -0,0 +1,20 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(fonts/material-icons.ttf) format('truetype'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; +} diff --git a/report/css/milligram.min.css b/report/css/milligram.min.css new file mode 100644 index 00000000..c9d72065 --- /dev/null +++ b/report/css/milligram.min.css @@ -0,0 +1,12 @@ +/*! + * Milligram v1.1.0 + * http://milligram.github.io + * + * Copyright (c) 2016 CJ Patoilo + * Licensed under the MIT license +*/ + + +html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:"Roboto","Helvetica Neue","Helvetica","Arial",sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}*,*:after,*:before{box-sizing:inherit}blockquote{border-left:.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:hover,.button:focus,button:hover,button:focus,input[type='button']:hover,input[type='button']:focus,input[type='reset']:hover,input[type='reset']:focus,input[type='submit']:hover,input[type='submit']:focus{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button.button-disabled,.button[disabled],button.button-disabled,button[disabled],input[type='button'].button-disabled,input[type='button'][disabled],input[type='reset'].button-disabled,input[type='reset'][disabled],input[type='submit'].button-disabled,input[type='submit'][disabled]{opacity:.5;cursor:default}.button.button-disabled:hover,.button.button-disabled:focus,.button[disabled]:hover,.button[disabled]:focus,button.button-disabled:hover,button.button-disabled:focus,button[disabled]:hover,button[disabled]:focus,input[type='button'].button-disabled:hover,input[type='button'].button-disabled:focus,input[type='button'][disabled]:hover,input[type='button'][disabled]:focus,input[type='reset'].button-disabled:hover,input[type='reset'].button-disabled:focus,input[type='reset'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='submit'].button-disabled:hover,input[type='submit'].button-disabled:focus,input[type='submit'][disabled]:hover,input[type='submit'][disabled]:focus{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{color:#9b4dca;background-color:transparent}.button.button-outline:hover,.button.button-outline:focus,button.button-outline:hover,button.button-outline:focus,input[type='button'].button-outline:hover,input[type='button'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='submit'].button-outline:hover,input[type='submit'].button-outline:focus{color:#606c76;background-color:transparent;border-color:#606c76}.button.button-outline.button-disabled:hover,.button.button-outline.button-disabled:focus,.button.button-outline[disabled]:hover,.button.button-outline[disabled]:focus,button.button-outline.button-disabled:hover,button.button-outline.button-disabled:focus,button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,input[type='button'].button-outline.button-disabled:hover,input[type='button'].button-outline.button-disabled:focus,input[type='button'].button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='reset'].button-outline.button-disabled:hover,input[type='reset'].button-outline.button-disabled:focus,input[type='reset'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='submit'].button-outline.button-disabled:hover,input[type='submit'].button-outline.button-disabled:focus,input[type='submit'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus{color:#9b4dca;border-color:inherit}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{color:#9b4dca;background-color:transparent;border-color:transparent}.button.button-clear:hover,.button.button-clear:focus,button.button-clear:hover,button.button-clear:focus,input[type='button'].button-clear:hover,input[type='button'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='submit'].button-clear:hover,input[type='submit'].button-clear:focus{color:#606c76;background-color:transparent;border-color:transparent}.button.button-clear.button-disabled:hover,.button.button-clear.button-disabled:focus,.button.button-clear[disabled]:hover,.button.button-clear[disabled]:focus,button.button-clear.button-disabled:hover,button.button-clear.button-disabled:focus,button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,input[type='button'].button-clear.button-disabled:hover,input[type='button'].button-clear.button-disabled:focus,input[type='button'].button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='reset'].button-clear.button-disabled:hover,input[type='reset'].button-clear.button-disabled:focus,input[type='reset'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='submit'].button-clear.button-disabled:hover,input[type='submit'].button-clear.button-disabled:focus,input[type='submit'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;padding:.2rem .5rem;margin:0 .2rem;white-space:nowrap}pre{background:#f4f5f6;border-left:.3rem solid #9b4dca;font-family:"Menlo","Consolas","Bitstream Vera Sans Mono","DejaVu Sans Mono","Monaco",monospace}pre>code{background:transparent;border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:.1rem solid #f4f5f6;margin-bottom:3.5rem;margin-top:3rem}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;height:3.8rem;padding:.6rem 1rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border:.1rem solid #9b4dca;outline:0}select{padding:.6rem 3rem .6rem 1rem;background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjkgMTQiICAgaGVpZ2h0PSIxNHB4IiAgIGlkPSJMYXllcl8xIiAgIHZlcnNpb249IjEuMSIgICB2aWV3Qm94PSIwIDAgMjkgMTQiICAgd2lkdGg9IjI5cHgiICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ4LjQgcjk5MzkiICAgc29kaXBvZGk6ZG9jbmFtZT0iY2FyZXQtZ3JheS5zdmciPjxtZXRhZGF0YSAgICAgaWQ9Im1ldGFkYXRhMzAzOSI+PHJkZjpSREY+PGNjOldvcmsgICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+PC9jYzpXb3JrPjwvcmRmOlJERj48L21ldGFkYXRhPjxkZWZzICAgICBpZD0iZGVmczMwMzciIC8+PHNvZGlwb2RpOm5hbWVkdmlldyAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiICAgICBib3JkZXJvcGFjaXR5PSIxIiAgICAgb2JqZWN0dG9sZXJhbmNlPSIxMCIgICAgIGdyaWR0b2xlcmFuY2U9IjEwIiAgICAgZ3VpZGV0b2xlcmFuY2U9IjEwIiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAiICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSI5MDMiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI1OTQiICAgICBpZD0ibmFtZWR2aWV3MzAzNSIgICAgIHNob3dncmlkPSJ0cnVlIiAgICAgaW5rc2NhcGU6em9vbT0iMTIuMTM3OTMxIiAgICAgaW5rc2NhcGU6Y3g9Ii00LjExOTMxODJlLTA4IiAgICAgaW5rc2NhcGU6Y3k9IjciICAgICBpbmtzY2FwZTp3aW5kb3cteD0iNTAyIiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjMwMiIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJMYXllcl8xIj48aW5rc2NhcGU6Z3JpZCAgICAgICB0eXBlPSJ4eWdyaWQiICAgICAgIGlkPSJncmlkMzA0MSIgLz48L3NvZGlwb2RpOm5hbWVkdmlldz48cG9seWdvbiAgICAgcG9pbnRzPSIwLjE1LDAgMTQuNSwxNC4zNSAyOC44NSwwICIgICAgIGlkPSJwb2x5Z29uMzAzMyIgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAuMzU0MTEzODcsMCwwLDAuNDgzMjkxMSw5LjMyNDE1NDUsMy42MjQ5OTkyKSIgICAgIHN0eWxlPSJmaWxsOiNkMWQxZDE7ZmlsbC1vcGFjaXR5OjEiIC8+PC9zdmc+) center right no-repeat}select:focus{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjkgMTQiICAgaGVpZ2h0PSIxNHB4IiAgIGlkPSJMYXllcl8xIiAgIHZlcnNpb249IjEuMSIgICB2aWV3Qm94PSIwIDAgMjkgMTQiICAgd2lkdGg9IjI5cHgiICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ4LjQgcjk5MzkiICAgc29kaXBvZGk6ZG9jbmFtZT0iY2FyZXQuc3ZnIj48bWV0YWRhdGEgICAgIGlkPSJtZXRhZGF0YTMwMzkiPjxyZGY6UkRGPjxjYzpXb3JrICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcyAgICAgaWQ9ImRlZnMzMDM3IiAvPjxzb2RpcG9kaTpuYW1lZHZpZXcgICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIgICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IiAgICAgYm9yZGVyb3BhY2l0eT0iMSIgICAgIG9iamVjdHRvbGVyYW5jZT0iMTAiICAgICBncmlkdG9sZXJhbmNlPSIxMCIgICAgIGd1aWRldG9sZXJhbmNlPSIxMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIgICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iOTAzIiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNTk0IiAgICAgaWQ9Im5hbWVkdmlldzMwMzUiICAgICBzaG93Z3JpZD0idHJ1ZSIgICAgIGlua3NjYXBlOnpvb209IjEyLjEzNzkzMSIgICAgIGlua3NjYXBlOmN4PSItNC4xMTkzMTgyZS0wOCIgICAgIGlua3NjYXBlOmN5PSI3IiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjUwMiIgICAgIGlua3NjYXBlOndpbmRvdy15PSIzMDIiICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIwIiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0iTGF5ZXJfMSI+PGlua3NjYXBlOmdyaWQgICAgICAgdHlwZT0ieHlncmlkIiAgICAgICBpZD0iZ3JpZDMwNDEiIC8+PC9zb2RpcG9kaTpuYW1lZHZpZXc+PHBvbHlnb24gICAgIHBvaW50cz0iMjguODUsMCAwLjE1LDAgMTQuNSwxNC4zNSAiICAgICBpZD0icG9seWdvbjMwMzMiICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLjM1NDExMzg3LDAsMCwwLjQ4MzI5MTEsOS4zMjQxNTUzLDMuNjI1KSIgICAgIHN0eWxlPSJmaWxsOiM5YjRkY2Y7ZmlsbC1vcGFjaXR5OjEiIC8+PC9zdmc+)}textarea{padding-bottom:.6rem;padding-top:.6rem;min-height:6.5rem}label,legend{font-size:1.6rem;font-weight:700;display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{font-weight:normal;display:inline-block;margin-left:.5rem}.container{margin:0 auto;max-width:112rem;padding:0 2rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row .row-wrap{flex-wrap:wrap}.row .row-no-padding{padding:0}.row .row-no-padding>.column{padding:0}.row .row-top{align-items:flex-start}.row .row-bottom{align-items:flex-end}.row .row-center{align-items:center}.row .row-stretch{align-items:stretch}.row .row-baseline{align-items:baseline}.row .column{display:block;flex:1;margin-left:0;max-width:100%;width:100%}.row .column .col-top{align-self:flex-start}.row .column .col-bottom{align-self:flex-end}.row .column .col-center{align-self:center}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1rem}}a{color:#9b4dca;text-decoration:none}a:hover{color:#606c76}dl,ol,ul{margin-top:0;padding-left:0}dl ul,dl ol,ol ul,ol ol,ul ul,ul ol{font-size:90%;margin:1.5rem 0 1.5rem 3rem}dl{list-style:none}ul{list-style:circle inside}ol{list-style:decimal inside}dt,dd,li{margin-bottom:1rem}.button,button{margin-bottom:1rem}input,textarea,select,fieldset{margin-bottom:1.5rem}pre,blockquote,dl,figure,table,p,ul,ol,form{margin-bottom:2.5rem}table{width:100%}th,td{border-bottom:.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}th:first-child,td:first-child{padding-left:0}th:last-child,td:last-child{padding-right:0}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;margin-bottom:2rem;margin-top:0}h1{font-size:4rem;letter-spacing:-0.1rem;line-height:1.2}h2{font-size:3.6rem;letter-spacing:-0.1rem;line-height:1.25}h3{font-size:3rem;letter-spacing:-0.1rem;line-height:1.3}h4{font-size:2.4rem;letter-spacing:-0.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-0.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}@media (min-width: 40rem){h1{font-size:5rem}h2{font-size:4.2rem}h3{font-size:3.6rem}h4{font-size:3rem}h5{font-size:2.4rem}h6{font-size:1.5rem}}.float-right{float:right}.float-left{float:left}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{content:"";display:table}.clearfix:after{clear:both} + +/*# sourceMappingURL=milligram.min.css.map */ \ No newline at end of file diff --git a/report/css/milligram.min.css.map b/report/css/milligram.min.css.map new file mode 100644 index 00000000..4a28342a --- /dev/null +++ b/report/css/milligram.min.css.map @@ -0,0 +1,12 @@ +{ + "version": 3, + "sources": [ + "milligram.min.css" + ], + "names": [], + "mappings": "AAAA;;;;;;EAME;;;AAGF,KAAK,sBAAsB,eAAe,CAAC,KAAK,cAAc,qEAAqE,gBAAgB,gBAAgB,qBAAqB,eAAe,CAAC,mBAAmB,kBAAkB,CAAC,WAAW,gCAAgC,cAAc,eAAe,mBAAmB,CAAC,wBAAwB,QAAQ,CAAC,6EAA6E,yBAAyB,2BAA2B,oBAAoB,WAAW,eAAe,qBAAqB,iBAAiB,gBAAgB,cAAc,qBAAqB,mBAAmB,eAAe,kBAAkB,qBAAqB,yBAAyB,kBAAkB,CAAC,sNAAsN,yBAAyB,qBAAqB,WAAW,SAAS,CAAC,4RAA4R,WAAW,cAAc,CAAC,grBAAgrB,yBAAyB,oBAAoB,CAAC,wJAAwJ,cAAc,4BAA4B,CAAC,4WAA4W,cAAc,6BAA6B,oBAAoB,CAAC,49BAA49B,cAAc,oBAAoB,CAAC,8IAA8I,cAAc,6BAA6B,wBAAwB,CAAC,wVAAwV,cAAc,6BAA6B,wBAAwB,CAAC,o7BAAo7B,aAAa,CAAC,KAAK,mBAAmB,oBAAoB,cAAc,oBAAoB,eAAe,kBAAkB,CAAC,IAAI,mBAAmB,gCAAgC,+FAA+F,CAAC,SAAS,uBAAuB,gBAAgB,cAAc,oBAAoB,eAAe,CAAC,GAAG,SAAS,+BAA+B,qBAAqB,eAAe,CAAC,4JAA4J,wBAAgB,AAAhB,qBAAgB,AAAhB,gBAAgB,6BAA6B,2BAA2B,oBAAoB,gBAAgB,cAAc,mBAAmB,UAAU,CAAC,kNAAkN,2BAA2B,SAAS,CAAC,OAAO,8BAA8B,yvEAAyvE,CAAC,aAAa,4tEAA4tE,CAAC,SAAS,qBAAqB,kBAAkB,iBAAiB,CAAC,aAAa,iBAAiB,gBAAgB,cAAc,mBAAmB,CAAC,SAAS,eAAe,SAAS,CAAC,2CAA2C,cAAc,CAAC,cAAc,mBAAmB,qBAAqB,iBAAiB,CAAC,WAAW,cAAc,iBAAiB,eAAe,kBAAkB,UAAU,CAAC,KAAK,aAAa,sBAAsB,UAAU,UAAU,CAAC,eAAe,cAAc,CAAC,qBAAqB,SAAS,CAAC,6BAA6B,SAAS,CAAC,cAAc,sBAAsB,CAAC,iBAAiB,oBAAoB,CAAC,iBAAiB,kBAAkB,CAAC,kBAAkB,mBAAmB,CAAC,mBAAmB,oBAAoB,CAAC,aAAa,cAAc,OAAO,cAAc,eAAe,UAAU,CAAC,sBAAsB,qBAAqB,CAAC,yBAAyB,mBAAmB,CAAC,yBAAyB,iBAAiB,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,4DAA4D,oBAAoB,CAAC,8BAA8B,eAAe,CAAC,4DAA4D,oBAAoB,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,uBAAuB,aAAa,aAAa,CAAC,uBAAuB,aAAa,aAAa,CAAC,uBAAuB,aAAa,aAAa,CAAC,8CAA8C,kBAAkB,kBAAkB,CAAC,uBAAuB,aAAa,aAAa,CAAC,uBAAuB,aAAa,aAAa,CAAC,uBAAuB,aAAa,aAAa,CAAC,8CAA8C,kBAAkB,kBAAkB,CAAC,uBAAuB,aAAa,aAAa,CAAC,uBAAuB,aAAa,aAAa,CAAC,uBAAuB,aAAa,aAAa,CAAC,0BAA0B,KAAK,mBAAmB,kBAAkB,yBAAyB,CAAC,aAAa,sBAAsB,cAAc,CAAC,CAAC,EAAE,cAAc,oBAAoB,CAAC,QAAQ,aAAa,CAAC,SAAS,aAAa,cAAc,CAAC,oCAAoC,cAAc,2BAA2B,CAAC,GAAG,eAAe,CAAC,GAAG,wBAAwB,CAAC,GAAG,yBAAyB,CAAC,SAAS,kBAAkB,CAAC,eAAe,kBAAkB,CAAC,+BAA+B,oBAAoB,CAAC,4CAA4C,oBAAoB,CAAC,MAAM,UAAU,CAAC,MAAM,kCAAkC,sBAAsB,eAAe,CAAC,8BAA8B,cAAc,CAAC,4BAA4B,eAAe,CAAC,EAAE,YAAY,CAAC,kBAAkB,gBAAgB,mBAAmB,YAAY,CAAC,GAAG,eAAe,uBAAuB,eAAe,CAAC,GAAG,iBAAiB,uBAAuB,gBAAgB,CAAC,GAAG,eAAe,uBAAuB,eAAe,CAAC,GAAG,iBAAiB,wBAAwB,gBAAgB,CAAC,GAAG,iBAAiB,wBAAwB,eAAe,CAAC,GAAG,iBAAiB,iBAAiB,eAAe,CAAC,0BAA0B,GAAG,cAAc,CAAC,GAAG,gBAAgB,CAAC,GAAG,gBAAgB,CAAC,GAAG,cAAc,CAAC,GAAG,gBAAgB,CAAC,GAAG,gBAAgB,CAAC,CAAC,aAAa,WAAW,CAAC,YAAY,UAAU,CAAC,WAAU,MAAO,CAAC,iCAAiC,WAAW,aAAa,CAAC,gBAAgB,UAAU,CAAC", + "file": "milligram.min.css", + "sourcesContent": [ + "/*!\n * Milligram v1.1.0\n * http://milligram.github.io\n *\n * Copyright (c) 2016 CJ Patoilo\n * Licensed under the MIT license\n*/\n\n\nhtml{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:\"Roboto\",\"Helvetica Neue\",\"Helvetica\",\"Arial\",sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}*,*:after,*:before{box-sizing:inherit}blockquote{border-left:.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:hover,.button:focus,button:hover,button:focus,input[type='button']:hover,input[type='button']:focus,input[type='reset']:hover,input[type='reset']:focus,input[type='submit']:hover,input[type='submit']:focus{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button.button-disabled,.button[disabled],button.button-disabled,button[disabled],input[type='button'].button-disabled,input[type='button'][disabled],input[type='reset'].button-disabled,input[type='reset'][disabled],input[type='submit'].button-disabled,input[type='submit'][disabled]{opacity:.5;cursor:default}.button.button-disabled:hover,.button.button-disabled:focus,.button[disabled]:hover,.button[disabled]:focus,button.button-disabled:hover,button.button-disabled:focus,button[disabled]:hover,button[disabled]:focus,input[type='button'].button-disabled:hover,input[type='button'].button-disabled:focus,input[type='button'][disabled]:hover,input[type='button'][disabled]:focus,input[type='reset'].button-disabled:hover,input[type='reset'].button-disabled:focus,input[type='reset'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='submit'].button-disabled:hover,input[type='submit'].button-disabled:focus,input[type='submit'][disabled]:hover,input[type='submit'][disabled]:focus{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{color:#9b4dca;background-color:transparent}.button.button-outline:hover,.button.button-outline:focus,button.button-outline:hover,button.button-outline:focus,input[type='button'].button-outline:hover,input[type='button'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='submit'].button-outline:hover,input[type='submit'].button-outline:focus{color:#606c76;background-color:transparent;border-color:#606c76}.button.button-outline.button-disabled:hover,.button.button-outline.button-disabled:focus,.button.button-outline[disabled]:hover,.button.button-outline[disabled]:focus,button.button-outline.button-disabled:hover,button.button-outline.button-disabled:focus,button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,input[type='button'].button-outline.button-disabled:hover,input[type='button'].button-outline.button-disabled:focus,input[type='button'].button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='reset'].button-outline.button-disabled:hover,input[type='reset'].button-outline.button-disabled:focus,input[type='reset'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='submit'].button-outline.button-disabled:hover,input[type='submit'].button-outline.button-disabled:focus,input[type='submit'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus{color:#9b4dca;border-color:inherit}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{color:#9b4dca;background-color:transparent;border-color:transparent}.button.button-clear:hover,.button.button-clear:focus,button.button-clear:hover,button.button-clear:focus,input[type='button'].button-clear:hover,input[type='button'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='submit'].button-clear:hover,input[type='submit'].button-clear:focus{color:#606c76;background-color:transparent;border-color:transparent}.button.button-clear.button-disabled:hover,.button.button-clear.button-disabled:focus,.button.button-clear[disabled]:hover,.button.button-clear[disabled]:focus,button.button-clear.button-disabled:hover,button.button-clear.button-disabled:focus,button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,input[type='button'].button-clear.button-disabled:hover,input[type='button'].button-clear.button-disabled:focus,input[type='button'].button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='reset'].button-clear.button-disabled:hover,input[type='reset'].button-clear.button-disabled:focus,input[type='reset'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='submit'].button-clear.button-disabled:hover,input[type='submit'].button-clear.button-disabled:focus,input[type='submit'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;padding:.2rem .5rem;margin:0 .2rem;white-space:nowrap}pre{background:#f4f5f6;border-left:.3rem solid #9b4dca;font-family:\"Menlo\",\"Consolas\",\"Bitstream Vera Sans Mono\",\"DejaVu Sans Mono\",\"Monaco\",monospace}pre>code{background:transparent;border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:.1rem solid #f4f5f6;margin-bottom:3.5rem;margin-top:3rem}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{appearance:none;background-color:transparent;border:.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;height:3.8rem;padding:.6rem 1rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border:.1rem solid #9b4dca;outline:0}select{padding:.6rem 3rem .6rem 1rem;background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjkgMTQiICAgaGVpZ2h0PSIxNHB4IiAgIGlkPSJMYXllcl8xIiAgIHZlcnNpb249IjEuMSIgICB2aWV3Qm94PSIwIDAgMjkgMTQiICAgd2lkdGg9IjI5cHgiICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ4LjQgcjk5MzkiICAgc29kaXBvZGk6ZG9jbmFtZT0iY2FyZXQtZ3JheS5zdmciPjxtZXRhZGF0YSAgICAgaWQ9Im1ldGFkYXRhMzAzOSI+PHJkZjpSREY+PGNjOldvcmsgICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+PC9jYzpXb3JrPjwvcmRmOlJERj48L21ldGFkYXRhPjxkZWZzICAgICBpZD0iZGVmczMwMzciIC8+PHNvZGlwb2RpOm5hbWVkdmlldyAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiICAgICBib3JkZXJvcGFjaXR5PSIxIiAgICAgb2JqZWN0dG9sZXJhbmNlPSIxMCIgICAgIGdyaWR0b2xlcmFuY2U9IjEwIiAgICAgZ3VpZGV0b2xlcmFuY2U9IjEwIiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAiICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSI5MDMiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI1OTQiICAgICBpZD0ibmFtZWR2aWV3MzAzNSIgICAgIHNob3dncmlkPSJ0cnVlIiAgICAgaW5rc2NhcGU6em9vbT0iMTIuMTM3OTMxIiAgICAgaW5rc2NhcGU6Y3g9Ii00LjExOTMxODJlLTA4IiAgICAgaW5rc2NhcGU6Y3k9IjciICAgICBpbmtzY2FwZTp3aW5kb3cteD0iNTAyIiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjMwMiIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJMYXllcl8xIj48aW5rc2NhcGU6Z3JpZCAgICAgICB0eXBlPSJ4eWdyaWQiICAgICAgIGlkPSJncmlkMzA0MSIgLz48L3NvZGlwb2RpOm5hbWVkdmlldz48cG9seWdvbiAgICAgcG9pbnRzPSIwLjE1LDAgMTQuNSwxNC4zNSAyOC44NSwwICIgICAgIGlkPSJwb2x5Z29uMzAzMyIgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAuMzU0MTEzODcsMCwwLDAuNDgzMjkxMSw5LjMyNDE1NDUsMy42MjQ5OTkyKSIgICAgIHN0eWxlPSJmaWxsOiNkMWQxZDE7ZmlsbC1vcGFjaXR5OjEiIC8+PC9zdmc+) center right no-repeat}select:focus{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjkgMTQiICAgaGVpZ2h0PSIxNHB4IiAgIGlkPSJMYXllcl8xIiAgIHZlcnNpb249IjEuMSIgICB2aWV3Qm94PSIwIDAgMjkgMTQiICAgd2lkdGg9IjI5cHgiICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ4LjQgcjk5MzkiICAgc29kaXBvZGk6ZG9jbmFtZT0iY2FyZXQuc3ZnIj48bWV0YWRhdGEgICAgIGlkPSJtZXRhZGF0YTMwMzkiPjxyZGY6UkRGPjxjYzpXb3JrICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcyAgICAgaWQ9ImRlZnMzMDM3IiAvPjxzb2RpcG9kaTpuYW1lZHZpZXcgICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIgICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IiAgICAgYm9yZGVyb3BhY2l0eT0iMSIgICAgIG9iamVjdHRvbGVyYW5jZT0iMTAiICAgICBncmlkdG9sZXJhbmNlPSIxMCIgICAgIGd1aWRldG9sZXJhbmNlPSIxMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIgICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iOTAzIiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNTk0IiAgICAgaWQ9Im5hbWVkdmlldzMwMzUiICAgICBzaG93Z3JpZD0idHJ1ZSIgICAgIGlua3NjYXBlOnpvb209IjEyLjEzNzkzMSIgICAgIGlua3NjYXBlOmN4PSItNC4xMTkzMTgyZS0wOCIgICAgIGlua3NjYXBlOmN5PSI3IiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjUwMiIgICAgIGlua3NjYXBlOndpbmRvdy15PSIzMDIiICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIwIiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0iTGF5ZXJfMSI+PGlua3NjYXBlOmdyaWQgICAgICAgdHlwZT0ieHlncmlkIiAgICAgICBpZD0iZ3JpZDMwNDEiIC8+PC9zb2RpcG9kaTpuYW1lZHZpZXc+PHBvbHlnb24gICAgIHBvaW50cz0iMjguODUsMCAwLjE1LDAgMTQuNSwxNC4zNSAiICAgICBpZD0icG9seWdvbjMwMzMiICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLjM1NDExMzg3LDAsMCwwLjQ4MzI5MTEsOS4zMjQxNTUzLDMuNjI1KSIgICAgIHN0eWxlPSJmaWxsOiM5YjRkY2Y7ZmlsbC1vcGFjaXR5OjEiIC8+PC9zdmc+)}textarea{padding-bottom:.6rem;padding-top:.6rem;min-height:6.5rem}label,legend{font-size:1.6rem;font-weight:700;display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{font-weight:normal;display:inline-block;margin-left:.5rem}.container{margin:0 auto;max-width:112rem;padding:0 2rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row .row-wrap{flex-wrap:wrap}.row .row-no-padding{padding:0}.row .row-no-padding>.column{padding:0}.row .row-top{align-items:flex-start}.row .row-bottom{align-items:flex-end}.row .row-center{align-items:center}.row .row-stretch{align-items:stretch}.row .row-baseline{align-items:baseline}.row .column{display:block;flex:1;margin-left:0;max-width:100%;width:100%}.row .column .col-top{align-self:flex-start}.row .column .col-bottom{align-self:flex-end}.row .column .col-center{align-self:center}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1rem}}a{color:#9b4dca;text-decoration:none}a:hover{color:#606c76}dl,ol,ul{margin-top:0;padding-left:0}dl ul,dl ol,ol ul,ol ol,ul ul,ul ol{font-size:90%;margin:1.5rem 0 1.5rem 3rem}dl{list-style:none}ul{list-style:circle inside}ol{list-style:decimal inside}dt,dd,li{margin-bottom:1rem}.button,button{margin-bottom:1rem}input,textarea,select,fieldset{margin-bottom:1.5rem}pre,blockquote,dl,figure,table,p,ul,ol,form{margin-bottom:2.5rem}table{width:100%}th,td{border-bottom:.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}th:first-child,td:first-child{padding-left:0}th:last-child,td:last-child{padding-right:0}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;margin-bottom:2rem;margin-top:0}h1{font-size:4rem;letter-spacing:-0.1rem;line-height:1.2}h2{font-size:3.6rem;letter-spacing:-0.1rem;line-height:1.25}h3{font-size:3rem;letter-spacing:-0.1rem;line-height:1.3}h4{font-size:2.4rem;letter-spacing:-0.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-0.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}@media (min-width: 40rem){h1{font-size:5rem}h2{font-size:4.2rem}h3{font-size:3.6rem}h4{font-size:3rem}h5{font-size:2.4rem}h6{font-size:1.5rem}}.float-right{float:right}.float-left{float:left}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{content:\"\";display:table}.clearfix:after{clear:both}\n" + ] +} \ No newline at end of file diff --git a/report/css/normalize.css b/report/css/normalize.css new file mode 100644 index 00000000..5e5e3c89 --- /dev/null +++ b/report/css/normalize.css @@ -0,0 +1,424 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS and IE text size adjust after device orientation change, + * without disabling user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability of focused elements when they are also in an + * active/hover state. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + box-sizing: content-box; /* 2 */ +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/report/css/roboto.css b/report/css/roboto.css new file mode 100644 index 00000000..7ab7186d --- /dev/null +++ b/report/css/roboto.css @@ -0,0 +1,12 @@ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/roboto-light.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + src: local('Roboto Bold'), local('Roboto-Bold'), url(../fonts/roboto-bold.ttf) format('truetype'); +} diff --git a/report/css/style.css b/report/css/style.css new file mode 100644 index 00000000..442d553b --- /dev/null +++ b/report/css/style.css @@ -0,0 +1,705 @@ +/* ------------- layout -------- */ +body { + background: #EAEAEA; + padding-top: 80px; + font-family: "Roboto", "Helvetica Neue", "Helvetica", "Arial", sans-serif; +} + +.row { + margin-bottom: .5em; + align-items: stretch; +} + +.headerbar { + background-color: #fff; + border-bottom:3px solid #E4E4E4; + height:80px; + line-height:80px; + position: fixed; + top:0; + width: 100%; + z-index: 1; +} +.headerbarInner { + padding: 0 1em; +} +.headerbar img { + display:inline-block;; + vertical-align: middle; + height:60px; +} +.headerbar .title { + display:inline-block;; + font-size: 1.2em; + font-weight: bold; +} +.headerbar .subtitle { + display:inline-block;; + font-size: 1.2em; +} + + + +/* ----------- text ---------- */ +a { + color: #4CAF50; + cursor: pointer; +} + + +/* ------------- menu -------- */ +.navigation { + left: 0; + max-width: 100vw; + max-width: 100%; + right: 0; + top: 0; + z-index: 99; + margin-bottom: 1em; +} + +/* Re-overiding the width 100% declaration to match size of% based container */ +.navigation .container { + padding-bottom: 0; + padding-top: 0; +} + +.navigation { + background: #f4f5f6; + border-bottom: .1rem solid #d1d1d1; + display: block; + height: 5.2rem; + width: 100%; +} +.navigation-list { + list-style: none; + margin-bottom: 0; + padding-right: 1.5em; +} + +@media (min-width: 80.0rem) { + .navigation-list { + margin-right: 0; + } +} +@media (max-width: 600px) { + .navigation-list { + display:none + } +} + + +.navigation-item { + float: left; + margin-bottom: 0; + margin-left: 2.5rem; + position: relative; +} + +.navigation .img { + height: 2.0rem; + position: relative; + top: .3rem; +} + +.navigation .title, +.navigation-title a { + color: #606c76; + display: inline; + font-family: 'Gotham Rounded A', 'Gotham Rounded B', 'Helvetica Neue', Arial, sans-serif; + font-size: 1.6rem; + line-height: 5.2rem; + padding: 0; + position: relative; + text-decoration: none; +} + +.navigation-link { + display: inline; + font-size: 1.6rem; + line-height: 5.2rem; + padding: 0; + text-decoration: none; +} + +.navigation-link.active { + color: #606c76; +} + +/* Github */ +.github { + border: 0; + color: #f4f5f6; + fill: #4CAF50; + height: 5.2rem; + position: fixed; + right: 0; + top: 0; + width: 5.2rem; + z-index: 99; +} + +.github:hover .octo-arm { + -webkit-animation: octocat-wave 560ms infinite; + animation: octocat-wave 560ms infinite; +} + +@-webkit-keyframes octocat-wave { + 0%, 50% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 25%, 75% { + -webkit-transform: rotate(-25deg); + transform: rotate(-25deg); + } +} + +@keyframes octocat-wave { + 0%, 50% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 25%, 75% { + -webkit-transform: rotate(-25deg); + transform: rotate(-25deg); + } +} + +/* ---------- sidebar ------------- */ +.page { + margin-left: 300px; +} +.page .content { + padding: 2em 2em; + position:relative; +} +.content-first { + margin-top: 80px; +} +.content-full { + padding: 0; +} +.report-details { + position: absolute; + top:0; + right: 3em; + color: #666; + font-size: 0.8em; +} +.report-details a { + color: #666; + text-decoration: underline; +} +@media (max-width: 600px) { + .report-details { + display: none; + } +} + + +#sidebar { + position: fixed; + top: 80px; + left: 0; + background: #4CAF50; + width: 300px; + height: 92%; + overflow: auto; + color: #FFF; + text-align: left; +} +@media (max-width: 600px) { + #sidebar { + display: none; + } + .page { + margin-left: 0; + } +} + +#sidebar .content { + padding: 1em; +} + +#sidebar .logo { + text-align: center; + margin-bottom: 1em; +} + +#sidebar .logo img { + width: 150px; +} + +#sidebar .links ul { + list-style: none; +} + +#sidebar .links li { +} + +#sidebar .links a { + display: block; + color: #FFF; + line-height: 24px; + padding: 10px; +} +#sidebar .links .sep { + margin-top:1em; + padding-top: 1em; + border-top:1px solid #81C784; +} + +#sidebar .links svg, #sidebar .links img { + vertical-align: top; + margin-right: .5em; +} + +#sidebar .links a:hover { + background-color: #81C784; +} + +/* ------------- fullwidth -------- */ +.fullwidth #content { + margin-left: 0; +} +.fullwidth #content, .fullwidth .container, .fullwidth .row, .fullwidth .column { + width: auto; + max-width:none; + flex:auto; + float:none; + +} + + +/* ------------- text -------- */ +.badge { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + background-color: #EEE; + color: #333; + display: inline-block; + padding: 1px 5px; + margin: 4px auto; + font-size: 0.8em; +} + +.badge-score { + background-color: #4CAF50; + color: #FFF; +} +.progress { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + background-color: #EEE; + color: #333; + display: inline-block; + padding: 1px 5px; + font-size: 0.8em; + position: absolute; + right: 10px; + top: 10px; +} +.progress svg { + vertical-align:middle;; +} +.progress-good { + background-color: #4CAF50; + color: #FFF; +} +.progress-bad { + background-color: #F44336; + color: #FFF; +} +.path { + font-family: "Menlo", "Consolas", "Bitstream Vera Sans Mono", "DejaVu Sans Mono", "Monaco", monospace; + color: #2f855a; + background-color: #f0fff4; + display: inline-block; + padding: 1px 4px; +} +a[target="_blank"]::before { + content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); + margin: 0 5px 0 3px; +} + + +/* ------------ Bloc number ------ */ +.bloc { + position: relative; + text-align: center; + background: #FFF; + padding: 15px; + border: 0; + box-shadow: 0 2px 7px 0 rgba(42, 51, 83, 0.12), 0 5px 15px rgba(0, 0, 0, 0.06); + transition: all .15s ease; + border-radius: .5rem; + border-top: 4px solid #48bb78 +} + +.bloc .number { + font-size: 2.1em; + font-weight: bold; + color: #333; + text-align: left; +} +.bloc .number, .bloc .number-alternate { + min-height: 55px; +} +.bloc .chart-in-number { + margin-top:1em; +} +.bloc .bloc-action { + background-color: #f3f7fa; + text-align: center; + padding:10px 0; + margin:0 -15px -15px -15px; + -webkit-border-bottom-right-radius: 3px; + -webkit-border-bottom-left-radius: 3px; + -moz-border-radius-bottomright: 3px; + -moz-border-radius-bottomleft: 3px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + font-size: 0.9em; + color: #95999c; +} +.bloc .bloc-action a { + color: #48566c; + text-decoration: none; +} +.bloc .bloc-action a:hover { + color: #000; +} +.bloc .label { + color: #333; + text-align: left; + margin-bottom:.5em; + font-weight: 700; +} + +.bloc-number { + min-height: 140px; +} +.bloc h4 { + text-align: left; +} +.column.with-help { + padding-right:0; + padding-bottom:0; +} +.column-help .column-help-inner { + background-color: #fff; + height:100%; +} +.help { + padding-left:0; + color: hsl(0, 0%, 55%); + text-align: left; + font-size: 0.9em; +} +.column.help { + align-items: stretch; + display: flex; +} +.column.help .help-inner { + border-left:2px solid #E4E4E4; + padding:1em; + margin-bottom: 0 !important; +} +.column-help { + margin-bottom: 0 !important; +} + +/* ----- list ----- */ +.list { + text-align: left; +} +.list-item { + padding:1em; + position: relative; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.list-item-title { + font-weight: bold; +} +.list-item:hover { + background-color: #EBF8FF; +} +.table-metrics { + margin: 0.5em; + text-align: center; +} +.table-metrics td { + text-align: center; +} +.table-metrics .card-number { + font-weight: bold; +} +.table-metrics .card-label { + color: #333; + font-size: 0.9em; +} + +/* -------- charts ---------------- */ +.tooltip { + position: absolute; + background: #333; + border-radius: 5px; + padding: 5px 15px; + box-shadow: 1px 1px 3px; + text-align: left; + color: #FFF; + z-index: 4; +} + +.bar { + fill: #4CAF50; +} + +.bar:hover { + fill: #81C784; +} + +.axis { + font: 10px sans-serif; +} + +.axis path, +.axis line, +.scattered-plot path { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.x.axis path { + display: none; +} +.scattered-plot .x.axis path { + display: block; +} +.axis path, +.axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} +.svg-container { + position:relative; +} +.btn-save-image { + position:absolute; + top: 0; + left: 0; + background:#333; + color: #FFF; + font-size: 0.8em; + line-height: 1em; + height: 1em; + cursor: pointer; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; + filter: alpha(opacity=20); + -moz-opacity: 0.20; + -khtml-opacity: 0.20; + opacity: 0.20; + transition: opacity .2s ease-out; + -moz-transition: opacity .2s ease-out; + -webkit-transition: opacity .2s ease-out; + -o-transition: opacity .2s ease-out; +} +.btn-save-image:hover { + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; + filter: alpha(opacity=100); + -moz-opacity: 1; + -khtml-opacity: 1; + opacity: 1; +} + +/* -------- Table ------------ */ +table tr td { + border:none; + padding: 4px 0; +} +#table-length tbody { + font-size: 0.8em; +} + +#table-junit tbody { + font-size: 0.8em; +} + +#table-pagerank tbody { + font-size: 0.8em; +} + +#table-relations tbody { + font-size: 0.8em; +} +.table-small { + font-size:0.8em; +} + +#pagination a { + display: inline-block; + padding: 0 .5em; + cursor: pointer; +} + +.js-sort-table thead th { + cursor: pointer; +} + +/* ---- tabs ---- */ +.tabs { + list-style: none; + margin: 0; + padding: 0; +} +.tabs li { + list-style: none; + display: inline-block; + margin:0; +} +.tabs li a { + text-decoration: none; + padding: .5em 1em; + display: inline-block; + border-top: 4px solid #FFF; + border-bottom: 4px solid #FFF; +} +.tabs li a:hover, .tabs li.active a { + border-bottom: 4px solid #48bb78; +} +.tabs li.active a { + font-weight: bold; +} + +.group-tabs { + background-color: #fff; + line-height: 2em; + margin-bottom: 1em; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +/* ---- relations ---- */ +.node { + font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif; + fill: #bbb; +} + +.node:hover { + fill: #000; +} + +.link { + stroke: steelblue; + stroke-opacity: .4; + fill: none; + pointer-events: none; +} + +.node:hover, +.node--source, +.node--target { + font-weight: 700; +} + +.node--source { + fill: #AE113D; +} + +.node--target { + fill: #4617B4; +} + +.link--source, +.link--target { + stroke-opacity: 1; + stroke-width: 2px; +} + +.link--source { + stroke: #AE113D; +} + +.link--target { + stroke: #4617B4; +} + +.relation-source { + background-color: #AE113D; +} + +.relation-target { + background-color: #4617B4; +} + +/* ---------- footer ---------- */ +.container { + padding-bottom: 40px; +} + +footer { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + background: #FFF; + border-top: 1px solid #CCC; + padding: 5px 0; + text-align: center; + font-size: .8em; +} + + +/* ---------- violations ---------- */ +.violation-list { + display: none; +} +.violation { + padding-left:50px; + margin-top: .5em; +} +.violation .name { + font-weight: bold; + margin-top:1em; +} +.progress-good { + background-color: #4CAF50; + color: #FFF; +} +.level-critical{ + background-color: #F44336; + color: #FFF; +} +.level-error{ + background-color: #F44336; + color: #FFF; +} +.level-warning{ + background-color: darkorange; + color: #FFF; +} + + +/* ------- overrides -------- */ +@media (min-width: 600px) { + .clusterize-scroll { + max-height: 400px !important; + } +} + + +/* ------ composer ----- */ +.help-warning { + background-color: #fbd38d; +} +.help-info { + background-color:#A7F9FC; +} diff --git a/report/favicon.ico b/report/favicon.ico new file mode 100644 index 00000000..36692aec Binary files /dev/null and b/report/favicon.ico differ diff --git a/report/fonts/material-icons.ttf b/report/fonts/material-icons.ttf new file mode 100644 index 00000000..691bd26d Binary files /dev/null and b/report/fonts/material-icons.ttf differ diff --git a/report/fonts/roboto-bold.ttf b/report/fonts/roboto-bold.ttf new file mode 100644 index 00000000..c76118b2 Binary files /dev/null and b/report/fonts/roboto-bold.ttf differ diff --git a/report/fonts/roboto-light.ttf b/report/fonts/roboto-light.ttf new file mode 100644 index 00000000..ef52b760 Binary files /dev/null and b/report/fonts/roboto-light.ttf differ diff --git a/report/images/logo-git.png b/report/images/logo-git.png new file mode 100644 index 00000000..adf6623a Binary files /dev/null and b/report/images/logo-git.png differ diff --git a/report/images/logo.png b/report/images/logo.png new file mode 100644 index 00000000..eacdee22 Binary files /dev/null and b/report/images/logo.png differ diff --git a/report/images/phpmetrics-maintenability.png b/report/images/phpmetrics-maintenability.png new file mode 100644 index 00000000..eacdee22 Binary files /dev/null and b/report/images/phpmetrics-maintenability.png differ diff --git a/report/index.html b/report/index.html new file mode 100644 index 00000000..9dc9b7ce --- /dev/null +++ b/report/index.html @@ -0,0 +1,1816 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ +
+
+
+
Violations (0 criticals, 8 errors) +
+
26
+ +
+
+
+
+ +
4,961
+ +
+
+
+
+ +
142
+ +
+
+
+ + +
+ +
+
+ +
+
+ No JUnit report found. Use the --junit=<junit.xml> option to analyse your unit tests. + See documentation of PHPUnit if needed +
+
+
+ No details +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ Maintainability / complexity + + (with comments) + +
+
+
+
+
+
+

Each file is symbolized by a circle. Size of the circle represents the Cyclomatic + complexity. + Color of the circle represents the Maintainability Index.

+

Large red circles will be probably hard to maintain.

+
+
+
+
+
+
+ +
+
+ +
+
+

+ Page Rank is a way to measure the importance of a class. There is no "good" or "bad" page rank. This metric reflects interactions in your code. +

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassRank
+ 0.1 + + App\Models\Order + 81.02 + 44.71 +
+ 0.06 + + App\Models\Store + 97.12 + 56.27 +
+ 0.04 + + App\Models\Cart + 96 + 57.91 +
+ 0.04 + + App\Models\Product + 95.92 + 55.07 +
+ 0.02 + + App\Models\WebhookSubscription + 100.98 + 62.64 +
+ 0.02 + + App\Models\User + 92.09 + 49.64 +
+ 0.02 + + App\Models\Fulfillment + 99.17 + 59.7 +
+ 0.02 + + App\Models\Checkout + 99.55 + 60.56 +
+ 0.02 + + App\Support\CartSession + 49.84 + 49.84 +
+ 0.01 + + App\Models\NavigationItem + 88.64 + 55.06 +
+ 0.01 + + App\Models\Refund + 100.54 + 61.07 +
+ 0.01 + + App\Models\InventoryItem + 96.03 + 60.03 +
+ 0.01 + + App\Models\NavigationMenu + 105.83 + 69 +
+ 0.01 + + App\Models\AppInstallation + 97.35 + 58.82 +
+ 0.01 + + App\Models\Discount + 77.67 + 51.26 +
+ 0.01 + + App\Models\Theme + 101.3 + 61.34 +
+ 0.01 + + App\Models\ProductMedia + 98.91 + 61.19 +
+ 0.01 + + App\Models\WebhookDelivery + 99 + 61.29 +
+ 0.01 + + App\Models\Payment + 99.05 + 59.58 +
+ 0.01 + + App\Models\AnalyticsDaily + 97.28 + 55.94 +
+ 0.01 + + App\Models\Customer + 90.66 + 52.51 +
+ 0.01 + + App\Models\TaxSettings + 95.77 + 59.77 +
+ 0.01 + + App\Models\ShippingZone + 102.96 + 63.97 +
+ 0.01 + + App\Models\Collection + 102.41 + 63.41 +
+ 0.01 + + App\Models\Page + 104.54 + 67.71 +
+ 0.01 + + App\Models\ShippingRate + 102.32 + 63.33 +
+ 0.01 + + App\Models\CustomerAddress + 101.92 + 63.58 +
+ 0.01 + + App\Models\FulfillmentLine + 104.07 + 65.73 +
+ 0.01 + + App\Models\AnalyticsEvent + 99.72 + 64.75 +
+ 0.01 + + App\Models\Scopes\StoreScope + 85.71 + 64.88 +
+ 0.01 + + App\Exceptions\InvalidDiscountException + 52.71 + 52.71 +
+ 0.01 + + App\Livewire\Actions\Logout + 105.27 + 72.41 +
+ 0.01 + + App\Support\HandleGenerator + 51.82 + 51.82 +
+ 0.01 + + App\Jobs\DeliverWebhook + 70.68 + 43.53 +
+ 0.01 + + App\Events\OrderCreated + 171 + 171 +
+ 0.01 + + App\Events\OrderPaid + 171 + 171 +
+ 0.01 + + App\Events\OrderFulfilled + 171 + 171 +
+ 0.01 + + App\Services\WebhookService + 84.08 + 57.02 +
+ 0.01 + + App\Services\OrderService + 45.1 + 33.05 +
+ 0.01 + + App\Services\CheckoutService + 43.38 + 25.03 +
+ 0.01 + + App\Services\FulfillmentService + 69.57 + 41.94 +
+ 0.01 + + App\Services\InventoryService + 45.93 + 45.93 +
+ 0.01 + + App\Services\ProductService + 48.4 + 27.5 +
+ 0.01 + + App\Services\AnalyticsService + 100.52 + 61.53 +
+ 0.01 + + App\Services\SearchService + 65.8 + 40.36 +
+ 0.01 + + App\ValueObjects\PaymentResult + 60.6 + 60.6 +
+ 0.01 + + App\ValueObjects\DiscountResult + 114.21 + 76.69 +
+ 0.01 + + App\ValueObjects\PricingResult + 109.23 + 63.06 +
+ 0.01 + + App\ValueObjects\TaxLine + 100.45 + 67.58 +
+ 0.01 + + App\ValueObjects\RefundResult + 68.09 + 68.09 +
+ 0 + + App\Auth\CustomerUserProvider + 62.02 + 34.05 +
+ 0 + + App\Providers\AppServiceProvider + 85.37 + 50.66 +
+ 0 + + App\Providers\FortifyServiceProvider + 88.27 + 50.93 +
+ 0 + + App\Models\OrderLine + 95.52 + 55 +
+ 0 + + App\Models\ThemeFile + 102.21 + 63.87 +
+ 0 + + App\Models\ProductOption + 104.07 + 65.73 +
+ 0 + + App\Models\App + 101.75 + 63.41 +
+ 0 + + App\Models\CartLine + 101.49 + 63.15 +
+ 0 + + App\Models\StoreDomain + 101.92 + 63.58 +
+ 0 + + App\Models\ThemeSettings + 97.83 + 61.83 +
+ 0 + + App\Models\ProductVariant + 96.78 + 56.28 +
+ 0 + + App\Models\ProductOptionValue + 105.01 + 69.14 +
+ 0 + + App\Models\StoreSettings + 99.46 + 62.91 +
+ 0 + + App\Models\StoreUser + 88 + 61.46 +
+ 0 + + App\Models\Organization + 108.56 + 71.73 +
+ 0 + + App\Models\Concerns\BelongsToStore + 91.36 + 60.68 +
+ 0 + + App\Exceptions\InsufficientInventoryException + 171 + 171 +
+ 0 + + App\Exceptions\FulfillmentGuardException + 171 + 171 +
+ 0 + + App\Exceptions\PaymentFailedException + 171 + 171 +
+ 0 + + App\Policies\StorePolicy + 60.23 + 60.23 +
+ 0 + + App\Policies\Concerns\ChecksStoreRole + 77.44 + 59.58 +
+ 0 + + App\Livewire\Settings\TwoFactor + 74.44 + 38.1 +
+ 0 + + App\Livewire\Settings\DeleteUserForm + 99.37 + 67.44 +
+ 0 + + App\Livewire\Settings\TwoFactor\RecoveryCodes + 92.57 + 56.13 +
+ 0 + + App\Livewire\Settings\Password + 84.33 + 57.79 +
+ 0 + + App\Livewire\Settings\Profile + 81.49 + 48.81 +
+ 0 + + App\Livewire\Settings\Appearance + 202.94 + 171 +
+ 0 + + App\Livewire\Storefront\Products\Show + 54.11 + 43.55 +
+ 0 + + App\Livewire\Storefront\Home + 83.79 + 52.94 +
+ 0 + + App\Livewire\Storefront\Checkout\Show + 52.12 + 28.19 +
+ 0 + + App\Livewire\Storefront\Checkout\Confirmation + 81.42 + 62.53 +
+ 0 + + App\Livewire\Storefront\Search\Index + 80.58 + 58.07 +
+ 0 + + App\Livewire\Storefront\CartDrawer + 73.29 + 53.51 +
+ 0 + + App\Livewire\Storefront\Cart\Show + 55.41 + 44.76 +
+ 0 + + App\Livewire\Storefront\Account\Dashboard + 87.28 + 61.94 +
+ 0 + + App\Livewire\Storefront\Account\Auth\Login + 64.33 + 51.01 +
+ 0 + + App\Livewire\Storefront\Account\Auth\Register + 70.32 + 51.43 +
+ 0 + + App\Livewire\Storefront\Account\Addresses\Index + 67.36 + 44.25 +
+ 0 + + App\Livewire\Storefront\Account\Orders\Index + 87.66 + 62.32 +
+ 0 + + App\Livewire\Storefront\Account\Orders\Show + 85.35 + 60.67 +
+ 0 + + App\Livewire\Storefront\Collections\Index + 83.53 + 64.06 +
+ 0 + + App\Livewire\Storefront\Collections\Show + 70.52 + 51.63 +
+ 0 + + App\Livewire\Storefront\Pages\Show + 80.01 + 61.66 +
+ 0 + + App\Livewire\Storefront\Concerns\EnsuresStore + 91.4 + 64.6 +
+ 0 + + App\Livewire\Admin\Customers\Index + 78.7 + 56.19 +
+ 0 + + App\Livewire\Admin\Customers\Show + 77.75 + 58.87 +
+ 0 + + App\Livewire\Admin\Settings\Taxes + 80.07 + 50.15 +
+ 0 + + App\Livewire\Admin\Settings\Index + 84.84 + 52.52 +
+ 0 + + App\Livewire\Admin\Settings\Shipping + 65.37 + 41.53 +
+ 0 + + App\Livewire\Admin\Dashboard + 84.71 + 50.84 +
+ 0 + + App\Livewire\Admin\Products\Index + 71.45 + 48.44 +
+ 0 + + App\Livewire\Admin\Products\Form + 66.4 + 37.19 +
+ 0 + + App\Livewire\Admin\Auth\Login + 68.55 + 47.97 +
+ 0 + + App\Livewire\Admin\Navigation\Index + 60.51 + 38.13 +
+ 0 + + App\Livewire\Admin\Discounts\Index + 83.16 + 57.12 +
+ 0 + + App\Livewire\Admin\Discounts\Form + 69.57 + 40.21 +
+ 0 + + App\Livewire\Admin\Orders\Index + 75.17 + 49.13 +
+ 0 + + App\Livewire\Admin\Orders\Show + 45.03 + 34.22 +
+ 0 + + App\Livewire\Admin\Collections\Index + 84.35 + 59.67 +
+ 0 + + App\Livewire\Admin\Collections\Form + 63.1 + 38.1 +
+ 0 + + App\Livewire\Admin\Pages\Index + 77.27 + 55.64 +
+ 0 + + App\Livewire\Admin\Pages\Form + 69.45 + 42.53 +
+ 0 + + App\Livewire\Admin\Apps\Index + 76.05 + 50.37 +
+ 0 + + App\Livewire\Admin\Themes\Index + 70 + 48.92 +
+ 0 + + App\Livewire\Admin\Analytics\Index + 77.35 + 54.34 +
+ 0 + + App\Livewire\Admin\Developers\Index + 74.34 + 45.8 +
+ 0 + + App\Http\Middleware\ResolveStore + 43.35 + 43.35 +
+ 0 + + App\Http\Controllers\Controller + 202.94 + 171 +
+ 0 + + App\Actions\Fortify\ResetUserPassword + 108.25 + 69.26 +
+ 0 + + App\Actions\Fortify\CreateNewUser + 105.92 + 66.93 +
+ 0 + + App\Jobs\ExpireAbandonedCheckouts + 67.9 + 67.9 +
+ 0 + + App\Jobs\CleanupAbandonedCarts + 72.05 + 72.05 +
+ 0 + + App\Jobs\AggregateAnalytics + 49.28 + 49.28 +
+ 0 + + App\Jobs\CancelUnpaidBankTransferOrders + 66.52 + 66.52 +
+ 0 + + App\Jobs\ProcessMediaUpload + 65.29 + 65.29 +
+ 0 + + App\Events\OrderRefunded + 78.06 + 78.06 +
+ 0 + + App\Events\OrderCancelled + 171 + 171 +
+ 0 + + App\Events\FulfillmentDelivered + 171 + 171 +
+ 0 + + App\Observers\ProductObserver + 63.57 + 63.57 +
+ 0 + + App\Listeners\DispatchOrderWebhooks + 56.37 + 56.37 +
+ 0 + + App\Services\Payments\MockPaymentProvider + 75.89 + 47.96 +
+ 0 + + App\Services\TaxCalculator + 73.32 + 47.98 +
+ 0 + + App\Services\ThemeSettingsService + 91.19 + 54.58 +
+ 0 + + App\Services\NavigationService + 92.68 + 59.02 +
+ 0 + + App\Services\RefundService + 57.3 + 45.18 +
+ 0 + + App\Services\ShippingCalculator + 60.12 + 32.77 +
+ 0 + + App\Services\CartService + 28.72 + 28.72 +
+ 0 + + App\Services\PricingEngine + 38 + 38 +
+ 0 + + App\Services\DiscountService + 50.15 + 35.37 +
+ 0 + + App\Services\VariantMatrixService + 62.86 + 39.33 +
+ 0 + + App\Concerns\ProfileValidationRules + 104.89 + 60.85 +
+ 0 + + App\Concerns\PasswordValidationRules + 110.67 + 67.44 +
+
+
+
+
+
+ +
+
+
+
+ Composer +
+
No composer.json file found
+ +
+ +
+
+
+
+
+ + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + diff --git a/report/js/FileSaver.min.js b/report/js/FileSaver.min.js new file mode 100644 index 00000000..183d42a1 --- /dev/null +++ b/report/js/FileSaver.min.js @@ -0,0 +1,3 @@ +(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(b,c,d){var e=new XMLHttpRequest;e.open("GET",b),e.responseType="blob",e.onload=function(){a(e.response,c,d)},e.onerror=function(){console.error("could not download file")},e.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(a,b,d,e){if(e=e||open("","_blank"),e&&(e.document.title=e.document.body.innerText="downloading..."),"string"==typeof a)return c(a,b,d);var g="application/octet-stream"===a.type,h=/constructor/i.test(f.HTMLElement)||f.safari,i=/CriOS\/[\d]+/.test(navigator.userAgent);if((i||g&&h)&&"undefined"!=typeof FileReader){var j=new FileReader;j.onloadend=function(){var a=j.result;a=i?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),e?e.location.href=a:location=a,e=null},j.readAsDataURL(a)}else{var k=f.URL||f.webkitURL,l=k.createObjectURL(a);e?e.location=l:location.href=l,e=null,setTimeout(function(){k.revokeObjectURL(l)},4E4)}});f.saveAs=a.saveAs=a,"undefined"!=typeof module&&(module.exports=a)}); + +//# sourceMappingURL=FileSaver.min.js.map \ No newline at end of file diff --git a/report/js/FileSaver.min.js.map b/report/js/FileSaver.min.js.map new file mode 100644 index 00000000..4fbcdd2e --- /dev/null +++ b/report/js/FileSaver.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/FileSaver.js"],"names":[],"mappings":"uLAkBA,QAAS,CAAA,CAAT,CAAc,CAAd,CAAoB,CAApB,CAA0B,OACJ,WAAhB,QAAO,CAAA,CADa,CACS,CAAI,CAAG,CAAE,OAAO,GAAT,CADhB,CAEC,QAAhB,QAAO,CAAA,CAFQ,GAGtB,OAAO,CAAC,IAAR,CAAa,oDAAb,CAHsB,CAItB,CAAI,CAAG,CAAE,OAAO,CAAE,CAAC,CAAZ,CAJe,EASpB,CAAI,CAAC,OAAL,EAAgB,6EAA6E,IAA7E,CAAkF,CAAI,CAAC,IAAvF,CATI,CAUf,GAAI,CAAA,IAAJ,CAAS,UAA8B,CAA9B,CAAT,CAA8C,CAAE,IAAI,CAAE,CAAI,CAAC,IAAb,CAA9C,CAVe,CAYjB,CACR,CAED,QAAS,CAAA,CAAT,CAAmB,CAAnB,CAAwB,CAAxB,CAA8B,CAA9B,CAAoC,CAClC,GAAI,CAAA,CAAG,CAAG,GAAI,CAAA,cAAd,CACA,CAAG,CAAC,IAAJ,CAAS,KAAT,CAAgB,CAAhB,CAFkC,CAGlC,CAAG,CAAC,YAAJ,CAAmB,MAHe,CAIlC,CAAG,CAAC,MAAJ,CAAa,UAAY,CACvB,CAAM,CAAC,CAAG,CAAC,QAAL,CAAe,CAAf,CAAqB,CAArB,CACP,CANiC,CAOlC,CAAG,CAAC,OAAJ,CAAc,UAAY,CACxB,OAAO,CAAC,KAAR,CAAc,yBAAd,CACD,CATiC,CAUlC,CAAG,CAAC,IAAJ,EACD,CAED,QAAS,CAAA,CAAT,CAAsB,CAAtB,CAA2B,CACzB,GAAI,CAAA,CAAG,CAAG,GAAI,CAAA,cAAd,CAEA,CAAG,CAAC,IAAJ,CAAS,MAAT,CAAiB,CAAjB,IAHyB,CAIzB,GAAI,CACF,CAAG,CAAC,IAAJ,EACD,CAAC,MAAO,CAAP,CAAU,CAAE,CACd,MAAqB,IAAd,EAAA,CAAG,CAAC,MAAJ,EAAmC,GAAd,EAAA,CAAG,CAAC,MACjC,CAGD,QAAS,CAAA,CAAT,CAAgB,CAAhB,CAAsB,CACpB,GAAI,CACF,CAAI,CAAC,aAAL,CAAmB,GAAI,CAAA,UAAJ,CAAe,OAAf,CAAnB,CACD,CAAC,MAAO,CAAP,CAAU,CACV,GAAI,CAAA,CAAG,CAAG,QAAQ,CAAC,WAAT,CAAqB,aAArB,CAAV,CACA,CAAG,CAAC,cAAJ,CAAmB,OAAnB,OAAwC,MAAxC,CAAgD,CAAhD,CAAmD,CAAnD,CAAsD,CAAtD,CAAyD,EAAzD,CACsB,EADtB,aACsD,CADtD,CACyD,IADzD,CAFU,CAIV,CAAI,CAAC,aAAL,CAAmB,CAAnB,CACD,CACF,C,GAtDG,CAAA,CAAO,CAAqB,QAAlB,QAAO,CAAA,MAAP,EAA8B,MAAM,CAAC,MAAP,GAAkB,MAAhD,CACV,MADU,CACe,QAAhB,QAAO,CAAA,IAAP,EAA4B,IAAI,CAAC,IAAL,GAAc,IAA1C,CACT,IADS,CACgB,QAAlB,QAAO,CAAA,MAAP,EAA8B,MAAM,CAAC,MAAP,GAAkB,MAAhD,CACP,MADO,O,CAsDP,CAAM,CAAG,CAAO,CAAC,MAAR,GAEQ,QAAlB,QAAO,CAAA,MAAP,EAA8B,MAAM,GAAK,CAA1C,CACI,UAAmB,CAAc,CADrC,CAIE,YAAc,CAAA,iBAAiB,CAAC,SAAhC,CACA,SAAiB,CAAjB,CAAuB,CAAvB,CAA6B,CAA7B,CAAmC,IAC/B,CAAA,CAAG,CAAG,CAAO,CAAC,GAAR,EAAe,CAAO,CAAC,SADE,CAE/B,CAAC,CAAG,QAAQ,CAAC,aAAT,CAAuB,GAAvB,CAF2B,CAGnC,CAAI,CAAG,CAAI,EAAI,CAAI,CAAC,IAAb,EAAqB,UAHO,CAKnC,CAAC,CAAC,QAAF,CAAa,CALsB,CAMnC,CAAC,CAAC,GAAF,CAAQ,UAN2B,CAWf,QAAhB,QAAO,CAAA,CAXwB,EAajC,CAAC,CAAC,IAAF,CAAS,CAbwB,CAc7B,CAAC,CAAC,MAAF,GAAa,QAAQ,CAAC,MAdO,CAmB/B,CAAK,CAAC,CAAD,CAnB0B,CAe/B,CAAW,CAAC,CAAC,CAAC,IAAH,CAAX,CACI,CAAQ,CAAC,CAAD,CAAO,CAAP,CAAa,CAAb,CADZ,CAEI,CAAK,CAAC,CAAD,CAAI,CAAC,CAAC,MAAF,CAAW,QAAf,CAjBsB,GAuBjC,CAAC,CAAC,IAAF,CAAS,CAAG,CAAC,eAAJ,CAAoB,CAApB,CAvBwB,CAwBjC,UAAU,CAAC,UAAY,CAAE,CAAG,CAAC,eAAJ,CAAoB,CAAC,CAAC,IAAtB,CAA6B,CAA5C,CAA8C,GAA9C,CAxBuB,CAyBjC,UAAU,CAAC,UAAY,CAAE,CAAK,CAAC,CAAD,CAAK,CAAzB,CAA2B,CAA3B,CAzBuB,CA2BpC,CA5BC,CA+BA,oBAAsB,CAAA,SAAtB,CACA,SAAiB,CAAjB,CAAuB,CAAvB,CAA6B,CAA7B,CAAmC,CAGnC,GAFA,CAAI,CAAG,CAAI,EAAI,CAAI,CAAC,IAAb,EAAqB,UAE5B,CAAoB,QAAhB,QAAO,CAAA,CAAX,CAUE,SAAS,CAAC,gBAAV,CAA2B,CAAG,CAAC,CAAD,CAAO,CAAP,CAA9B,CAA4C,CAA5C,CAVF,KACE,IAAI,CAAW,CAAC,CAAD,CAAf,CACE,CAAQ,CAAC,CAAD,CAAO,CAAP,CAAa,CAAb,CADV,KAEO,CACL,GAAI,CAAA,CAAC,CAAG,QAAQ,CAAC,aAAT,CAAuB,GAAvB,CAAR,CACA,CAAC,CAAC,IAAF,CAAS,CAFJ,CAGL,CAAC,CAAC,MAAF,CAAW,QAHN,CAIL,UAAU,CAAC,UAAY,CAAE,CAAK,CAAC,CAAD,CAAK,CAAzB,CACX,CAIJ,CAhBC,CAmBA,SAAiB,CAAjB,CAAuB,CAAvB,CAA6B,CAA7B,CAAmC,CAAnC,CAA0C,CAS1C,GANA,CAAK,CAAG,CAAK,EAAI,IAAI,CAAC,EAAD,CAAK,QAAL,CAMrB,CALI,CAKJ,GAJE,CAAK,CAAC,QAAN,CAAe,KAAf,CACA,CAAK,CAAC,QAAN,CAAe,IAAf,CAAoB,SAApB,CAAgC,gBAGlC,EAAoB,QAAhB,QAAO,CAAA,CAAX,CAA8B,MAAO,CAAA,CAAQ,CAAC,CAAD,CAAO,CAAP,CAAa,CAAb,CAAf,CATY,GAWtC,CAAA,CAAK,CAAiB,0BAAd,GAAA,CAAI,CAAC,IAXyB,CAYtC,CAAQ,CAAG,eAAe,IAAf,CAAoB,CAAO,CAAC,WAA5B,GAA4C,CAAO,CAAC,MAZzB,CAatC,CAAW,CAAG,eAAe,IAAf,CAAoB,SAAS,CAAC,SAA9B,CAbwB,CAe1C,GAAI,CAAC,CAAW,EAAK,CAAK,EAAI,CAA1B,GAA8D,WAAtB,QAAO,CAAA,UAAnD,CAA+E,CAE7E,GAAI,CAAA,CAAM,CAAG,GAAI,CAAA,UAAjB,CACA,CAAM,CAAC,SAAP,CAAmB,UAAY,CAC7B,GAAI,CAAA,CAAG,CAAG,CAAM,CAAC,MAAjB,CACA,CAAG,CAAG,CAAW,CAAG,CAAH,CAAS,CAAG,CAAC,OAAJ,CAAY,cAAZ,CAA4B,uBAA5B,CAFG,CAGzB,CAHyB,CAGlB,CAAK,CAAC,QAAN,CAAe,IAAf,CAAsB,CAHJ,CAIxB,QAAQ,CAAG,CAJa,CAK7B,CAAK,CAAG,IACT,CAT4E,CAU7E,CAAM,CAAC,aAAP,CAAqB,CAArB,CACD,CAXD,IAWO,IACD,CAAA,CAAG,CAAG,CAAO,CAAC,GAAR,EAAe,CAAO,CAAC,SAD5B,CAED,CAAG,CAAG,CAAG,CAAC,eAAJ,CAAoB,CAApB,CAFL,CAGD,CAHC,CAGM,CAAK,CAAC,QAAN,CAAiB,CAHvB,CAIA,QAAQ,CAAC,IAAT,CAAgB,CAJhB,CAKL,CAAK,CAAG,IALH,CAML,UAAU,CAAC,UAAY,CAAE,CAAG,CAAC,eAAJ,CAAoB,CAApB,CAA0B,CAAzC,CAA2C,GAA3C,CACX,CACF,CA1FU,C,CA6Fb,CAAO,CAAC,MAAR,CAAiB,CAAM,CAAC,MAAP,CAAgB,C,CAEX,WAAlB,QAAO,CAAA,M,GACT,MAAM,CAAC,OAAP,CAAiB,C","file":"FileSaver.min.js","sourcesContent":["/*\n* FileSaver.js\n* A saveAs() FileSaver implementation.\n*\n* By Eli Grey, http://eligrey.com\n*\n* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)\n* source : http://purl.eligrey.com/github/FileSaver.js\n*/\n\n// The one and only way of getting global scope in all environments\n// https://stackoverflow.com/q/3277182/1008999\nvar _global = typeof window === 'object' && window.window === window\n ? window : typeof self === 'object' && self.self === self\n ? self : typeof global === 'object' && global.global === global\n ? global\n : this\n\nfunction bom (blob, opts) {\n if (typeof opts === 'undefined') opts = { autoBom: false }\n else if (typeof opts !== 'object') {\n console.warn('Deprecated: Expected third argument to be a object')\n opts = { autoBom: !opts }\n }\n\n // prepend BOM for UTF-8 XML and text/* types (including HTML)\n // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF\n if (opts.autoBom && /^\\s*(?:text\\/\\S*|application\\/xml|\\S*\\/\\S*\\+xml)\\s*;.*charset\\s*=\\s*utf-8/i.test(blob.type)) {\n return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type })\n }\n return blob\n}\n\nfunction download (url, name, opts) {\n var xhr = new XMLHttpRequest()\n xhr.open('GET', url)\n xhr.responseType = 'blob'\n xhr.onload = function () {\n saveAs(xhr.response, name, opts)\n }\n xhr.onerror = function () {\n console.error('could not download file')\n }\n xhr.send()\n}\n\nfunction corsEnabled (url) {\n var xhr = new XMLHttpRequest()\n // use sync to avoid popup blocker\n xhr.open('HEAD', url, false)\n try {\n xhr.send()\n } catch (e) {}\n return xhr.status >= 200 && xhr.status <= 299\n}\n\n// `a.click()` doesn't work for all browsers (#465)\nfunction click (node) {\n try {\n node.dispatchEvent(new MouseEvent('click'))\n } catch (e) {\n var evt = document.createEvent('MouseEvents')\n evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,\n 20, false, false, false, false, 0, null)\n node.dispatchEvent(evt)\n }\n}\n\nvar saveAs = _global.saveAs || (\n // probably in some web worker\n (typeof window !== 'object' || window !== _global)\n ? function saveAs () { /* noop */ }\n\n // Use download attribute first if possible (#193 Lumia mobile)\n : 'download' in HTMLAnchorElement.prototype\n ? function saveAs (blob, name, opts) {\n var URL = _global.URL || _global.webkitURL\n var a = document.createElement('a')\n name = name || blob.name || 'download'\n\n a.download = name\n a.rel = 'noopener' // tabnabbing\n\n // TODO: detect chrome extensions & packaged apps\n // a.target = '_blank'\n\n if (typeof blob === 'string') {\n // Support regular links\n a.href = blob\n if (a.origin !== location.origin) {\n corsEnabled(a.href)\n ? download(blob, name, opts)\n : click(a, a.target = '_blank')\n } else {\n click(a)\n }\n } else {\n // Support blobs\n a.href = URL.createObjectURL(blob)\n setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s\n setTimeout(function () { click(a) }, 0)\n }\n }\n\n // Use msSaveOrOpenBlob as a second approach\n : 'msSaveOrOpenBlob' in navigator\n ? function saveAs (blob, name, opts) {\n name = name || blob.name || 'download'\n\n if (typeof blob === 'string') {\n if (corsEnabled(blob)) {\n download(blob, name, opts)\n } else {\n var a = document.createElement('a')\n a.href = blob\n a.target = '_blank'\n setTimeout(function () { click(a) })\n }\n } else {\n navigator.msSaveOrOpenBlob(bom(blob, opts), name)\n }\n }\n\n // Fallback to using FileReader and a popup\n : function saveAs (blob, name, opts, popup) {\n // Open a popup immediately do go around popup blocker\n // Mostly only available on user interaction and the fileReader is async so...\n popup = popup || open('', '_blank')\n if (popup) {\n popup.document.title =\n popup.document.body.innerText = 'downloading...'\n }\n\n if (typeof blob === 'string') return download(blob, name, opts)\n\n var force = blob.type === 'application/octet-stream'\n var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari\n var isChromeIOS = /CriOS\\/[\\d]+/.test(navigator.userAgent)\n\n if ((isChromeIOS || (force && isSafari)) && typeof FileReader !== 'undefined') {\n // Safari doesn't allow downloading of blob URLs\n var reader = new FileReader()\n reader.onloadend = function () {\n var url = reader.result\n url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')\n if (popup) popup.location.href = url\n else location = url\n popup = null // reverse-tabnabbing #460\n }\n reader.readAsDataURL(blob)\n } else {\n var URL = _global.URL || _global.webkitURL\n var url = URL.createObjectURL(blob)\n if (popup) popup.location = url\n else location.href = url\n popup = null // reverse-tabnabbing #460\n setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s\n }\n }\n)\n\n_global.saveAs = saveAs.saveAs = saveAs\n\nif (typeof module !== 'undefined') {\n module.exports = saveAs;\n}\n"]} \ No newline at end of file diff --git a/report/js/clusterize.min.js b/report/js/clusterize.min.js new file mode 100644 index 00000000..b2d3c8e7 --- /dev/null +++ b/report/js/clusterize.min.js @@ -0,0 +1,16 @@ +/*! Clusterize.js - v0.17.6 - 2017-03-05 +* http://NeXTs.github.com/Clusterize.js/ +* Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */ + +;(function(q,n){"undefined"!=typeof module?module.exports=n():"function"==typeof define&&"object"==typeof define.amd?define(n):this[q]=n()})("Clusterize",function(){function q(b,a,c){return a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)}function n(b,a,c){return a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent("on"+b,c)}function r(b){return"[object Array]"===Object.prototype.toString.call(b)}function m(b,a){return window.getComputedStyle?window.getComputedStyle(a)[b]: +a.currentStyle[b]}var l=function(){for(var b=3,a=document.createElement("b"),c=a.all||[];a.innerHTML="\x3c!--[if gt IE "+ ++b+"]>=l&&!c.tag&&(c.tag=b[0].match(/<([^>\s/]*)/)[1].toLowerCase()),1>=this.content_elem.children.length&&(a.data=this.html(b[0]+b[0]+b[0])),c.tag||(c.tag=this.content_elem.children[0].tagName.toLowerCase()), +this.getRowsHeight(b))},getRowsHeight:function(b){var a=this.options,c=a.item_height;a.cluster_height=0;if(b.length){b=this.content_elem.children;var d=b[Math.floor(b.length/2)];a.item_height=d.offsetHeight;"tr"==a.tag&&"collapse"!=m("borderCollapse",this.content_elem)&&(a.item_height+=parseInt(m("borderSpacing",this.content_elem),10)||0);"tr"!=a.tag&&(b=parseInt(m("marginTop",d),10)||0,d=parseInt(m("marginBottom",d),10)||0,a.item_height+=Math.max(b,d));a.block_height=a.item_height*a.rows_in_block; +a.rows_in_cluster=a.blocks_in_cluster*a.rows_in_block;a.cluster_height=a.blocks_in_cluster*a.block_height;return c!=a.item_height}},getClusterNum:function(){this.options.scroll_top=this.scroll_elem.scrollTop;return Math.floor(this.options.scroll_top/(this.options.cluster_height-this.options.block_height))||0},generateEmptyRow:function(){var b=this.options;if(!b.tag||!b.show_no_data_row)return[];var a=document.createElement(b.tag),c=document.createTextNode(b.no_data_text),d;a.className=b.no_data_class; +"tr"==b.tag&&(d=document.createElement("td"),d.colSpan=100,d.appendChild(c));a.appendChild(d||c);return[a.outerHTML]},generate:function(b,a){var c=this.options,d=b.length;if(de&&g++;f=l&&"tr"==this.options.tag){var c=document.createElement("div");for(c.innerHTML=""+b+"
";b=a.lastChild;)a.removeChild(b);for(c=this.getChildNodes(c.firstChild.firstChild);c.length;)a.appendChild(c.shift())}else a.innerHTML=b},getChildNodes:function(b){b=b.children;for(var a=[],c=0,d=b.length;c 1) { + var px1 = px - pi, + pi2 = pi + (px < pi ? -1 : 1) / 2, + pj2 = pj + (py < pj ? -1 : 1), + px2 = px - pi2, + py2 = py - pj2; + if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2; + } + + var id = pi + "-" + pj, bin = binsById[id]; + if (bin) bin.push(point); else { + bin = binsById[id] = [point]; + bin.i = pi; + bin.j = pj; + bin.x = (pi + (pj & 1 ? 1 / 2 : 0)) * dx; + bin.y = pj * dy; + } + }); + + return d3.values(binsById); + } + + function hexagon(radius) { + var x0 = 0, y0 = 0; + return d3_hexbinAngles.map(function(angle) { + var x1 = Math.sin(angle) * radius, + y1 = -Math.cos(angle) * radius, + dx = x1 - x0, + dy = y1 - y0; + x0 = x1, y0 = y1; + return [dx, dy]; + }); + } + + hexbin.x = function(_) { + if (!arguments.length) return x; + x = _; + return hexbin; + }; + + hexbin.y = function(_) { + if (!arguments.length) return y; + y = _; + return hexbin; + }; + + hexbin.hexagon = function(radius) { + if (arguments.length < 1) radius = r; + return "m" + hexagon(radius).join("l") + "z"; + }; + + hexbin.centers = function() { + var centers = []; + for (var y = 0, odd = false, j = 0; y < height + r; y += dy, odd = !odd, ++j) { + for (var x = odd ? dx / 2 : 0, i = 0; x < width + dx / 2; x += dx, ++i) { + var center = [x, y]; + center.i = i; + center.j = j; + centers.push(center); + } + } + return centers; + }; + + hexbin.mesh = function() { + var fragment = hexagon(r).slice(0, 4).join("l"); + return hexbin.centers().map(function(p) { return "M" + p + "m" + fragment; }).join(""); + }; + + hexbin.size = function(_) { + if (!arguments.length) return [width, height]; + width = +_[0], height = +_[1]; + return hexbin; + }; + + hexbin.radius = function(_) { + if (!arguments.length) return r; + r = +_; + dx = r * 2 * Math.sin(Math.PI / 3); + dy = r * 1.5; + return hexbin; + }; + + return hexbin.radius(1); +}; + +var d3_hexbinAngles = d3.range(0, 2 * Math.PI, Math.PI / 3), + d3_hexbinX = function(d) { return d[0]; }, + d3_hexbinY = function(d) { return d[1]; }; + +})(); diff --git a/report/js/d3.v3.js b/report/js/d3.v3.js new file mode 100644 index 00000000..aded45c4 --- /dev/null +++ b/report/js/d3.v3.js @@ -0,0 +1,9554 @@ +!function() { + var d3 = { + version: "3.5.17" + }; + var d3_arraySlice = [].slice, d3_array = function(list) { + return d3_arraySlice.call(list); + }; + var d3_document = this.document; + function d3_documentElement(node) { + return node && (node.ownerDocument || node.document || node).documentElement; + } + function d3_window(node) { + return node && (node.ownerDocument && node.ownerDocument.defaultView || node.document && node || node.defaultView); + } + if (d3_document) { + try { + d3_array(d3_document.documentElement.childNodes)[0].nodeType; + } catch (e) { + d3_array = function(list) { + var i = list.length, array = new Array(i); + while (i--) array[i] = list[i]; + return array; + }; + } + } + if (!Date.now) Date.now = function() { + return +new Date(); + }; + if (d3_document) { + try { + d3_document.createElement("DIV").style.setProperty("opacity", 0, ""); + } catch (error) { + var d3_element_prototype = this.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = this.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty; + d3_element_prototype.setAttribute = function(name, value) { + d3_element_setAttribute.call(this, name, value + ""); + }; + d3_element_prototype.setAttributeNS = function(space, local, value) { + d3_element_setAttributeNS.call(this, space, local, value + ""); + }; + d3_style_prototype.setProperty = function(name, value, priority) { + d3_style_setProperty.call(this, name, value + "", priority); + }; + } + } + d3.ascending = d3_ascending; + function d3_ascending(a, b) { + return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; + } + d3.descending = function(a, b) { + return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN; + }; + d3.min = function(array, f) { + var i = -1, n = array.length, a, b; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = array[i]) != null && a > b) a = b; + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b; + } + return a; + }; + d3.max = function(array, f) { + var i = -1, n = array.length, a, b; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = array[i]) != null && b > a) a = b; + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b; + } + return a; + }; + d3.extent = function(array, f) { + var i = -1, n = array.length, a, b, c; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = c = b; + break; + } + while (++i < n) if ((b = array[i]) != null) { + if (a > b) a = b; + if (c < b) c = b; + } + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = c = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null) { + if (a > b) a = b; + if (c < b) c = b; + } + } + return [ a, c ]; + }; + function d3_number(x) { + return x === null ? NaN : +x; + } + function d3_numeric(x) { + return !isNaN(x); + } + d3.sum = function(array, f) { + var s = 0, n = array.length, a, i = -1; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = +array[i])) s += a; + } else { + while (++i < n) if (d3_numeric(a = +f.call(array, array[i], i))) s += a; + } + return s; + }; + d3.mean = function(array, f) { + var s = 0, n = array.length, a, i = -1, j = n; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = d3_number(array[i]))) s += a; else --j; + } else { + while (++i < n) if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) s += a; else --j; + } + if (j) return s / j; + }; + d3.quantile = function(values, p) { + var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h; + return e ? v + e * (values[h] - v) : v; + }; + d3.median = function(array, f) { + var numbers = [], n = array.length, a, i = -1; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = d3_number(array[i]))) numbers.push(a); + } else { + while (++i < n) if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) numbers.push(a); + } + if (numbers.length) return d3.quantile(numbers.sort(d3_ascending), .5); + }; + d3.variance = function(array, f) { + var n = array.length, m = 0, a, d, s = 0, i = -1, j = 0; + if (arguments.length === 1) { + while (++i < n) { + if (d3_numeric(a = d3_number(array[i]))) { + d = a - m; + m += d / ++j; + s += d * (a - m); + } + } + } else { + while (++i < n) { + if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) { + d = a - m; + m += d / ++j; + s += d * (a - m); + } + } + } + if (j > 1) return s / (j - 1); + }; + d3.deviation = function() { + var v = d3.variance.apply(this, arguments); + return v ? Math.sqrt(v) : v; + }; + function d3_bisector(compare) { + return { + left: function(a, x, lo, hi) { + if (arguments.length < 3) lo = 0; + if (arguments.length < 4) hi = a.length; + while (lo < hi) { + var mid = lo + hi >>> 1; + if (compare(a[mid], x) < 0) lo = mid + 1; else hi = mid; + } + return lo; + }, + right: function(a, x, lo, hi) { + if (arguments.length < 3) lo = 0; + if (arguments.length < 4) hi = a.length; + while (lo < hi) { + var mid = lo + hi >>> 1; + if (compare(a[mid], x) > 0) hi = mid; else lo = mid + 1; + } + return lo; + } + }; + } + var d3_bisect = d3_bisector(d3_ascending); + d3.bisectLeft = d3_bisect.left; + d3.bisect = d3.bisectRight = d3_bisect.right; + d3.bisector = function(f) { + return d3_bisector(f.length === 1 ? function(d, x) { + return d3_ascending(f(d), x); + } : f); + }; + d3.shuffle = function(array, i0, i1) { + if ((m = arguments.length) < 3) { + i1 = array.length; + if (m < 2) i0 = 0; + } + var m = i1 - i0, t, i; + while (m) { + i = Math.random() * m-- | 0; + t = array[m + i0], array[m + i0] = array[i + i0], array[i + i0] = t; + } + return array; + }; + d3.permute = function(array, indexes) { + var i = indexes.length, permutes = new Array(i); + while (i--) permutes[i] = array[indexes[i]]; + return permutes; + }; + d3.pairs = function(array) { + var i = 0, n = array.length - 1, p0, p1 = array[0], pairs = new Array(n < 0 ? 0 : n); + while (i < n) pairs[i] = [ p0 = p1, p1 = array[++i] ]; + return pairs; + }; + d3.transpose = function(matrix) { + if (!(n = matrix.length)) return []; + for (var i = -1, m = d3.min(matrix, d3_transposeLength), transpose = new Array(m); ++i < m; ) { + for (var j = -1, n, row = transpose[i] = new Array(n); ++j < n; ) { + row[j] = matrix[j][i]; + } + } + return transpose; + }; + function d3_transposeLength(d) { + return d.length; + } + d3.zip = function() { + return d3.transpose(arguments); + }; + d3.keys = function(map) { + var keys = []; + for (var key in map) keys.push(key); + return keys; + }; + d3.values = function(map) { + var values = []; + for (var key in map) values.push(map[key]); + return values; + }; + d3.entries = function(map) { + var entries = []; + for (var key in map) entries.push({ + key: key, + value: map[key] + }); + return entries; + }; + d3.merge = function(arrays) { + var n = arrays.length, m, i = -1, j = 0, merged, array; + while (++i < n) j += arrays[i].length; + merged = new Array(j); + while (--n >= 0) { + array = arrays[n]; + m = array.length; + while (--m >= 0) { + merged[--j] = array[m]; + } + } + return merged; + }; + var abs = Math.abs; + d3.range = function(start, stop, step) { + if (arguments.length < 3) { + step = 1; + if (arguments.length < 2) { + stop = start; + start = 0; + } + } + if ((stop - start) / step === Infinity) throw new Error("infinite range"); + var range = [], k = d3_range_integerScale(abs(step)), i = -1, j; + start *= k, stop *= k, step *= k; + if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k); + return range; + }; + function d3_range_integerScale(x) { + var k = 1; + while (x * k % 1) k *= 10; + return k; + } + function d3_class(ctor, properties) { + for (var key in properties) { + Object.defineProperty(ctor.prototype, key, { + value: properties[key], + enumerable: false + }); + } + } + d3.map = function(object, f) { + var map = new d3_Map(); + if (object instanceof d3_Map) { + object.forEach(function(key, value) { + map.set(key, value); + }); + } else if (Array.isArray(object)) { + var i = -1, n = object.length, o; + if (arguments.length === 1) while (++i < n) map.set(i, object[i]); else while (++i < n) map.set(f.call(object, o = object[i], i), o); + } else { + for (var key in object) map.set(key, object[key]); + } + return map; + }; + function d3_Map() { + this._ = Object.create(null); + } + var d3_map_proto = "__proto__", d3_map_zero = "\x00"; + d3_class(d3_Map, { + has: d3_map_has, + get: function(key) { + return this._[d3_map_escape(key)]; + }, + set: function(key, value) { + return this._[d3_map_escape(key)] = value; + }, + remove: d3_map_remove, + keys: d3_map_keys, + values: function() { + var values = []; + for (var key in this._) values.push(this._[key]); + return values; + }, + entries: function() { + var entries = []; + for (var key in this._) entries.push({ + key: d3_map_unescape(key), + value: this._[key] + }); + return entries; + }, + size: d3_map_size, + empty: d3_map_empty, + forEach: function(f) { + for (var key in this._) f.call(this, d3_map_unescape(key), this._[key]); + } + }); + function d3_map_escape(key) { + return (key += "") === d3_map_proto || key[0] === d3_map_zero ? d3_map_zero + key : key; + } + function d3_map_unescape(key) { + return (key += "")[0] === d3_map_zero ? key.slice(1) : key; + } + function d3_map_has(key) { + return d3_map_escape(key) in this._; + } + function d3_map_remove(key) { + return (key = d3_map_escape(key)) in this._ && delete this._[key]; + } + function d3_map_keys() { + var keys = []; + for (var key in this._) keys.push(d3_map_unescape(key)); + return keys; + } + function d3_map_size() { + var size = 0; + for (var key in this._) ++size; + return size; + } + function d3_map_empty() { + for (var key in this._) return false; + return true; + } + d3.nest = function() { + var nest = {}, keys = [], sortKeys = [], sortValues, rollup; + function map(mapType, array, depth) { + if (depth >= keys.length) return rollup ? rollup.call(nest, array) : sortValues ? array.sort(sortValues) : array; + var i = -1, n = array.length, key = keys[depth++], keyValue, object, setter, valuesByKey = new d3_Map(), values; + while (++i < n) { + if (values = valuesByKey.get(keyValue = key(object = array[i]))) { + values.push(object); + } else { + valuesByKey.set(keyValue, [ object ]); + } + } + if (mapType) { + object = mapType(); + setter = function(keyValue, values) { + object.set(keyValue, map(mapType, values, depth)); + }; + } else { + object = {}; + setter = function(keyValue, values) { + object[keyValue] = map(mapType, values, depth); + }; + } + valuesByKey.forEach(setter); + return object; + } + function entries(map, depth) { + if (depth >= keys.length) return map; + var array = [], sortKey = sortKeys[depth++]; + map.forEach(function(key, keyMap) { + array.push({ + key: key, + values: entries(keyMap, depth) + }); + }); + return sortKey ? array.sort(function(a, b) { + return sortKey(a.key, b.key); + }) : array; + } + nest.map = function(array, mapType) { + return map(mapType, array, 0); + }; + nest.entries = function(array) { + return entries(map(d3.map, array, 0), 0); + }; + nest.key = function(d) { + keys.push(d); + return nest; + }; + nest.sortKeys = function(order) { + sortKeys[keys.length - 1] = order; + return nest; + }; + nest.sortValues = function(order) { + sortValues = order; + return nest; + }; + nest.rollup = function(f) { + rollup = f; + return nest; + }; + return nest; + }; + d3.set = function(array) { + var set = new d3_Set(); + if (array) for (var i = 0, n = array.length; i < n; ++i) set.add(array[i]); + return set; + }; + function d3_Set() { + this._ = Object.create(null); + } + d3_class(d3_Set, { + has: d3_map_has, + add: function(key) { + this._[d3_map_escape(key += "")] = true; + return key; + }, + remove: d3_map_remove, + values: d3_map_keys, + size: d3_map_size, + empty: d3_map_empty, + forEach: function(f) { + for (var key in this._) f.call(this, d3_map_unescape(key)); + } + }); + d3.behavior = {}; + function d3_identity(d) { + return d; + } + d3.rebind = function(target, source) { + var i = 1, n = arguments.length, method; + while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]); + return target; + }; + function d3_rebind(target, source, method) { + return function() { + var value = method.apply(source, arguments); + return value === source ? target : value; + }; + } + function d3_vendorSymbol(object, name) { + if (name in object) return name; + name = name.charAt(0).toUpperCase() + name.slice(1); + for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) { + var prefixName = d3_vendorPrefixes[i] + name; + if (prefixName in object) return prefixName; + } + } + var d3_vendorPrefixes = [ "webkit", "ms", "moz", "Moz", "o", "O" ]; + function d3_noop() {} + d3.dispatch = function() { + var dispatch = new d3_dispatch(), i = -1, n = arguments.length; + while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); + return dispatch; + }; + function d3_dispatch() {} + d3_dispatch.prototype.on = function(type, listener) { + var i = type.indexOf("."), name = ""; + if (i >= 0) { + name = type.slice(i + 1); + type = type.slice(0, i); + } + if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener); + if (arguments.length === 2) { + if (listener == null) for (type in this) { + if (this.hasOwnProperty(type)) this[type].on(name, null); + } + return this; + } + }; + function d3_dispatch_event(dispatch) { + var listeners = [], listenerByName = new d3_Map(); + function event() { + var z = listeners, i = -1, n = z.length, l; + while (++i < n) if (l = z[i].on) l.apply(this, arguments); + return dispatch; + } + event.on = function(name, listener) { + var l = listenerByName.get(name), i; + if (arguments.length < 2) return l && l.on; + if (l) { + l.on = null; + listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1)); + listenerByName.remove(name); + } + if (listener) listeners.push(listenerByName.set(name, { + on: listener + })); + return dispatch; + }; + return event; + } + d3.event = null; + function d3_eventPreventDefault() { + d3.event.preventDefault(); + } + function d3_eventSource() { + var e = d3.event, s; + while (s = e.sourceEvent) e = s; + return e; + } + function d3_eventDispatch(target) { + var dispatch = new d3_dispatch(), i = 0, n = arguments.length; + while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); + dispatch.of = function(thiz, argumentz) { + return function(e1) { + try { + var e0 = e1.sourceEvent = d3.event; + e1.target = target; + d3.event = e1; + dispatch[e1.type].apply(thiz, argumentz); + } finally { + d3.event = e0; + } + }; + }; + return dispatch; + } + d3.requote = function(s) { + return s.replace(d3_requote_re, "\\$&"); + }; + var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; + var d3_subclass = {}.__proto__ ? function(object, prototype) { + object.__proto__ = prototype; + } : function(object, prototype) { + for (var property in prototype) object[property] = prototype[property]; + }; + function d3_selection(groups) { + d3_subclass(groups, d3_selectionPrototype); + return groups; + } + var d3_select = function(s, n) { + return n.querySelector(s); + }, d3_selectAll = function(s, n) { + return n.querySelectorAll(s); + }, d3_selectMatches = function(n, s) { + var d3_selectMatcher = n.matches || n[d3_vendorSymbol(n, "matchesSelector")]; + d3_selectMatches = function(n, s) { + return d3_selectMatcher.call(n, s); + }; + return d3_selectMatches(n, s); + }; + if (typeof Sizzle === "function") { + d3_select = function(s, n) { + return Sizzle(s, n)[0] || null; + }; + d3_selectAll = Sizzle; + d3_selectMatches = Sizzle.matchesSelector; + } + d3.selection = function() { + return d3.select(d3_document.documentElement); + }; + var d3_selectionPrototype = d3.selection.prototype = []; + d3_selectionPrototype.select = function(selector) { + var subgroups = [], subgroup, subnode, group, node; + selector = d3_selection_selector(selector); + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + subgroup.parentNode = (group = this[j]).parentNode; + for (var i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroup.push(subnode = selector.call(node, node.__data__, i, j)); + if (subnode && "__data__" in node) subnode.__data__ = node.__data__; + } else { + subgroup.push(null); + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_selector(selector) { + return typeof selector === "function" ? selector : function() { + return d3_select(selector, this); + }; + } + d3_selectionPrototype.selectAll = function(selector) { + var subgroups = [], subgroup, node; + selector = d3_selection_selectorAll(selector); + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j))); + subgroup.parentNode = node; + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_selectorAll(selector) { + return typeof selector === "function" ? selector : function() { + return d3_selectAll(selector, this); + }; + } + var d3_nsXhtml = "http://www.w3.org/1999/xhtml"; + var d3_nsPrefix = { + svg: "http://www.w3.org/2000/svg", + xhtml: d3_nsXhtml, + xlink: "http://www.w3.org/1999/xlink", + xml: "http://www.w3.org/XML/1998/namespace", + xmlns: "http://www.w3.org/2000/xmlns/" + }; + d3.ns = { + prefix: d3_nsPrefix, + qualify: function(name) { + var i = name.indexOf(":"), prefix = name; + if (i >= 0 && (prefix = name.slice(0, i)) !== "xmlns") name = name.slice(i + 1); + return d3_nsPrefix.hasOwnProperty(prefix) ? { + space: d3_nsPrefix[prefix], + local: name + } : name; + } + }; + d3_selectionPrototype.attr = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") { + var node = this.node(); + name = d3.ns.qualify(name); + return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name); + } + for (value in name) this.each(d3_selection_attr(value, name[value])); + return this; + } + return this.each(d3_selection_attr(name, value)); + }; + function d3_selection_attr(name, value) { + name = d3.ns.qualify(name); + function attrNull() { + this.removeAttribute(name); + } + function attrNullNS() { + this.removeAttributeNS(name.space, name.local); + } + function attrConstant() { + this.setAttribute(name, value); + } + function attrConstantNS() { + this.setAttributeNS(name.space, name.local, value); + } + function attrFunction() { + var x = value.apply(this, arguments); + if (x == null) this.removeAttribute(name); else this.setAttribute(name, x); + } + function attrFunctionNS() { + var x = value.apply(this, arguments); + if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x); + } + return value == null ? name.local ? attrNullNS : attrNull : typeof value === "function" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant; + } + function d3_collapse(s) { + return s.trim().replace(/\s+/g, " "); + } + d3_selectionPrototype.classed = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") { + var node = this.node(), n = (name = d3_selection_classes(name)).length, i = -1; + if (value = node.classList) { + while (++i < n) if (!value.contains(name[i])) return false; + } else { + value = node.getAttribute("class"); + while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false; + } + return true; + } + for (value in name) this.each(d3_selection_classed(value, name[value])); + return this; + } + return this.each(d3_selection_classed(name, value)); + }; + function d3_selection_classedRe(name) { + return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g"); + } + function d3_selection_classes(name) { + return (name + "").trim().split(/^|\s+/); + } + function d3_selection_classed(name, value) { + name = d3_selection_classes(name).map(d3_selection_classedName); + var n = name.length; + function classedConstant() { + var i = -1; + while (++i < n) name[i](this, value); + } + function classedFunction() { + var i = -1, x = value.apply(this, arguments); + while (++i < n) name[i](this, x); + } + return typeof value === "function" ? classedFunction : classedConstant; + } + function d3_selection_classedName(name) { + var re = d3_selection_classedRe(name); + return function(node, value) { + if (c = node.classList) return value ? c.add(name) : c.remove(name); + var c = node.getAttribute("class") || ""; + if (value) { + re.lastIndex = 0; + if (!re.test(c)) node.setAttribute("class", d3_collapse(c + " " + name)); + } else { + node.setAttribute("class", d3_collapse(c.replace(re, " "))); + } + }; + } + d3_selectionPrototype.style = function(name, value, priority) { + var n = arguments.length; + if (n < 3) { + if (typeof name !== "string") { + if (n < 2) value = ""; + for (priority in name) this.each(d3_selection_style(priority, name[priority], value)); + return this; + } + if (n < 2) { + var node = this.node(); + return d3_window(node).getComputedStyle(node, null).getPropertyValue(name); + } + priority = ""; + } + return this.each(d3_selection_style(name, value, priority)); + }; + function d3_selection_style(name, value, priority) { + function styleNull() { + this.style.removeProperty(name); + } + function styleConstant() { + this.style.setProperty(name, value, priority); + } + function styleFunction() { + var x = value.apply(this, arguments); + if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority); + } + return value == null ? styleNull : typeof value === "function" ? styleFunction : styleConstant; + } + d3_selectionPrototype.property = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") return this.node()[name]; + for (value in name) this.each(d3_selection_property(value, name[value])); + return this; + } + return this.each(d3_selection_property(name, value)); + }; + function d3_selection_property(name, value) { + function propertyNull() { + delete this[name]; + } + function propertyConstant() { + this[name] = value; + } + function propertyFunction() { + var x = value.apply(this, arguments); + if (x == null) delete this[name]; else this[name] = x; + } + return value == null ? propertyNull : typeof value === "function" ? propertyFunction : propertyConstant; + } + d3_selectionPrototype.text = function(value) { + return arguments.length ? this.each(typeof value === "function" ? function() { + var v = value.apply(this, arguments); + this.textContent = v == null ? "" : v; + } : value == null ? function() { + this.textContent = ""; + } : function() { + this.textContent = value; + }) : this.node().textContent; + }; + d3_selectionPrototype.html = function(value) { + return arguments.length ? this.each(typeof value === "function" ? function() { + var v = value.apply(this, arguments); + this.innerHTML = v == null ? "" : v; + } : value == null ? function() { + this.innerHTML = ""; + } : function() { + this.innerHTML = value; + }) : this.node().innerHTML; + }; + d3_selectionPrototype.append = function(name) { + name = d3_selection_creator(name); + return this.select(function() { + return this.appendChild(name.apply(this, arguments)); + }); + }; + function d3_selection_creator(name) { + function create() { + var document = this.ownerDocument, namespace = this.namespaceURI; + return namespace === d3_nsXhtml && document.documentElement.namespaceURI === d3_nsXhtml ? document.createElement(name) : document.createElementNS(namespace, name); + } + function createNS() { + return this.ownerDocument.createElementNS(name.space, name.local); + } + return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? createNS : create; + } + d3_selectionPrototype.insert = function(name, before) { + name = d3_selection_creator(name); + before = d3_selection_selector(before); + return this.select(function() { + return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null); + }); + }; + d3_selectionPrototype.remove = function() { + return this.each(d3_selectionRemove); + }; + function d3_selectionRemove() { + var parent = this.parentNode; + if (parent) parent.removeChild(this); + } + d3_selectionPrototype.data = function(value, key) { + var i = -1, n = this.length, group, node; + if (!arguments.length) { + value = new Array(n = (group = this[0]).length); + while (++i < n) { + if (node = group[i]) { + value[i] = node.__data__; + } + } + return value; + } + function bind(group, groupData) { + var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData; + if (key) { + var nodeByKeyValue = new d3_Map(), keyValues = new Array(n), keyValue; + for (i = -1; ++i < n; ) { + if (node = group[i]) { + if (nodeByKeyValue.has(keyValue = key.call(node, node.__data__, i))) { + exitNodes[i] = node; + } else { + nodeByKeyValue.set(keyValue, node); + } + keyValues[i] = keyValue; + } + } + for (i = -1; ++i < m; ) { + if (!(node = nodeByKeyValue.get(keyValue = key.call(groupData, nodeData = groupData[i], i)))) { + enterNodes[i] = d3_selection_dataNode(nodeData); + } else if (node !== true) { + updateNodes[i] = node; + node.__data__ = nodeData; + } + nodeByKeyValue.set(keyValue, true); + } + for (i = -1; ++i < n; ) { + if (i in keyValues && nodeByKeyValue.get(keyValues[i]) !== true) { + exitNodes[i] = group[i]; + } + } + } else { + for (i = -1; ++i < n0; ) { + node = group[i]; + nodeData = groupData[i]; + if (node) { + node.__data__ = nodeData; + updateNodes[i] = node; + } else { + enterNodes[i] = d3_selection_dataNode(nodeData); + } + } + for (;i < m; ++i) { + enterNodes[i] = d3_selection_dataNode(groupData[i]); + } + for (;i < n; ++i) { + exitNodes[i] = group[i]; + } + } + enterNodes.update = updateNodes; + enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode; + enter.push(enterNodes); + update.push(updateNodes); + exit.push(exitNodes); + } + var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]); + if (typeof value === "function") { + while (++i < n) { + bind(group = this[i], value.call(group, group.parentNode.__data__, i)); + } + } else { + while (++i < n) { + bind(group = this[i], value); + } + } + update.enter = function() { + return enter; + }; + update.exit = function() { + return exit; + }; + return update; + }; + function d3_selection_dataNode(data) { + return { + __data__: data + }; + } + d3_selectionPrototype.datum = function(value) { + return arguments.length ? this.property("__data__", value) : this.property("__data__"); + }; + d3_selectionPrototype.filter = function(filter) { + var subgroups = [], subgroup, group, node; + if (typeof filter !== "function") filter = d3_selection_filter(filter); + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + subgroup.parentNode = (group = this[j]).parentNode; + for (var i = 0, n = group.length; i < n; i++) { + if ((node = group[i]) && filter.call(node, node.__data__, i, j)) { + subgroup.push(node); + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_filter(selector) { + return function() { + return d3_selectMatches(this, selector); + }; + } + d3_selectionPrototype.order = function() { + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0; ) { + if (node = group[i]) { + if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next); + next = node; + } + } + } + return this; + }; + d3_selectionPrototype.sort = function(comparator) { + comparator = d3_selection_sortComparator.apply(this, arguments); + for (var j = -1, m = this.length; ++j < m; ) this[j].sort(comparator); + return this.order(); + }; + function d3_selection_sortComparator(comparator) { + if (!arguments.length) comparator = d3_ascending; + return function(a, b) { + return a && b ? comparator(a.__data__, b.__data__) : !a - !b; + }; + } + d3_selectionPrototype.each = function(callback) { + return d3_selection_each(this, function(node, i, j) { + callback.call(node, node.__data__, i, j); + }); + }; + function d3_selection_each(groups, callback) { + for (var j = 0, m = groups.length; j < m; j++) { + for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) { + if (node = group[i]) callback(node, i, j); + } + } + return groups; + } + d3_selectionPrototype.call = function(callback) { + var args = d3_array(arguments); + callback.apply(args[0] = this, args); + return this; + }; + d3_selectionPrototype.empty = function() { + return !this.node(); + }; + d3_selectionPrototype.node = function() { + for (var j = 0, m = this.length; j < m; j++) { + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + var node = group[i]; + if (node) return node; + } + } + return null; + }; + d3_selectionPrototype.size = function() { + var n = 0; + d3_selection_each(this, function() { + ++n; + }); + return n; + }; + function d3_selection_enter(selection) { + d3_subclass(selection, d3_selection_enterPrototype); + return selection; + } + var d3_selection_enterPrototype = []; + d3.selection.enter = d3_selection_enter; + d3.selection.enter.prototype = d3_selection_enterPrototype; + d3_selection_enterPrototype.append = d3_selectionPrototype.append; + d3_selection_enterPrototype.empty = d3_selectionPrototype.empty; + d3_selection_enterPrototype.node = d3_selectionPrototype.node; + d3_selection_enterPrototype.call = d3_selectionPrototype.call; + d3_selection_enterPrototype.size = d3_selectionPrototype.size; + d3_selection_enterPrototype.select = function(selector) { + var subgroups = [], subgroup, subnode, upgroup, group, node; + for (var j = -1, m = this.length; ++j < m; ) { + upgroup = (group = this[j]).update; + subgroups.push(subgroup = []); + subgroup.parentNode = group.parentNode; + for (var i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j)); + subnode.__data__ = node.__data__; + } else { + subgroup.push(null); + } + } + } + return d3_selection(subgroups); + }; + d3_selection_enterPrototype.insert = function(name, before) { + if (arguments.length < 2) before = d3_selection_enterInsertBefore(this); + return d3_selectionPrototype.insert.call(this, name, before); + }; + function d3_selection_enterInsertBefore(enter) { + var i0, j0; + return function(d, i, j) { + var group = enter[j].update, n = group.length, node; + if (j != j0) j0 = j, i0 = 0; + if (i >= i0) i0 = i + 1; + while (!(node = group[i0]) && ++i0 < n) ; + return node; + }; + } + d3.select = function(node) { + var group; + if (typeof node === "string") { + group = [ d3_select(node, d3_document) ]; + group.parentNode = d3_document.documentElement; + } else { + group = [ node ]; + group.parentNode = d3_documentElement(node); + } + return d3_selection([ group ]); + }; + d3.selectAll = function(nodes) { + var group; + if (typeof nodes === "string") { + group = d3_array(d3_selectAll(nodes, d3_document)); + group.parentNode = d3_document.documentElement; + } else { + group = d3_array(nodes); + group.parentNode = null; + } + return d3_selection([ group ]); + }; + d3_selectionPrototype.on = function(type, listener, capture) { + var n = arguments.length; + if (n < 3) { + if (typeof type !== "string") { + if (n < 2) listener = false; + for (capture in type) this.each(d3_selection_on(capture, type[capture], listener)); + return this; + } + if (n < 2) return (n = this.node()["__on" + type]) && n._; + capture = false; + } + return this.each(d3_selection_on(type, listener, capture)); + }; + function d3_selection_on(type, listener, capture) { + var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener; + if (i > 0) type = type.slice(0, i); + var filter = d3_selection_onFilters.get(type); + if (filter) type = filter, wrap = d3_selection_onFilter; + function onRemove() { + var l = this[name]; + if (l) { + this.removeEventListener(type, l, l.$); + delete this[name]; + } + } + function onAdd() { + var l = wrap(listener, d3_array(arguments)); + onRemove.call(this); + this.addEventListener(type, this[name] = l, l.$ = capture); + l._ = listener; + } + function removeAll() { + var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"), match; + for (var name in this) { + if (match = name.match(re)) { + var l = this[name]; + this.removeEventListener(match[1], l, l.$); + delete this[name]; + } + } + } + return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll; + } + var d3_selection_onFilters = d3.map({ + mouseenter: "mouseover", + mouseleave: "mouseout" + }); + if (d3_document) { + d3_selection_onFilters.forEach(function(k) { + if ("on" + k in d3_document) d3_selection_onFilters.remove(k); + }); + } + function d3_selection_onListener(listener, argumentz) { + return function(e) { + var o = d3.event; + d3.event = e; + argumentz[0] = this.__data__; + try { + listener.apply(this, argumentz); + } finally { + d3.event = o; + } + }; + } + function d3_selection_onFilter(listener, argumentz) { + var l = d3_selection_onListener(listener, argumentz); + return function(e) { + var target = this, related = e.relatedTarget; + if (!related || related !== target && !(related.compareDocumentPosition(target) & 8)) { + l.call(target, e); + } + }; + } + var d3_event_dragSelect, d3_event_dragId = 0; + function d3_event_dragSuppress(node) { + var name = ".dragsuppress-" + ++d3_event_dragId, click = "click" + name, w = d3.select(d3_window(node)).on("touchmove" + name, d3_eventPreventDefault).on("dragstart" + name, d3_eventPreventDefault).on("selectstart" + name, d3_eventPreventDefault); + if (d3_event_dragSelect == null) { + d3_event_dragSelect = "onselectstart" in node ? false : d3_vendorSymbol(node.style, "userSelect"); + } + if (d3_event_dragSelect) { + var style = d3_documentElement(node).style, select = style[d3_event_dragSelect]; + style[d3_event_dragSelect] = "none"; + } + return function(suppressClick) { + w.on(name, null); + if (d3_event_dragSelect) style[d3_event_dragSelect] = select; + if (suppressClick) { + var off = function() { + w.on(click, null); + }; + w.on(click, function() { + d3_eventPreventDefault(); + off(); + }, true); + setTimeout(off, 0); + } + }; + } + d3.mouse = function(container) { + return d3_mousePoint(container, d3_eventSource()); + }; + var d3_mouse_bug44083 = this.navigator && /WebKit/.test(this.navigator.userAgent) ? -1 : 0; + function d3_mousePoint(container, e) { + if (e.changedTouches) e = e.changedTouches[0]; + var svg = container.ownerSVGElement || container; + if (svg.createSVGPoint) { + var point = svg.createSVGPoint(); + if (d3_mouse_bug44083 < 0) { + var window = d3_window(container); + if (window.scrollX || window.scrollY) { + svg = d3.select("body").append("svg").style({ + position: "absolute", + top: 0, + left: 0, + margin: 0, + padding: 0, + border: "none" + }, "important"); + var ctm = svg[0][0].getScreenCTM(); + d3_mouse_bug44083 = !(ctm.f || ctm.e); + svg.remove(); + } + } + if (d3_mouse_bug44083) point.x = e.pageX, point.y = e.pageY; else point.x = e.clientX, + point.y = e.clientY; + point = point.matrixTransform(container.getScreenCTM().inverse()); + return [ point.x, point.y ]; + } + var rect = container.getBoundingClientRect(); + return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ]; + } + d3.touch = function(container, touches, identifier) { + if (arguments.length < 3) identifier = touches, touches = d3_eventSource().changedTouches; + if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) { + if ((touch = touches[i]).identifier === identifier) { + return d3_mousePoint(container, touch); + } + } + }; + d3.behavior.drag = function() { + var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, d3_window, "mousemove", "mouseup"), touchstart = dragstart(d3_behavior_dragTouchId, d3.touch, d3_identity, "touchmove", "touchend"); + function drag() { + this.on("mousedown.drag", mousedown).on("touchstart.drag", touchstart); + } + function dragstart(id, position, subject, move, end) { + return function() { + var that = this, target = d3.event.target.correspondingElement || d3.event.target, parent = that.parentNode, dispatch = event.of(that, arguments), dragged = 0, dragId = id(), dragName = ".drag" + (dragId == null ? "" : "-" + dragId), dragOffset, dragSubject = d3.select(subject(target)).on(move + dragName, moved).on(end + dragName, ended), dragRestore = d3_event_dragSuppress(target), position0 = position(parent, dragId); + if (origin) { + dragOffset = origin.apply(that, arguments); + dragOffset = [ dragOffset.x - position0[0], dragOffset.y - position0[1] ]; + } else { + dragOffset = [ 0, 0 ]; + } + dispatch({ + type: "dragstart" + }); + function moved() { + var position1 = position(parent, dragId), dx, dy; + if (!position1) return; + dx = position1[0] - position0[0]; + dy = position1[1] - position0[1]; + dragged |= dx | dy; + position0 = position1; + dispatch({ + type: "drag", + x: position1[0] + dragOffset[0], + y: position1[1] + dragOffset[1], + dx: dx, + dy: dy + }); + } + function ended() { + if (!position(parent, dragId)) return; + dragSubject.on(move + dragName, null).on(end + dragName, null); + dragRestore(dragged); + dispatch({ + type: "dragend" + }); + } + }; + } + drag.origin = function(x) { + if (!arguments.length) return origin; + origin = x; + return drag; + }; + return d3.rebind(drag, event, "on"); + }; + function d3_behavior_dragTouchId() { + return d3.event.changedTouches[0].identifier; + } + d3.touches = function(container, touches) { + if (arguments.length < 2) touches = d3_eventSource().touches; + return touches ? d3_array(touches).map(function(touch) { + var point = d3_mousePoint(container, touch); + point.identifier = touch.identifier; + return point; + }) : []; + }; + var ε = 1e-6, ε2 = ε * ε, π = Math.PI, τ = 2 * π, τε = τ - ε, halfπ = π / 2, d3_radians = π / 180, d3_degrees = 180 / π; + function d3_sgn(x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; + } + function d3_cross2d(a, b, c) { + return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); + } + function d3_acos(x) { + return x > 1 ? 0 : x < -1 ? π : Math.acos(x); + } + function d3_asin(x) { + return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x); + } + function d3_sinh(x) { + return ((x = Math.exp(x)) - 1 / x) / 2; + } + function d3_cosh(x) { + return ((x = Math.exp(x)) + 1 / x) / 2; + } + function d3_tanh(x) { + return ((x = Math.exp(2 * x)) - 1) / (x + 1); + } + function d3_haversin(x) { + return (x = Math.sin(x / 2)) * x; + } + var ρ = Math.SQRT2, ρ2 = 2, ρ4 = 4; + d3.interpolateZoom = function(p0, p1) { + var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], ux1 = p1[0], uy1 = p1[1], w1 = p1[2], dx = ux1 - ux0, dy = uy1 - uy0, d2 = dx * dx + dy * dy, i, S; + if (d2 < ε2) { + S = Math.log(w1 / w0) / ρ; + i = function(t) { + return [ ux0 + t * dx, uy0 + t * dy, w0 * Math.exp(ρ * t * S) ]; + }; + } else { + var d1 = Math.sqrt(d2), b0 = (w1 * w1 - w0 * w0 + ρ4 * d2) / (2 * w0 * ρ2 * d1), b1 = (w1 * w1 - w0 * w0 - ρ4 * d2) / (2 * w1 * ρ2 * d1), r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1); + S = (r1 - r0) / ρ; + i = function(t) { + var s = t * S, coshr0 = d3_cosh(r0), u = w0 / (ρ2 * d1) * (coshr0 * d3_tanh(ρ * s + r0) - d3_sinh(r0)); + return [ ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / d3_cosh(ρ * s + r0) ]; + }; + } + i.duration = S * 1e3; + return i; + }; + d3.behavior.zoom = function() { + var view = { + x: 0, + y: 0, + k: 1 + }, translate0, center0, center, size = [ 960, 500 ], scaleExtent = d3_behavior_zoomInfinity, duration = 250, zooming = 0, mousedown = "mousedown.zoom", mousemove = "mousemove.zoom", mouseup = "mouseup.zoom", mousewheelTimer, touchstart = "touchstart.zoom", touchtime, event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), x0, x1, y0, y1; + if (!d3_behavior_zoomWheel) { + d3_behavior_zoomWheel = "onwheel" in d3_document ? (d3_behavior_zoomDelta = function() { + return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1); + }, "wheel") : "onmousewheel" in d3_document ? (d3_behavior_zoomDelta = function() { + return d3.event.wheelDelta; + }, "mousewheel") : (d3_behavior_zoomDelta = function() { + return -d3.event.detail; + }, "MozMousePixelScroll"); + } + function zoom(g) { + g.on(mousedown, mousedowned).on(d3_behavior_zoomWheel + ".zoom", mousewheeled).on("dblclick.zoom", dblclicked).on(touchstart, touchstarted); + } + zoom.event = function(g) { + g.each(function() { + var dispatch = event.of(this, arguments), view1 = view; + if (d3_transitionInheritId) { + d3.select(this).transition().each("start.zoom", function() { + view = this.__chart__ || { + x: 0, + y: 0, + k: 1 + }; + zoomstarted(dispatch); + }).tween("zoom:zoom", function() { + var dx = size[0], dy = size[1], cx = center0 ? center0[0] : dx / 2, cy = center0 ? center0[1] : dy / 2, i = d3.interpolateZoom([ (cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k ], [ (cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k ]); + return function(t) { + var l = i(t), k = dx / l[2]; + this.__chart__ = view = { + x: cx - l[0] * k, + y: cy - l[1] * k, + k: k + }; + zoomed(dispatch); + }; + }).each("interrupt.zoom", function() { + zoomended(dispatch); + }).each("end.zoom", function() { + zoomended(dispatch); + }); + } else { + this.__chart__ = view; + zoomstarted(dispatch); + zoomed(dispatch); + zoomended(dispatch); + } + }); + }; + zoom.translate = function(_) { + if (!arguments.length) return [ view.x, view.y ]; + view = { + x: +_[0], + y: +_[1], + k: view.k + }; + rescale(); + return zoom; + }; + zoom.scale = function(_) { + if (!arguments.length) return view.k; + view = { + x: view.x, + y: view.y, + k: null + }; + scaleTo(+_); + rescale(); + return zoom; + }; + zoom.scaleExtent = function(_) { + if (!arguments.length) return scaleExtent; + scaleExtent = _ == null ? d3_behavior_zoomInfinity : [ +_[0], +_[1] ]; + return zoom; + }; + zoom.center = function(_) { + if (!arguments.length) return center; + center = _ && [ +_[0], +_[1] ]; + return zoom; + }; + zoom.size = function(_) { + if (!arguments.length) return size; + size = _ && [ +_[0], +_[1] ]; + return zoom; + }; + zoom.duration = function(_) { + if (!arguments.length) return duration; + duration = +_; + return zoom; + }; + zoom.x = function(z) { + if (!arguments.length) return x1; + x1 = z; + x0 = z.copy(); + view = { + x: 0, + y: 0, + k: 1 + }; + return zoom; + }; + zoom.y = function(z) { + if (!arguments.length) return y1; + y1 = z; + y0 = z.copy(); + view = { + x: 0, + y: 0, + k: 1 + }; + return zoom; + }; + function location(p) { + return [ (p[0] - view.x) / view.k, (p[1] - view.y) / view.k ]; + } + function point(l) { + return [ l[0] * view.k + view.x, l[1] * view.k + view.y ]; + } + function scaleTo(s) { + view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s)); + } + function translateTo(p, l) { + l = point(l); + view.x += p[0] - l[0]; + view.y += p[1] - l[1]; + } + function zoomTo(that, p, l, k) { + that.__chart__ = { + x: view.x, + y: view.y, + k: view.k + }; + scaleTo(Math.pow(2, k)); + translateTo(center0 = p, l); + that = d3.select(that); + if (duration > 0) that = that.transition().duration(duration); + that.call(zoom.event); + } + function rescale() { + if (x1) x1.domain(x0.range().map(function(x) { + return (x - view.x) / view.k; + }).map(x0.invert)); + if (y1) y1.domain(y0.range().map(function(y) { + return (y - view.y) / view.k; + }).map(y0.invert)); + } + function zoomstarted(dispatch) { + if (!zooming++) dispatch({ + type: "zoomstart" + }); + } + function zoomed(dispatch) { + rescale(); + dispatch({ + type: "zoom", + scale: view.k, + translate: [ view.x, view.y ] + }); + } + function zoomended(dispatch) { + if (!--zooming) dispatch({ + type: "zoomend" + }), center0 = null; + } + function mousedowned() { + var that = this, dispatch = event.of(that, arguments), dragged = 0, subject = d3.select(d3_window(that)).on(mousemove, moved).on(mouseup, ended), location0 = location(d3.mouse(that)), dragRestore = d3_event_dragSuppress(that); + d3_selection_interrupt.call(that); + zoomstarted(dispatch); + function moved() { + dragged = 1; + translateTo(d3.mouse(that), location0); + zoomed(dispatch); + } + function ended() { + subject.on(mousemove, null).on(mouseup, null); + dragRestore(dragged); + zoomended(dispatch); + } + } + function touchstarted() { + var that = this, dispatch = event.of(that, arguments), locations0 = {}, distance0 = 0, scale0, zoomName = ".zoom-" + d3.event.changedTouches[0].identifier, touchmove = "touchmove" + zoomName, touchend = "touchend" + zoomName, targets = [], subject = d3.select(that), dragRestore = d3_event_dragSuppress(that); + started(); + zoomstarted(dispatch); + subject.on(mousedown, null).on(touchstart, started); + function relocate() { + var touches = d3.touches(that); + scale0 = view.k; + touches.forEach(function(t) { + if (t.identifier in locations0) locations0[t.identifier] = location(t); + }); + return touches; + } + function started() { + var target = d3.event.target; + d3.select(target).on(touchmove, moved).on(touchend, ended); + targets.push(target); + var changed = d3.event.changedTouches; + for (var i = 0, n = changed.length; i < n; ++i) { + locations0[changed[i].identifier] = null; + } + var touches = relocate(), now = Date.now(); + if (touches.length === 1) { + if (now - touchtime < 500) { + var p = touches[0]; + zoomTo(that, p, locations0[p.identifier], Math.floor(Math.log(view.k) / Math.LN2) + 1); + d3_eventPreventDefault(); + } + touchtime = now; + } else if (touches.length > 1) { + var p = touches[0], q = touches[1], dx = p[0] - q[0], dy = p[1] - q[1]; + distance0 = dx * dx + dy * dy; + } + } + function moved() { + var touches = d3.touches(that), p0, l0, p1, l1; + d3_selection_interrupt.call(that); + for (var i = 0, n = touches.length; i < n; ++i, l1 = null) { + p1 = touches[i]; + if (l1 = locations0[p1.identifier]) { + if (l0) break; + p0 = p1, l0 = l1; + } + } + if (l1) { + var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1, scale1 = distance0 && Math.sqrt(distance1 / distance0); + p0 = [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ]; + l0 = [ (l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2 ]; + scaleTo(scale1 * scale0); + } + touchtime = null; + translateTo(p0, l0); + zoomed(dispatch); + } + function ended() { + if (d3.event.touches.length) { + var changed = d3.event.changedTouches; + for (var i = 0, n = changed.length; i < n; ++i) { + delete locations0[changed[i].identifier]; + } + for (var identifier in locations0) { + return void relocate(); + } + } + d3.selectAll(targets).on(zoomName, null); + subject.on(mousedown, mousedowned).on(touchstart, touchstarted); + dragRestore(); + zoomended(dispatch); + } + } + function mousewheeled() { + var dispatch = event.of(this, arguments); + if (mousewheelTimer) clearTimeout(mousewheelTimer); else d3_selection_interrupt.call(this), + translate0 = location(center0 = center || d3.mouse(this)), zoomstarted(dispatch); + mousewheelTimer = setTimeout(function() { + mousewheelTimer = null; + zoomended(dispatch); + }, 50); + d3_eventPreventDefault(); + scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * view.k); + translateTo(center0, translate0); + zoomed(dispatch); + } + function dblclicked() { + var p = d3.mouse(this), k = Math.log(view.k) / Math.LN2; + zoomTo(this, p, location(p), d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1); + } + return d3.rebind(zoom, event, "on"); + }; + var d3_behavior_zoomInfinity = [ 0, Infinity ], d3_behavior_zoomDelta, d3_behavior_zoomWheel; + d3.color = d3_color; + function d3_color() {} + d3_color.prototype.toString = function() { + return this.rgb() + ""; + }; + d3.hsl = d3_hsl; + function d3_hsl(h, s, l) { + return this instanceof d3_hsl ? void (this.h = +h, this.s = +s, this.l = +l) : arguments.length < 2 ? h instanceof d3_hsl ? new d3_hsl(h.h, h.s, h.l) : d3_rgb_parse("" + h, d3_rgb_hsl, d3_hsl) : new d3_hsl(h, s, l); + } + var d3_hslPrototype = d3_hsl.prototype = new d3_color(); + d3_hslPrototype.brighter = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_hsl(this.h, this.s, this.l / k); + }; + d3_hslPrototype.darker = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_hsl(this.h, this.s, k * this.l); + }; + d3_hslPrototype.rgb = function() { + return d3_hsl_rgb(this.h, this.s, this.l); + }; + function d3_hsl_rgb(h, s, l) { + var m1, m2; + h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h; + s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s; + l = l < 0 ? 0 : l > 1 ? 1 : l; + m2 = l <= .5 ? l * (1 + s) : l + s - l * s; + m1 = 2 * l - m2; + function v(h) { + if (h > 360) h -= 360; else if (h < 0) h += 360; + if (h < 60) return m1 + (m2 - m1) * h / 60; + if (h < 180) return m2; + if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; + return m1; + } + function vv(h) { + return Math.round(v(h) * 255); + } + return new d3_rgb(vv(h + 120), vv(h), vv(h - 120)); + } + d3.hcl = d3_hcl; + function d3_hcl(h, c, l) { + return this instanceof d3_hcl ? void (this.h = +h, this.c = +c, this.l = +l) : arguments.length < 2 ? h instanceof d3_hcl ? new d3_hcl(h.h, h.c, h.l) : h instanceof d3_lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : new d3_hcl(h, c, l); + } + var d3_hclPrototype = d3_hcl.prototype = new d3_color(); + d3_hclPrototype.brighter = function(k) { + return new d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1))); + }; + d3_hclPrototype.darker = function(k) { + return new d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1))); + }; + d3_hclPrototype.rgb = function() { + return d3_hcl_lab(this.h, this.c, this.l).rgb(); + }; + function d3_hcl_lab(h, c, l) { + if (isNaN(h)) h = 0; + if (isNaN(c)) c = 0; + return new d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c); + } + d3.lab = d3_lab; + function d3_lab(l, a, b) { + return this instanceof d3_lab ? void (this.l = +l, this.a = +a, this.b = +b) : arguments.length < 2 ? l instanceof d3_lab ? new d3_lab(l.l, l.a, l.b) : l instanceof d3_hcl ? d3_hcl_lab(l.h, l.c, l.l) : d3_rgb_lab((l = d3_rgb(l)).r, l.g, l.b) : new d3_lab(l, a, b); + } + var d3_lab_K = 18; + var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883; + var d3_labPrototype = d3_lab.prototype = new d3_color(); + d3_labPrototype.brighter = function(k) { + return new d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b); + }; + d3_labPrototype.darker = function(k) { + return new d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b); + }; + d3_labPrototype.rgb = function() { + return d3_lab_rgb(this.l, this.a, this.b); + }; + function d3_lab_rgb(l, a, b) { + var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200; + x = d3_lab_xyz(x) * d3_lab_X; + y = d3_lab_xyz(y) * d3_lab_Y; + z = d3_lab_xyz(z) * d3_lab_Z; + return new d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z)); + } + function d3_lab_hcl(l, a, b) { + return l > 0 ? new d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l) : new d3_hcl(NaN, NaN, l); + } + function d3_lab_xyz(x) { + return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037; + } + function d3_xyz_lab(x) { + return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29; + } + function d3_xyz_rgb(r) { + return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055)); + } + d3.rgb = d3_rgb; + function d3_rgb(r, g, b) { + return this instanceof d3_rgb ? void (this.r = ~~r, this.g = ~~g, this.b = ~~b) : arguments.length < 2 ? r instanceof d3_rgb ? new d3_rgb(r.r, r.g, r.b) : d3_rgb_parse("" + r, d3_rgb, d3_hsl_rgb) : new d3_rgb(r, g, b); + } + function d3_rgbNumber(value) { + return new d3_rgb(value >> 16, value >> 8 & 255, value & 255); + } + function d3_rgbString(value) { + return d3_rgbNumber(value) + ""; + } + var d3_rgbPrototype = d3_rgb.prototype = new d3_color(); + d3_rgbPrototype.brighter = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + var r = this.r, g = this.g, b = this.b, i = 30; + if (!r && !g && !b) return new d3_rgb(i, i, i); + if (r && r < i) r = i; + if (g && g < i) g = i; + if (b && b < i) b = i; + return new d3_rgb(Math.min(255, r / k), Math.min(255, g / k), Math.min(255, b / k)); + }; + d3_rgbPrototype.darker = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_rgb(k * this.r, k * this.g, k * this.b); + }; + d3_rgbPrototype.hsl = function() { + return d3_rgb_hsl(this.r, this.g, this.b); + }; + d3_rgbPrototype.toString = function() { + return "#" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b); + }; + function d3_rgb_hex(v) { + return v < 16 ? "0" + Math.max(0, v).toString(16) : Math.min(255, v).toString(16); + } + function d3_rgb_parse(format, rgb, hsl) { + var r = 0, g = 0, b = 0, m1, m2, color; + m1 = /([a-z]+)\((.*)\)/.exec(format = format.toLowerCase()); + if (m1) { + m2 = m1[2].split(","); + switch (m1[1]) { + case "hsl": + { + return hsl(parseFloat(m2[0]), parseFloat(m2[1]) / 100, parseFloat(m2[2]) / 100); + } + + case "rgb": + { + return rgb(d3_rgb_parseNumber(m2[0]), d3_rgb_parseNumber(m2[1]), d3_rgb_parseNumber(m2[2])); + } + } + } + if (color = d3_rgb_names.get(format)) { + return rgb(color.r, color.g, color.b); + } + if (format != null && format.charAt(0) === "#" && !isNaN(color = parseInt(format.slice(1), 16))) { + if (format.length === 4) { + r = (color & 3840) >> 4; + r = r >> 4 | r; + g = color & 240; + g = g >> 4 | g; + b = color & 15; + b = b << 4 | b; + } else if (format.length === 7) { + r = (color & 16711680) >> 16; + g = (color & 65280) >> 8; + b = color & 255; + } + } + return rgb(r, g, b); + } + function d3_rgb_hsl(r, g, b) { + var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2; + if (d) { + s = l < .5 ? d / (max + min) : d / (2 - max - min); + if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4; + h *= 60; + } else { + h = NaN; + s = l > 0 && l < 1 ? 0 : h; + } + return new d3_hsl(h, s, l); + } + function d3_rgb_lab(r, g, b) { + r = d3_rgb_xyz(r); + g = d3_rgb_xyz(g); + b = d3_rgb_xyz(b); + var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z); + return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z)); + } + function d3_rgb_xyz(r) { + return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4); + } + function d3_rgb_parseNumber(c) { + var f = parseFloat(c); + return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f; + } + var d3_rgb_names = d3.map({ + aliceblue: 15792383, + antiquewhite: 16444375, + aqua: 65535, + aquamarine: 8388564, + azure: 15794175, + beige: 16119260, + bisque: 16770244, + black: 0, + blanchedalmond: 16772045, + blue: 255, + blueviolet: 9055202, + brown: 10824234, + burlywood: 14596231, + cadetblue: 6266528, + chartreuse: 8388352, + chocolate: 13789470, + coral: 16744272, + cornflowerblue: 6591981, + cornsilk: 16775388, + crimson: 14423100, + cyan: 65535, + darkblue: 139, + darkcyan: 35723, + darkgoldenrod: 12092939, + darkgray: 11119017, + darkgreen: 25600, + darkgrey: 11119017, + darkkhaki: 12433259, + darkmagenta: 9109643, + darkolivegreen: 5597999, + darkorange: 16747520, + darkorchid: 10040012, + darkred: 9109504, + darksalmon: 15308410, + darkseagreen: 9419919, + darkslateblue: 4734347, + darkslategray: 3100495, + darkslategrey: 3100495, + darkturquoise: 52945, + darkviolet: 9699539, + deeppink: 16716947, + deepskyblue: 49151, + dimgray: 6908265, + dimgrey: 6908265, + dodgerblue: 2003199, + firebrick: 11674146, + floralwhite: 16775920, + forestgreen: 2263842, + fuchsia: 16711935, + gainsboro: 14474460, + ghostwhite: 16316671, + gold: 16766720, + goldenrod: 14329120, + gray: 8421504, + green: 32768, + greenyellow: 11403055, + grey: 8421504, + honeydew: 15794160, + hotpink: 16738740, + indianred: 13458524, + indigo: 4915330, + ivory: 16777200, + khaki: 15787660, + lavender: 15132410, + lavenderblush: 16773365, + lawngreen: 8190976, + lemonchiffon: 16775885, + lightblue: 11393254, + lightcoral: 15761536, + lightcyan: 14745599, + lightgoldenrodyellow: 16448210, + lightgray: 13882323, + lightgreen: 9498256, + lightgrey: 13882323, + lightpink: 16758465, + lightsalmon: 16752762, + lightseagreen: 2142890, + lightskyblue: 8900346, + lightslategray: 7833753, + lightslategrey: 7833753, + lightsteelblue: 11584734, + lightyellow: 16777184, + lime: 65280, + limegreen: 3329330, + linen: 16445670, + magenta: 16711935, + maroon: 8388608, + mediumaquamarine: 6737322, + mediumblue: 205, + mediumorchid: 12211667, + mediumpurple: 9662683, + mediumseagreen: 3978097, + mediumslateblue: 8087790, + mediumspringgreen: 64154, + mediumturquoise: 4772300, + mediumvioletred: 13047173, + midnightblue: 1644912, + mintcream: 16121850, + mistyrose: 16770273, + moccasin: 16770229, + navajowhite: 16768685, + navy: 128, + oldlace: 16643558, + olive: 8421376, + olivedrab: 7048739, + orange: 16753920, + orangered: 16729344, + orchid: 14315734, + palegoldenrod: 15657130, + palegreen: 10025880, + paleturquoise: 11529966, + palevioletred: 14381203, + papayawhip: 16773077, + peachpuff: 16767673, + peru: 13468991, + pink: 16761035, + plum: 14524637, + powderblue: 11591910, + purple: 8388736, + rebeccapurple: 6697881, + red: 16711680, + rosybrown: 12357519, + royalblue: 4286945, + saddlebrown: 9127187, + salmon: 16416882, + sandybrown: 16032864, + seagreen: 3050327, + seashell: 16774638, + sienna: 10506797, + silver: 12632256, + skyblue: 8900331, + slateblue: 6970061, + slategray: 7372944, + slategrey: 7372944, + snow: 16775930, + springgreen: 65407, + steelblue: 4620980, + tan: 13808780, + teal: 32896, + thistle: 14204888, + tomato: 16737095, + turquoise: 4251856, + violet: 15631086, + wheat: 16113331, + white: 16777215, + whitesmoke: 16119285, + yellow: 16776960, + yellowgreen: 10145074 + }); + d3_rgb_names.forEach(function(key, value) { + d3_rgb_names.set(key, d3_rgbNumber(value)); + }); + function d3_functor(v) { + return typeof v === "function" ? v : function() { + return v; + }; + } + d3.functor = d3_functor; + d3.xhr = d3_xhrType(d3_identity); + function d3_xhrType(response) { + return function(url, mimeType, callback) { + if (arguments.length === 2 && typeof mimeType === "function") callback = mimeType, + mimeType = null; + return d3_xhr(url, mimeType, response, callback); + }; + } + function d3_xhr(url, mimeType, response, callback) { + var xhr = {}, dispatch = d3.dispatch("beforesend", "progress", "load", "error"), headers = {}, request = new XMLHttpRequest(), responseType = null; + if (this.XDomainRequest && !("withCredentials" in request) && /^(http(s)?:)?\/\//.test(url)) request = new XDomainRequest(); + "onload" in request ? request.onload = request.onerror = respond : request.onreadystatechange = function() { + request.readyState > 3 && respond(); + }; + function respond() { + var status = request.status, result; + if (!status && d3_xhrHasResponse(request) || status >= 200 && status < 300 || status === 304) { + try { + result = response.call(xhr, request); + } catch (e) { + dispatch.error.call(xhr, e); + return; + } + dispatch.load.call(xhr, result); + } else { + dispatch.error.call(xhr, request); + } + } + request.onprogress = function(event) { + var o = d3.event; + d3.event = event; + try { + dispatch.progress.call(xhr, request); + } finally { + d3.event = o; + } + }; + xhr.header = function(name, value) { + name = (name + "").toLowerCase(); + if (arguments.length < 2) return headers[name]; + if (value == null) delete headers[name]; else headers[name] = value + ""; + return xhr; + }; + xhr.mimeType = function(value) { + if (!arguments.length) return mimeType; + mimeType = value == null ? null : value + ""; + return xhr; + }; + xhr.responseType = function(value) { + if (!arguments.length) return responseType; + responseType = value; + return xhr; + }; + xhr.response = function(value) { + response = value; + return xhr; + }; + [ "get", "post" ].forEach(function(method) { + xhr[method] = function() { + return xhr.send.apply(xhr, [ method ].concat(d3_array(arguments))); + }; + }); + xhr.send = function(method, data, callback) { + if (arguments.length === 2 && typeof data === "function") callback = data, data = null; + request.open(method, url, true); + if (mimeType != null && !("accept" in headers)) headers["accept"] = mimeType + ",*/*"; + if (request.setRequestHeader) for (var name in headers) request.setRequestHeader(name, headers[name]); + if (mimeType != null && request.overrideMimeType) request.overrideMimeType(mimeType); + if (responseType != null) request.responseType = responseType; + if (callback != null) xhr.on("error", callback).on("load", function(request) { + callback(null, request); + }); + dispatch.beforesend.call(xhr, request); + request.send(data == null ? null : data); + return xhr; + }; + xhr.abort = function() { + request.abort(); + return xhr; + }; + d3.rebind(xhr, dispatch, "on"); + return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback)); + } + function d3_xhr_fixCallback(callback) { + return callback.length === 1 ? function(error, request) { + callback(error == null ? request : null); + } : callback; + } + function d3_xhrHasResponse(request) { + var type = request.responseType; + return type && type !== "text" ? request.response : request.responseText; + } + d3.dsv = function(delimiter, mimeType) { + var reFormat = new RegExp('["' + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0); + function dsv(url, row, callback) { + if (arguments.length < 3) callback = row, row = null; + var xhr = d3_xhr(url, mimeType, row == null ? response : typedResponse(row), callback); + xhr.row = function(_) { + return arguments.length ? xhr.response((row = _) == null ? response : typedResponse(_)) : row; + }; + return xhr; + } + function response(request) { + return dsv.parse(request.responseText); + } + function typedResponse(f) { + return function(request) { + return dsv.parse(request.responseText, f); + }; + } + dsv.parse = function(text, f) { + var o; + return dsv.parseRows(text, function(row, i) { + if (o) return o(row, i - 1); + var a = new Function("d", "return {" + row.map(function(name, i) { + return JSON.stringify(name) + ": d[" + i + "]"; + }).join(",") + "}"); + o = f ? function(row, i) { + return f(a(row), i); + } : a; + }); + }; + dsv.parseRows = function(text, f) { + var EOL = {}, EOF = {}, rows = [], N = text.length, I = 0, n = 0, t, eol; + function token() { + if (I >= N) return EOF; + if (eol) return eol = false, EOL; + var j = I; + if (text.charCodeAt(j) === 34) { + var i = j; + while (i++ < N) { + if (text.charCodeAt(i) === 34) { + if (text.charCodeAt(i + 1) !== 34) break; + ++i; + } + } + I = i + 2; + var c = text.charCodeAt(i + 1); + if (c === 13) { + eol = true; + if (text.charCodeAt(i + 2) === 10) ++I; + } else if (c === 10) { + eol = true; + } + return text.slice(j + 1, i).replace(/""/g, '"'); + } + while (I < N) { + var c = text.charCodeAt(I++), k = 1; + if (c === 10) eol = true; else if (c === 13) { + eol = true; + if (text.charCodeAt(I) === 10) ++I, ++k; + } else if (c !== delimiterCode) continue; + return text.slice(j, I - k); + } + return text.slice(j); + } + while ((t = token()) !== EOF) { + var a = []; + while (t !== EOL && t !== EOF) { + a.push(t); + t = token(); + } + if (f && (a = f(a, n++)) == null) continue; + rows.push(a); + } + return rows; + }; + dsv.format = function(rows) { + if (Array.isArray(rows[0])) return dsv.formatRows(rows); + var fieldSet = new d3_Set(), fields = []; + rows.forEach(function(row) { + for (var field in row) { + if (!fieldSet.has(field)) { + fields.push(fieldSet.add(field)); + } + } + }); + return [ fields.map(formatValue).join(delimiter) ].concat(rows.map(function(row) { + return fields.map(function(field) { + return formatValue(row[field]); + }).join(delimiter); + })).join("\n"); + }; + dsv.formatRows = function(rows) { + return rows.map(formatRow).join("\n"); + }; + function formatRow(row) { + return row.map(formatValue).join(delimiter); + } + function formatValue(text) { + return reFormat.test(text) ? '"' + text.replace(/\"/g, '""') + '"' : text; + } + return dsv; + }; + d3.csv = d3.dsv(",", "text/csv"); + d3.tsv = d3.dsv(" ", "text/tab-separated-values"); + var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_frame = this[d3_vendorSymbol(this, "requestAnimationFrame")] || function(callback) { + setTimeout(callback, 17); + }; + d3.timer = function() { + d3_timer.apply(this, arguments); + }; + function d3_timer(callback, delay, then) { + var n = arguments.length; + if (n < 2) delay = 0; + if (n < 3) then = Date.now(); + var time = then + delay, timer = { + c: callback, + t: time, + n: null + }; + if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer; + d3_timer_queueTail = timer; + if (!d3_timer_interval) { + d3_timer_timeout = clearTimeout(d3_timer_timeout); + d3_timer_interval = 1; + d3_timer_frame(d3_timer_step); + } + return timer; + } + function d3_timer_step() { + var now = d3_timer_mark(), delay = d3_timer_sweep() - now; + if (delay > 24) { + if (isFinite(delay)) { + clearTimeout(d3_timer_timeout); + d3_timer_timeout = setTimeout(d3_timer_step, delay); + } + d3_timer_interval = 0; + } else { + d3_timer_interval = 1; + d3_timer_frame(d3_timer_step); + } + } + d3.timer.flush = function() { + d3_timer_mark(); + d3_timer_sweep(); + }; + function d3_timer_mark() { + var now = Date.now(), timer = d3_timer_queueHead; + while (timer) { + if (now >= timer.t && timer.c(now - timer.t)) timer.c = null; + timer = timer.n; + } + return now; + } + function d3_timer_sweep() { + var t0, t1 = d3_timer_queueHead, time = Infinity; + while (t1) { + if (t1.c) { + if (t1.t < time) time = t1.t; + t1 = (t0 = t1).n; + } else { + t1 = t0 ? t0.n = t1.n : d3_timer_queueHead = t1.n; + } + } + d3_timer_queueTail = t0; + return time; + } + function d3_format_precision(x, p) { + return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1); + } + d3.round = function(x, n) { + return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x); + }; + var d3_formatPrefixes = [ "y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" ].map(d3_formatPrefix); + d3.formatPrefix = function(value, precision) { + var i = 0; + if (value = +value) { + if (value < 0) value *= -1; + if (precision) value = d3.round(value, d3_format_precision(value, precision)); + i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); + i = Math.max(-24, Math.min(24, Math.floor((i - 1) / 3) * 3)); + } + return d3_formatPrefixes[8 + i / 3]; + }; + function d3_formatPrefix(d, i) { + var k = Math.pow(10, abs(8 - i) * 3); + return { + scale: i > 8 ? function(d) { + return d / k; + } : function(d) { + return d * k; + }, + symbol: d + }; + } + function d3_locale_numberFormat(locale) { + var locale_decimal = locale.decimal, locale_thousands = locale.thousands, locale_grouping = locale.grouping, locale_currency = locale.currency, formatGroup = locale_grouping && locale_thousands ? function(value, width) { + var i = value.length, t = [], j = 0, g = locale_grouping[0], length = 0; + while (i > 0 && g > 0) { + if (length + g + 1 > width) g = Math.max(1, width - length); + t.push(value.substring(i -= g, i + g)); + if ((length += g + 1) > width) break; + g = locale_grouping[j = (j + 1) % locale_grouping.length]; + } + return t.reverse().join(locale_thousands); + } : d3_identity; + return function(specifier) { + var match = d3_format_re.exec(specifier), fill = match[1] || " ", align = match[2] || ">", sign = match[3] || "-", symbol = match[4] || "", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, prefix = "", suffix = "", integer = false, exponent = true; + if (precision) precision = +precision.substring(1); + if (zfill || fill === "0" && align === "=") { + zfill = fill = "0"; + align = "="; + } + switch (type) { + case "n": + comma = true; + type = "g"; + break; + + case "%": + scale = 100; + suffix = "%"; + type = "f"; + break; + + case "p": + scale = 100; + suffix = "%"; + type = "r"; + break; + + case "b": + case "o": + case "x": + case "X": + if (symbol === "#") prefix = "0" + type.toLowerCase(); + + case "c": + exponent = false; + + case "d": + integer = true; + precision = 0; + break; + + case "s": + scale = -1; + type = "r"; + break; + } + if (symbol === "$") prefix = locale_currency[0], suffix = locale_currency[1]; + if (type == "r" && !precision) type = "g"; + if (precision != null) { + if (type == "g") precision = Math.max(1, Math.min(21, precision)); else if (type == "e" || type == "f") precision = Math.max(0, Math.min(20, precision)); + } + type = d3_format_types.get(type) || d3_format_typeDefault; + var zcomma = zfill && comma; + return function(value) { + var fullSuffix = suffix; + if (integer && value % 1) return ""; + var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, "-") : sign === "-" ? "" : sign; + if (scale < 0) { + var unit = d3.formatPrefix(value, precision); + value = unit.scale(value); + fullSuffix = unit.symbol + suffix; + } else { + value *= scale; + } + value = type(value, precision); + var i = value.lastIndexOf("."), before, after; + if (i < 0) { + var j = exponent ? value.lastIndexOf("e") : -1; + if (j < 0) before = value, after = ""; else before = value.substring(0, j), after = value.substring(j); + } else { + before = value.substring(0, i); + after = locale_decimal + value.substring(i + 1); + } + if (!zfill && comma) before = formatGroup(before, Infinity); + var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length), padding = length < width ? new Array(length = width - length + 1).join(fill) : ""; + if (zcomma) before = formatGroup(padding + before, padding.length ? width - after.length : Infinity); + negative += prefix; + value = before + after; + return (align === "<" ? negative + value + padding : align === ">" ? padding + negative + value : align === "^" ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + fullSuffix; + }; + }; + } + var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; + var d3_format_types = d3.map({ + b: function(x) { + return x.toString(2); + }, + c: function(x) { + return String.fromCharCode(x); + }, + o: function(x) { + return x.toString(8); + }, + x: function(x) { + return x.toString(16); + }, + X: function(x) { + return x.toString(16).toUpperCase(); + }, + g: function(x, p) { + return x.toPrecision(p); + }, + e: function(x, p) { + return x.toExponential(p); + }, + f: function(x, p) { + return x.toFixed(p); + }, + r: function(x, p) { + return (x = d3.round(x, d3_format_precision(x, p))).toFixed(Math.max(0, Math.min(20, d3_format_precision(x * (1 + 1e-15), p)))); + } + }); + function d3_format_typeDefault(x) { + return x + ""; + } + var d3_time = d3.time = {}, d3_date = Date; + function d3_date_utc() { + this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]); + } + d3_date_utc.prototype = { + getDate: function() { + return this._.getUTCDate(); + }, + getDay: function() { + return this._.getUTCDay(); + }, + getFullYear: function() { + return this._.getUTCFullYear(); + }, + getHours: function() { + return this._.getUTCHours(); + }, + getMilliseconds: function() { + return this._.getUTCMilliseconds(); + }, + getMinutes: function() { + return this._.getUTCMinutes(); + }, + getMonth: function() { + return this._.getUTCMonth(); + }, + getSeconds: function() { + return this._.getUTCSeconds(); + }, + getTime: function() { + return this._.getTime(); + }, + getTimezoneOffset: function() { + return 0; + }, + valueOf: function() { + return this._.valueOf(); + }, + setDate: function() { + d3_time_prototype.setUTCDate.apply(this._, arguments); + }, + setDay: function() { + d3_time_prototype.setUTCDay.apply(this._, arguments); + }, + setFullYear: function() { + d3_time_prototype.setUTCFullYear.apply(this._, arguments); + }, + setHours: function() { + d3_time_prototype.setUTCHours.apply(this._, arguments); + }, + setMilliseconds: function() { + d3_time_prototype.setUTCMilliseconds.apply(this._, arguments); + }, + setMinutes: function() { + d3_time_prototype.setUTCMinutes.apply(this._, arguments); + }, + setMonth: function() { + d3_time_prototype.setUTCMonth.apply(this._, arguments); + }, + setSeconds: function() { + d3_time_prototype.setUTCSeconds.apply(this._, arguments); + }, + setTime: function() { + d3_time_prototype.setTime.apply(this._, arguments); + } + }; + var d3_time_prototype = Date.prototype; + function d3_time_interval(local, step, number) { + function round(date) { + var d0 = local(date), d1 = offset(d0, 1); + return date - d0 < d1 - date ? d0 : d1; + } + function ceil(date) { + step(date = local(new d3_date(date - 1)), 1); + return date; + } + function offset(date, k) { + step(date = new d3_date(+date), k); + return date; + } + function range(t0, t1, dt) { + var time = ceil(t0), times = []; + if (dt > 1) { + while (time < t1) { + if (!(number(time) % dt)) times.push(new Date(+time)); + step(time, 1); + } + } else { + while (time < t1) times.push(new Date(+time)), step(time, 1); + } + return times; + } + function range_utc(t0, t1, dt) { + try { + d3_date = d3_date_utc; + var utc = new d3_date_utc(); + utc._ = t0; + return range(utc, t1, dt); + } finally { + d3_date = Date; + } + } + local.floor = local; + local.round = round; + local.ceil = ceil; + local.offset = offset; + local.range = range; + var utc = local.utc = d3_time_interval_utc(local); + utc.floor = utc; + utc.round = d3_time_interval_utc(round); + utc.ceil = d3_time_interval_utc(ceil); + utc.offset = d3_time_interval_utc(offset); + utc.range = range_utc; + return local; + } + function d3_time_interval_utc(method) { + return function(date, k) { + try { + d3_date = d3_date_utc; + var utc = new d3_date_utc(); + utc._ = date; + return method(utc, k)._; + } finally { + d3_date = Date; + } + }; + } + d3_time.year = d3_time_interval(function(date) { + date = d3_time.day(date); + date.setMonth(0, 1); + return date; + }, function(date, offset) { + date.setFullYear(date.getFullYear() + offset); + }, function(date) { + return date.getFullYear(); + }); + d3_time.years = d3_time.year.range; + d3_time.years.utc = d3_time.year.utc.range; + d3_time.day = d3_time_interval(function(date) { + var day = new d3_date(2e3, 0); + day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); + return day; + }, function(date, offset) { + date.setDate(date.getDate() + offset); + }, function(date) { + return date.getDate() - 1; + }); + d3_time.days = d3_time.day.range; + d3_time.days.utc = d3_time.day.utc.range; + d3_time.dayOfYear = function(date) { + var year = d3_time.year(date); + return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5); + }; + [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ].forEach(function(day, i) { + i = 7 - i; + var interval = d3_time[day] = d3_time_interval(function(date) { + (date = d3_time.day(date)).setDate(date.getDate() - (date.getDay() + i) % 7); + return date; + }, function(date, offset) { + date.setDate(date.getDate() + Math.floor(offset) * 7); + }, function(date) { + var day = d3_time.year(date).getDay(); + return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7) - (day !== i); + }); + d3_time[day + "s"] = interval.range; + d3_time[day + "s"].utc = interval.utc.range; + d3_time[day + "OfYear"] = function(date) { + var day = d3_time.year(date).getDay(); + return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7); + }; + }); + d3_time.week = d3_time.sunday; + d3_time.weeks = d3_time.sunday.range; + d3_time.weeks.utc = d3_time.sunday.utc.range; + d3_time.weekOfYear = d3_time.sundayOfYear; + function d3_locale_timeFormat(locale) { + var locale_dateTime = locale.dateTime, locale_date = locale.date, locale_time = locale.time, locale_periods = locale.periods, locale_days = locale.days, locale_shortDays = locale.shortDays, locale_months = locale.months, locale_shortMonths = locale.shortMonths; + function d3_time_format(template) { + var n = template.length; + function format(date) { + var string = [], i = -1, j = 0, c, p, f; + while (++i < n) { + if (template.charCodeAt(i) === 37) { + string.push(template.slice(j, i)); + if ((p = d3_time_formatPads[c = template.charAt(++i)]) != null) c = template.charAt(++i); + if (f = d3_time_formats[c]) c = f(date, p == null ? c === "e" ? " " : "0" : p); + string.push(c); + j = i + 1; + } + } + string.push(template.slice(j, i)); + return string.join(""); + } + format.parse = function(string) { + var d = { + y: 1900, + m: 0, + d: 1, + H: 0, + M: 0, + S: 0, + L: 0, + Z: null + }, i = d3_time_parse(d, template, string, 0); + if (i != string.length) return null; + if ("p" in d) d.H = d.H % 12 + d.p * 12; + var localZ = d.Z != null && d3_date !== d3_date_utc, date = new (localZ ? d3_date_utc : d3_date)(); + if ("j" in d) date.setFullYear(d.y, 0, d.j); else if ("W" in d || "U" in d) { + if (!("w" in d)) d.w = "W" in d ? 1 : 0; + date.setFullYear(d.y, 0, 1); + date.setFullYear(d.y, 0, "W" in d ? (d.w + 6) % 7 + d.W * 7 - (date.getDay() + 5) % 7 : d.w + d.U * 7 - (date.getDay() + 6) % 7); + } else date.setFullYear(d.y, d.m, d.d); + date.setHours(d.H + (d.Z / 100 | 0), d.M + d.Z % 100, d.S, d.L); + return localZ ? date._ : date; + }; + format.toString = function() { + return template; + }; + return format; + } + function d3_time_parse(date, template, string, j) { + var c, p, t, i = 0, n = template.length, m = string.length; + while (i < n) { + if (j >= m) return -1; + c = template.charCodeAt(i++); + if (c === 37) { + t = template.charAt(i++); + p = d3_time_parsers[t in d3_time_formatPads ? template.charAt(i++) : t]; + if (!p || (j = p(date, string, j)) < 0) return -1; + } else if (c != string.charCodeAt(j++)) { + return -1; + } + } + return j; + } + d3_time_format.utc = function(template) { + var local = d3_time_format(template); + function format(date) { + try { + d3_date = d3_date_utc; + var utc = new d3_date(); + utc._ = date; + return local(utc); + } finally { + d3_date = Date; + } + } + format.parse = function(string) { + try { + d3_date = d3_date_utc; + var date = local.parse(string); + return date && date._; + } finally { + d3_date = Date; + } + }; + format.toString = local.toString; + return format; + }; + d3_time_format.multi = d3_time_format.utc.multi = d3_time_formatMulti; + var d3_time_periodLookup = d3.map(), d3_time_dayRe = d3_time_formatRe(locale_days), d3_time_dayLookup = d3_time_formatLookup(locale_days), d3_time_dayAbbrevRe = d3_time_formatRe(locale_shortDays), d3_time_dayAbbrevLookup = d3_time_formatLookup(locale_shortDays), d3_time_monthRe = d3_time_formatRe(locale_months), d3_time_monthLookup = d3_time_formatLookup(locale_months), d3_time_monthAbbrevRe = d3_time_formatRe(locale_shortMonths), d3_time_monthAbbrevLookup = d3_time_formatLookup(locale_shortMonths); + locale_periods.forEach(function(p, i) { + d3_time_periodLookup.set(p.toLowerCase(), i); + }); + var d3_time_formats = { + a: function(d) { + return locale_shortDays[d.getDay()]; + }, + A: function(d) { + return locale_days[d.getDay()]; + }, + b: function(d) { + return locale_shortMonths[d.getMonth()]; + }, + B: function(d) { + return locale_months[d.getMonth()]; + }, + c: d3_time_format(locale_dateTime), + d: function(d, p) { + return d3_time_formatPad(d.getDate(), p, 2); + }, + e: function(d, p) { + return d3_time_formatPad(d.getDate(), p, 2); + }, + H: function(d, p) { + return d3_time_formatPad(d.getHours(), p, 2); + }, + I: function(d, p) { + return d3_time_formatPad(d.getHours() % 12 || 12, p, 2); + }, + j: function(d, p) { + return d3_time_formatPad(1 + d3_time.dayOfYear(d), p, 3); + }, + L: function(d, p) { + return d3_time_formatPad(d.getMilliseconds(), p, 3); + }, + m: function(d, p) { + return d3_time_formatPad(d.getMonth() + 1, p, 2); + }, + M: function(d, p) { + return d3_time_formatPad(d.getMinutes(), p, 2); + }, + p: function(d) { + return locale_periods[+(d.getHours() >= 12)]; + }, + S: function(d, p) { + return d3_time_formatPad(d.getSeconds(), p, 2); + }, + U: function(d, p) { + return d3_time_formatPad(d3_time.sundayOfYear(d), p, 2); + }, + w: function(d) { + return d.getDay(); + }, + W: function(d, p) { + return d3_time_formatPad(d3_time.mondayOfYear(d), p, 2); + }, + x: d3_time_format(locale_date), + X: d3_time_format(locale_time), + y: function(d, p) { + return d3_time_formatPad(d.getFullYear() % 100, p, 2); + }, + Y: function(d, p) { + return d3_time_formatPad(d.getFullYear() % 1e4, p, 4); + }, + Z: d3_time_zone, + "%": function() { + return "%"; + } + }; + var d3_time_parsers = { + a: d3_time_parseWeekdayAbbrev, + A: d3_time_parseWeekday, + b: d3_time_parseMonthAbbrev, + B: d3_time_parseMonth, + c: d3_time_parseLocaleFull, + d: d3_time_parseDay, + e: d3_time_parseDay, + H: d3_time_parseHour24, + I: d3_time_parseHour24, + j: d3_time_parseDayOfYear, + L: d3_time_parseMilliseconds, + m: d3_time_parseMonthNumber, + M: d3_time_parseMinutes, + p: d3_time_parseAmPm, + S: d3_time_parseSeconds, + U: d3_time_parseWeekNumberSunday, + w: d3_time_parseWeekdayNumber, + W: d3_time_parseWeekNumberMonday, + x: d3_time_parseLocaleDate, + X: d3_time_parseLocaleTime, + y: d3_time_parseYear, + Y: d3_time_parseFullYear, + Z: d3_time_parseZone, + "%": d3_time_parseLiteralPercent + }; + function d3_time_parseWeekdayAbbrev(date, string, i) { + d3_time_dayAbbrevRe.lastIndex = 0; + var n = d3_time_dayAbbrevRe.exec(string.slice(i)); + return n ? (date.w = d3_time_dayAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseWeekday(date, string, i) { + d3_time_dayRe.lastIndex = 0; + var n = d3_time_dayRe.exec(string.slice(i)); + return n ? (date.w = d3_time_dayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseMonthAbbrev(date, string, i) { + d3_time_monthAbbrevRe.lastIndex = 0; + var n = d3_time_monthAbbrevRe.exec(string.slice(i)); + return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseMonth(date, string, i) { + d3_time_monthRe.lastIndex = 0; + var n = d3_time_monthRe.exec(string.slice(i)); + return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseLocaleFull(date, string, i) { + return d3_time_parse(date, d3_time_formats.c.toString(), string, i); + } + function d3_time_parseLocaleDate(date, string, i) { + return d3_time_parse(date, d3_time_formats.x.toString(), string, i); + } + function d3_time_parseLocaleTime(date, string, i) { + return d3_time_parse(date, d3_time_formats.X.toString(), string, i); + } + function d3_time_parseAmPm(date, string, i) { + var n = d3_time_periodLookup.get(string.slice(i, i += 2).toLowerCase()); + return n == null ? -1 : (date.p = n, i); + } + return d3_time_format; + } + var d3_time_formatPads = { + "-": "", + _: " ", + "0": "0" + }, d3_time_numberRe = /^\s*\d+/, d3_time_percentRe = /^%/; + function d3_time_formatPad(value, fill, width) { + var sign = value < 0 ? "-" : "", string = (sign ? -value : value) + "", length = string.length; + return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string); + } + function d3_time_formatRe(names) { + return new RegExp("^(?:" + names.map(d3.requote).join("|") + ")", "i"); + } + function d3_time_formatLookup(names) { + var map = new d3_Map(), i = -1, n = names.length; + while (++i < n) map.set(names[i].toLowerCase(), i); + return map; + } + function d3_time_parseWeekdayNumber(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 1)); + return n ? (date.w = +n[0], i + n[0].length) : -1; + } + function d3_time_parseWeekNumberSunday(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i)); + return n ? (date.U = +n[0], i + n[0].length) : -1; + } + function d3_time_parseWeekNumberMonday(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i)); + return n ? (date.W = +n[0], i + n[0].length) : -1; + } + function d3_time_parseFullYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 4)); + return n ? (date.y = +n[0], i + n[0].length) : -1; + } + function d3_time_parseYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.y = d3_time_expandYear(+n[0]), i + n[0].length) : -1; + } + function d3_time_parseZone(date, string, i) { + return /^[+-]\d{4}$/.test(string = string.slice(i, i + 5)) ? (date.Z = -string, + i + 5) : -1; + } + function d3_time_expandYear(d) { + return d + (d > 68 ? 1900 : 2e3); + } + function d3_time_parseMonthNumber(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.m = n[0] - 1, i + n[0].length) : -1; + } + function d3_time_parseDay(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.d = +n[0], i + n[0].length) : -1; + } + function d3_time_parseDayOfYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 3)); + return n ? (date.j = +n[0], i + n[0].length) : -1; + } + function d3_time_parseHour24(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.H = +n[0], i + n[0].length) : -1; + } + function d3_time_parseMinutes(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.M = +n[0], i + n[0].length) : -1; + } + function d3_time_parseSeconds(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.S = +n[0], i + n[0].length) : -1; + } + function d3_time_parseMilliseconds(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 3)); + return n ? (date.L = +n[0], i + n[0].length) : -1; + } + function d3_time_zone(d) { + var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = abs(z) / 60 | 0, zm = abs(z) % 60; + return zs + d3_time_formatPad(zh, "0", 2) + d3_time_formatPad(zm, "0", 2); + } + function d3_time_parseLiteralPercent(date, string, i) { + d3_time_percentRe.lastIndex = 0; + var n = d3_time_percentRe.exec(string.slice(i, i + 1)); + return n ? i + n[0].length : -1; + } + function d3_time_formatMulti(formats) { + var n = formats.length, i = -1; + while (++i < n) formats[i][0] = this(formats[i][0]); + return function(date) { + var i = 0, f = formats[i]; + while (!f[1](date)) f = formats[++i]; + return f[0](date); + }; + } + d3.locale = function(locale) { + return { + numberFormat: d3_locale_numberFormat(locale), + timeFormat: d3_locale_timeFormat(locale) + }; + }; + var d3_locale_enUS = d3.locale({ + decimal: ".", + thousands: ",", + grouping: [ 3 ], + currency: [ "$", "" ], + dateTime: "%a %b %e %X %Y", + date: "%m/%d/%Y", + time: "%H:%M:%S", + periods: [ "AM", "PM" ], + days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], + shortDays: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], + months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], + shortMonths: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] + }); + d3.format = d3_locale_enUS.numberFormat; + d3.geo = {}; + function d3_adder() {} + d3_adder.prototype = { + s: 0, + t: 0, + add: function(y) { + d3_adderSum(y, this.t, d3_adderTemp); + d3_adderSum(d3_adderTemp.s, this.s, this); + if (this.s) this.t += d3_adderTemp.t; else this.s = d3_adderTemp.t; + }, + reset: function() { + this.s = this.t = 0; + }, + valueOf: function() { + return this.s; + } + }; + var d3_adderTemp = new d3_adder(); + function d3_adderSum(a, b, o) { + var x = o.s = a + b, bv = x - a, av = x - bv; + o.t = a - av + (b - bv); + } + d3.geo.stream = function(object, listener) { + if (object && d3_geo_streamObjectType.hasOwnProperty(object.type)) { + d3_geo_streamObjectType[object.type](object, listener); + } else { + d3_geo_streamGeometry(object, listener); + } + }; + function d3_geo_streamGeometry(geometry, listener) { + if (geometry && d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) { + d3_geo_streamGeometryType[geometry.type](geometry, listener); + } + } + var d3_geo_streamObjectType = { + Feature: function(feature, listener) { + d3_geo_streamGeometry(feature.geometry, listener); + }, + FeatureCollection: function(object, listener) { + var features = object.features, i = -1, n = features.length; + while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener); + } + }; + var d3_geo_streamGeometryType = { + Sphere: function(object, listener) { + listener.sphere(); + }, + Point: function(object, listener) { + object = object.coordinates; + listener.point(object[0], object[1], object[2]); + }, + MultiPoint: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) object = coordinates[i], listener.point(object[0], object[1], object[2]); + }, + LineString: function(object, listener) { + d3_geo_streamLine(object.coordinates, listener, 0); + }, + MultiLineString: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0); + }, + Polygon: function(object, listener) { + d3_geo_streamPolygon(object.coordinates, listener); + }, + MultiPolygon: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamPolygon(coordinates[i], listener); + }, + GeometryCollection: function(object, listener) { + var geometries = object.geometries, i = -1, n = geometries.length; + while (++i < n) d3_geo_streamGeometry(geometries[i], listener); + } + }; + function d3_geo_streamLine(coordinates, listener, closed) { + var i = -1, n = coordinates.length - closed, coordinate; + listener.lineStart(); + while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1], coordinate[2]); + listener.lineEnd(); + } + function d3_geo_streamPolygon(coordinates, listener) { + var i = -1, n = coordinates.length; + listener.polygonStart(); + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1); + listener.polygonEnd(); + } + d3.geo.area = function(object) { + d3_geo_areaSum = 0; + d3.geo.stream(object, d3_geo_area); + return d3_geo_areaSum; + }; + var d3_geo_areaSum, d3_geo_areaRingSum = new d3_adder(); + var d3_geo_area = { + sphere: function() { + d3_geo_areaSum += 4 * π; + }, + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_areaRingSum.reset(); + d3_geo_area.lineStart = d3_geo_areaRingStart; + }, + polygonEnd: function() { + var area = 2 * d3_geo_areaRingSum; + d3_geo_areaSum += area < 0 ? 4 * π + area : area; + d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop; + } + }; + function d3_geo_areaRingStart() { + var λ00, φ00, λ0, cosφ0, sinφ0; + d3_geo_area.point = function(λ, φ) { + d3_geo_area.point = nextPoint; + λ0 = (λ00 = λ) * d3_radians, cosφ0 = Math.cos(φ = (φ00 = φ) * d3_radians / 2 + π / 4), + sinφ0 = Math.sin(φ); + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + φ = φ * d3_radians / 2 + π / 4; + var dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, cosφ = Math.cos(φ), sinφ = Math.sin(φ), k = sinφ0 * sinφ, u = cosφ0 * cosφ + k * Math.cos(adλ), v = k * sdλ * Math.sin(adλ); + d3_geo_areaRingSum.add(Math.atan2(v, u)); + λ0 = λ, cosφ0 = cosφ, sinφ0 = sinφ; + } + d3_geo_area.lineEnd = function() { + nextPoint(λ00, φ00); + }; + } + function d3_geo_cartesian(spherical) { + var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ); + return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ]; + } + function d3_geo_cartesianDot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + function d3_geo_cartesianCross(a, b) { + return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; + } + function d3_geo_cartesianAdd(a, b) { + a[0] += b[0]; + a[1] += b[1]; + a[2] += b[2]; + } + function d3_geo_cartesianScale(vector, k) { + return [ vector[0] * k, vector[1] * k, vector[2] * k ]; + } + function d3_geo_cartesianNormalize(d) { + var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); + d[0] /= l; + d[1] /= l; + d[2] /= l; + } + function d3_geo_spherical(cartesian) { + return [ Math.atan2(cartesian[1], cartesian[0]), d3_asin(cartesian[2]) ]; + } + function d3_geo_sphericalEqual(a, b) { + return abs(a[0] - b[0]) < ε && abs(a[1] - b[1]) < ε; + } + d3.geo.bounds = function() { + var λ0, φ0, λ1, φ1, λ_, λ__, φ__, p0, dλSum, ranges, range; + var bound = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + bound.point = ringPoint; + bound.lineStart = ringStart; + bound.lineEnd = ringEnd; + dλSum = 0; + d3_geo_area.polygonStart(); + }, + polygonEnd: function() { + d3_geo_area.polygonEnd(); + bound.point = point; + bound.lineStart = lineStart; + bound.lineEnd = lineEnd; + if (d3_geo_areaRingSum < 0) λ0 = -(λ1 = 180), φ0 = -(φ1 = 90); else if (dλSum > ε) φ1 = 90; else if (dλSum < -ε) φ0 = -90; + range[0] = λ0, range[1] = λ1; + } + }; + function point(λ, φ) { + ranges.push(range = [ λ0 = λ, λ1 = λ ]); + if (φ < φ0) φ0 = φ; + if (φ > φ1) φ1 = φ; + } + function linePoint(λ, φ) { + var p = d3_geo_cartesian([ λ * d3_radians, φ * d3_radians ]); + if (p0) { + var normal = d3_geo_cartesianCross(p0, p), equatorial = [ normal[1], -normal[0], 0 ], inflection = d3_geo_cartesianCross(equatorial, normal); + d3_geo_cartesianNormalize(inflection); + inflection = d3_geo_spherical(inflection); + var dλ = λ - λ_, s = dλ > 0 ? 1 : -1, λi = inflection[0] * d3_degrees * s, antimeridian = abs(dλ) > 180; + if (antimeridian ^ (s * λ_ < λi && λi < s * λ)) { + var φi = inflection[1] * d3_degrees; + if (φi > φ1) φ1 = φi; + } else if (λi = (λi + 360) % 360 - 180, antimeridian ^ (s * λ_ < λi && λi < s * λ)) { + var φi = -inflection[1] * d3_degrees; + if (φi < φ0) φ0 = φi; + } else { + if (φ < φ0) φ0 = φ; + if (φ > φ1) φ1 = φ; + } + if (antimeridian) { + if (λ < λ_) { + if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ; + } else { + if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ; + } + } else { + if (λ1 >= λ0) { + if (λ < λ0) λ0 = λ; + if (λ > λ1) λ1 = λ; + } else { + if (λ > λ_) { + if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ; + } else { + if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ; + } + } + } + } else { + point(λ, φ); + } + p0 = p, λ_ = λ; + } + function lineStart() { + bound.point = linePoint; + } + function lineEnd() { + range[0] = λ0, range[1] = λ1; + bound.point = point; + p0 = null; + } + function ringPoint(λ, φ) { + if (p0) { + var dλ = λ - λ_; + dλSum += abs(dλ) > 180 ? dλ + (dλ > 0 ? 360 : -360) : dλ; + } else λ__ = λ, φ__ = φ; + d3_geo_area.point(λ, φ); + linePoint(λ, φ); + } + function ringStart() { + d3_geo_area.lineStart(); + } + function ringEnd() { + ringPoint(λ__, φ__); + d3_geo_area.lineEnd(); + if (abs(dλSum) > ε) λ0 = -(λ1 = 180); + range[0] = λ0, range[1] = λ1; + p0 = null; + } + function angle(λ0, λ1) { + return (λ1 -= λ0) < 0 ? λ1 + 360 : λ1; + } + function compareRanges(a, b) { + return a[0] - b[0]; + } + function withinRange(x, range) { + return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x; + } + return function(feature) { + φ1 = λ1 = -(λ0 = φ0 = Infinity); + ranges = []; + d3.geo.stream(feature, bound); + var n = ranges.length; + if (n) { + ranges.sort(compareRanges); + for (var i = 1, a = ranges[0], b, merged = [ a ]; i < n; ++i) { + b = ranges[i]; + if (withinRange(b[0], a) || withinRange(b[1], a)) { + if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1]; + if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0]; + } else { + merged.push(a = b); + } + } + var best = -Infinity, dλ; + for (var n = merged.length - 1, i = 0, a = merged[n], b; i <= n; a = b, ++i) { + b = merged[i]; + if ((dλ = angle(a[1], b[0])) > best) best = dλ, λ0 = b[0], λ1 = a[1]; + } + } + ranges = range = null; + return λ0 === Infinity || φ0 === Infinity ? [ [ NaN, NaN ], [ NaN, NaN ] ] : [ [ λ0, φ0 ], [ λ1, φ1 ] ]; + }; + }(); + d3.geo.centroid = function(object) { + d3_geo_centroidW0 = d3_geo_centroidW1 = d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0; + d3.geo.stream(object, d3_geo_centroid); + var x = d3_geo_centroidX2, y = d3_geo_centroidY2, z = d3_geo_centroidZ2, m = x * x + y * y + z * z; + if (m < ε2) { + x = d3_geo_centroidX1, y = d3_geo_centroidY1, z = d3_geo_centroidZ1; + if (d3_geo_centroidW1 < ε) x = d3_geo_centroidX0, y = d3_geo_centroidY0, z = d3_geo_centroidZ0; + m = x * x + y * y + z * z; + if (m < ε2) return [ NaN, NaN ]; + } + return [ Math.atan2(y, x) * d3_degrees, d3_asin(z / Math.sqrt(m)) * d3_degrees ]; + }; + var d3_geo_centroidW0, d3_geo_centroidW1, d3_geo_centroidX0, d3_geo_centroidY0, d3_geo_centroidZ0, d3_geo_centroidX1, d3_geo_centroidY1, d3_geo_centroidZ1, d3_geo_centroidX2, d3_geo_centroidY2, d3_geo_centroidZ2; + var d3_geo_centroid = { + sphere: d3_noop, + point: d3_geo_centroidPoint, + lineStart: d3_geo_centroidLineStart, + lineEnd: d3_geo_centroidLineEnd, + polygonStart: function() { + d3_geo_centroid.lineStart = d3_geo_centroidRingStart; + }, + polygonEnd: function() { + d3_geo_centroid.lineStart = d3_geo_centroidLineStart; + } + }; + function d3_geo_centroidPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + d3_geo_centroidPointXYZ(cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ)); + } + function d3_geo_centroidPointXYZ(x, y, z) { + ++d3_geo_centroidW0; + d3_geo_centroidX0 += (x - d3_geo_centroidX0) / d3_geo_centroidW0; + d3_geo_centroidY0 += (y - d3_geo_centroidY0) / d3_geo_centroidW0; + d3_geo_centroidZ0 += (z - d3_geo_centroidZ0) / d3_geo_centroidW0; + } + function d3_geo_centroidLineStart() { + var x0, y0, z0; + d3_geo_centroid.point = function(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + x0 = cosφ * Math.cos(λ); + y0 = cosφ * Math.sin(λ); + z0 = Math.sin(φ); + d3_geo_centroid.point = nextPoint; + d3_geo_centroidPointXYZ(x0, y0, z0); + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z); + d3_geo_centroidW1 += w; + d3_geo_centroidX1 += w * (x0 + (x0 = x)); + d3_geo_centroidY1 += w * (y0 + (y0 = y)); + d3_geo_centroidZ1 += w * (z0 + (z0 = z)); + d3_geo_centroidPointXYZ(x0, y0, z0); + } + } + function d3_geo_centroidLineEnd() { + d3_geo_centroid.point = d3_geo_centroidPoint; + } + function d3_geo_centroidRingStart() { + var λ00, φ00, x0, y0, z0; + d3_geo_centroid.point = function(λ, φ) { + λ00 = λ, φ00 = φ; + d3_geo_centroid.point = nextPoint; + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + x0 = cosφ * Math.cos(λ); + y0 = cosφ * Math.sin(λ); + z0 = Math.sin(φ); + d3_geo_centroidPointXYZ(x0, y0, z0); + }; + d3_geo_centroid.lineEnd = function() { + nextPoint(λ00, φ00); + d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd; + d3_geo_centroid.point = d3_geo_centroidPoint; + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), cx = y0 * z - z0 * y, cy = z0 * x - x0 * z, cz = x0 * y - y0 * x, m = Math.sqrt(cx * cx + cy * cy + cz * cz), u = x0 * x + y0 * y + z0 * z, v = m && -d3_acos(u) / m, w = Math.atan2(m, u); + d3_geo_centroidX2 += v * cx; + d3_geo_centroidY2 += v * cy; + d3_geo_centroidZ2 += v * cz; + d3_geo_centroidW1 += w; + d3_geo_centroidX1 += w * (x0 + (x0 = x)); + d3_geo_centroidY1 += w * (y0 + (y0 = y)); + d3_geo_centroidZ1 += w * (z0 + (z0 = z)); + d3_geo_centroidPointXYZ(x0, y0, z0); + } + } + function d3_geo_compose(a, b) { + function compose(x, y) { + return x = a(x, y), b(x[0], x[1]); + } + if (a.invert && b.invert) compose.invert = function(x, y) { + return x = b.invert(x, y), x && a.invert(x[0], x[1]); + }; + return compose; + } + function d3_true() { + return true; + } + function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) { + var subject = [], clip = []; + segments.forEach(function(segment) { + if ((n = segment.length - 1) <= 0) return; + var n, p0 = segment[0], p1 = segment[n]; + if (d3_geo_sphericalEqual(p0, p1)) { + listener.lineStart(); + for (var i = 0; i < n; ++i) listener.point((p0 = segment[i])[0], p0[1]); + listener.lineEnd(); + return; + } + var a = new d3_geo_clipPolygonIntersection(p0, segment, null, true), b = new d3_geo_clipPolygonIntersection(p0, null, a, false); + a.o = b; + subject.push(a); + clip.push(b); + a = new d3_geo_clipPolygonIntersection(p1, segment, null, false); + b = new d3_geo_clipPolygonIntersection(p1, null, a, true); + a.o = b; + subject.push(a); + clip.push(b); + }); + clip.sort(compare); + d3_geo_clipPolygonLinkCircular(subject); + d3_geo_clipPolygonLinkCircular(clip); + if (!subject.length) return; + for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) { + clip[i].e = entry = !entry; + } + var start = subject[0], points, point; + while (1) { + var current = start, isSubject = true; + while (current.v) if ((current = current.n) === start) return; + points = current.z; + listener.lineStart(); + do { + current.v = current.o.v = true; + if (current.e) { + if (isSubject) { + for (var i = 0, n = points.length; i < n; ++i) listener.point((point = points[i])[0], point[1]); + } else { + interpolate(current.x, current.n.x, 1, listener); + } + current = current.n; + } else { + if (isSubject) { + points = current.p.z; + for (var i = points.length - 1; i >= 0; --i) listener.point((point = points[i])[0], point[1]); + } else { + interpolate(current.x, current.p.x, -1, listener); + } + current = current.p; + } + current = current.o; + points = current.z; + isSubject = !isSubject; + } while (!current.v); + listener.lineEnd(); + } + } + function d3_geo_clipPolygonLinkCircular(array) { + if (!(n = array.length)) return; + var n, i = 0, a = array[0], b; + while (++i < n) { + a.n = b = array[i]; + b.p = a; + a = b; + } + a.n = b = array[0]; + b.p = a; + } + function d3_geo_clipPolygonIntersection(point, points, other, entry) { + this.x = point; + this.z = points; + this.o = other; + this.e = entry; + this.v = false; + this.n = this.p = null; + } + function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) { + return function(rotate, listener) { + var line = clipLine(listener), rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]); + var clip = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + clip.point = pointRing; + clip.lineStart = ringStart; + clip.lineEnd = ringEnd; + segments = []; + polygon = []; + }, + polygonEnd: function() { + clip.point = point; + clip.lineStart = lineStart; + clip.lineEnd = lineEnd; + segments = d3.merge(segments); + var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon); + if (segments.length) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener); + } else if (clipStartInside) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + } + if (polygonStarted) listener.polygonEnd(), polygonStarted = false; + segments = polygon = null; + }, + sphere: function() { + listener.polygonStart(); + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + listener.polygonEnd(); + } + }; + function point(λ, φ) { + var point = rotate(λ, φ); + if (pointVisible(λ = point[0], φ = point[1])) listener.point(λ, φ); + } + function pointLine(λ, φ) { + var point = rotate(λ, φ); + line.point(point[0], point[1]); + } + function lineStart() { + clip.point = pointLine; + line.lineStart(); + } + function lineEnd() { + clip.point = point; + line.lineEnd(); + } + var segments; + var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), polygonStarted = false, polygon, ring; + function pointRing(λ, φ) { + ring.push([ λ, φ ]); + var point = rotate(λ, φ); + ringListener.point(point[0], point[1]); + } + function ringStart() { + ringListener.lineStart(); + ring = []; + } + function ringEnd() { + pointRing(ring[0][0], ring[0][1]); + ringListener.lineEnd(); + var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length; + ring.pop(); + polygon.push(ring); + ring = null; + if (!n) return; + if (clean & 1) { + segment = ringSegments[0]; + var n = segment.length - 1, i = -1, point; + if (n > 0) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + listener.lineStart(); + while (++i < n) listener.point((point = segment[i])[0], point[1]); + listener.lineEnd(); + } + return; + } + if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); + segments.push(ringSegments.filter(d3_geo_clipSegmentLength1)); + } + return clip; + }; + } + function d3_geo_clipSegmentLength1(segment) { + return segment.length > 1; + } + function d3_geo_clipBufferListener() { + var lines = [], line; + return { + lineStart: function() { + lines.push(line = []); + }, + point: function(λ, φ) { + line.push([ λ, φ ]); + }, + lineEnd: d3_noop, + buffer: function() { + var buffer = lines; + lines = []; + line = null; + return buffer; + }, + rejoin: function() { + if (lines.length > 1) lines.push(lines.pop().concat(lines.shift())); + } + }; + } + function d3_geo_clipSort(a, b) { + return ((a = a.x)[0] < 0 ? a[1] - halfπ - ε : halfπ - a[1]) - ((b = b.x)[0] < 0 ? b[1] - halfπ - ε : halfπ - b[1]); + } + var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, -π / 2 ]); + function d3_geo_clipAntimeridianLine(listener) { + var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean; + return { + lineStart: function() { + listener.lineStart(); + clean = 1; + }, + point: function(λ1, φ1) { + var sλ1 = λ1 > 0 ? π : -π, dλ = abs(λ1 - λ0); + if (abs(dλ - π) < ε) { + listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? halfπ : -halfπ); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + listener.point(λ1, φ0); + clean = 0; + } else if (sλ0 !== sλ1 && dλ >= π) { + if (abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε; + if (abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε; + φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + clean = 0; + } + listener.point(λ0 = λ1, φ0 = φ1); + sλ0 = sλ1; + }, + lineEnd: function() { + listener.lineEnd(); + λ0 = φ0 = NaN; + }, + clean: function() { + return 2 - clean; + } + }; + } + function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) { + var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1); + return abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2; + } + function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) { + var φ; + if (from == null) { + φ = direction * halfπ; + listener.point(-π, φ); + listener.point(0, φ); + listener.point(π, φ); + listener.point(π, 0); + listener.point(π, -φ); + listener.point(0, -φ); + listener.point(-π, -φ); + listener.point(-π, 0); + listener.point(-π, φ); + } else if (abs(from[0] - to[0]) > ε) { + var s = from[0] < to[0] ? π : -π; + φ = direction * s / 2; + listener.point(-s, φ); + listener.point(0, φ); + listener.point(s, φ); + } else { + listener.point(to[0], to[1]); + } + } + function d3_geo_pointInPolygon(point, polygon) { + var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0; + d3_geo_areaRingSum.reset(); + for (var i = 0, n = polygon.length; i < n; ++i) { + var ring = polygon[i], m = ring.length; + if (!m) continue; + var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1; + while (true) { + if (j === m) j = 0; + point = ring[j]; + var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, antimeridian = adλ > π, k = sinφ0 * sinφ; + d3_geo_areaRingSum.add(Math.atan2(k * sdλ * Math.sin(adλ), cosφ0 * cosφ + k * Math.cos(adλ))); + polarAngle += antimeridian ? dλ + sdλ * τ : dλ; + if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) { + var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point)); + d3_geo_cartesianNormalize(arc); + var intersection = d3_geo_cartesianCross(meridianNormal, arc); + d3_geo_cartesianNormalize(intersection); + var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]); + if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) { + winding += antimeridian ^ dλ >= 0 ? 1 : -1; + } + } + if (!j++) break; + λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point; + } + } + return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < -ε) ^ winding & 1; + } + function d3_geo_clipCircle(radius) { + var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians); + return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]); + function visible(λ, φ) { + return Math.cos(λ) * Math.cos(φ) > cr; + } + function clipLine(listener) { + var point0, c0, v0, v00, clean; + return { + lineStart: function() { + v00 = v0 = false; + clean = 1; + }, + point: function(λ, φ) { + var point1 = [ λ, φ ], point2, v = visible(λ, φ), c = smallRadius ? v ? 0 : code(λ, φ) : v ? code(λ + (λ < 0 ? π : -π), φ) : 0; + if (!point0 && (v00 = v0 = v)) listener.lineStart(); + if (v !== v0) { + point2 = intersect(point0, point1); + if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) { + point1[0] += ε; + point1[1] += ε; + v = visible(point1[0], point1[1]); + } + } + if (v !== v0) { + clean = 0; + if (v) { + listener.lineStart(); + point2 = intersect(point1, point0); + listener.point(point2[0], point2[1]); + } else { + point2 = intersect(point0, point1); + listener.point(point2[0], point2[1]); + listener.lineEnd(); + } + point0 = point2; + } else if (notHemisphere && point0 && smallRadius ^ v) { + var t; + if (!(c & c0) && (t = intersect(point1, point0, true))) { + clean = 0; + if (smallRadius) { + listener.lineStart(); + listener.point(t[0][0], t[0][1]); + listener.point(t[1][0], t[1][1]); + listener.lineEnd(); + } else { + listener.point(t[1][0], t[1][1]); + listener.lineEnd(); + listener.lineStart(); + listener.point(t[0][0], t[0][1]); + } + } + } + if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) { + listener.point(point1[0], point1[1]); + } + point0 = point1, v0 = v, c0 = c; + }, + lineEnd: function() { + if (v0) listener.lineEnd(); + point0 = null; + }, + clean: function() { + return clean | (v00 && v0) << 1; + } + }; + } + function intersect(a, b, two) { + var pa = d3_geo_cartesian(a), pb = d3_geo_cartesian(b); + var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2; + if (!determinant) return !two && a; + var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2); + d3_geo_cartesianAdd(A, B); + var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1); + if (t2 < 0) return; + var t = Math.sqrt(t2), q = d3_geo_cartesianScale(u, (-w - t) / uu); + d3_geo_cartesianAdd(q, A); + q = d3_geo_spherical(q); + if (!two) return q; + var λ0 = a[0], λ1 = b[0], φ0 = a[1], φ1 = b[1], z; + if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z; + var δλ = λ1 - λ0, polar = abs(δλ - π) < ε, meridian = polar || δλ < ε; + if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z; + if (meridian ? polar ? φ0 + φ1 > 0 ^ q[1] < (abs(q[0] - λ0) < ε ? φ0 : φ1) : φ0 <= q[1] && q[1] <= φ1 : δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) { + var q1 = d3_geo_cartesianScale(u, (-w + t) / uu); + d3_geo_cartesianAdd(q1, A); + return [ q, d3_geo_spherical(q1) ]; + } + } + function code(λ, φ) { + var r = smallRadius ? radius : π - radius, code = 0; + if (λ < -r) code |= 1; else if (λ > r) code |= 2; + if (φ < -r) code |= 4; else if (φ > r) code |= 8; + return code; + } + } + function d3_geom_clipLine(x0, y0, x1, y1) { + return function(line) { + var a = line.a, b = line.b, ax = a.x, ay = a.y, bx = b.x, by = b.y, t0 = 0, t1 = 1, dx = bx - ax, dy = by - ay, r; + r = x0 - ax; + if (!dx && r > 0) return; + r /= dx; + if (dx < 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } else if (dx > 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } + r = x1 - ax; + if (!dx && r < 0) return; + r /= dx; + if (dx < 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } else if (dx > 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } + r = y0 - ay; + if (!dy && r > 0) return; + r /= dy; + if (dy < 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } else if (dy > 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } + r = y1 - ay; + if (!dy && r < 0) return; + r /= dy; + if (dy < 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } else if (dy > 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } + if (t0 > 0) line.a = { + x: ax + t0 * dx, + y: ay + t0 * dy + }; + if (t1 < 1) line.b = { + x: ax + t1 * dx, + y: ay + t1 * dy + }; + return line; + }; + } + var d3_geo_clipExtentMAX = 1e9; + d3.geo.clipExtent = function() { + var x0, y0, x1, y1, stream, clip, clipExtent = { + stream: function(output) { + if (stream) stream.valid = false; + stream = clip(output); + stream.valid = true; + return stream; + }, + extent: function(_) { + if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ]; + clip = d3_geo_clipExtent(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]); + if (stream) stream.valid = false, stream = null; + return clipExtent; + } + }; + return clipExtent.extent([ [ 0, 0 ], [ 960, 500 ] ]); + }; + function d3_geo_clipExtent(x0, y0, x1, y1) { + return function(listener) { + var listener_ = listener, bufferListener = d3_geo_clipBufferListener(), clipLine = d3_geom_clipLine(x0, y0, x1, y1), segments, polygon, ring; + var clip = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + listener = bufferListener; + segments = []; + polygon = []; + clean = true; + }, + polygonEnd: function() { + listener = listener_; + segments = d3.merge(segments); + var clipStartInside = insidePolygon([ x0, y1 ]), inside = clean && clipStartInside, visible = segments.length; + if (inside || visible) { + listener.polygonStart(); + if (inside) { + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + } + if (visible) { + d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener); + } + listener.polygonEnd(); + } + segments = polygon = ring = null; + } + }; + function insidePolygon(p) { + var wn = 0, n = polygon.length, y = p[1]; + for (var i = 0; i < n; ++i) { + for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) { + b = v[j]; + if (a[1] <= y) { + if (b[1] > y && d3_cross2d(a, b, p) > 0) ++wn; + } else { + if (b[1] <= y && d3_cross2d(a, b, p) < 0) --wn; + } + a = b; + } + } + return wn !== 0; + } + function interpolate(from, to, direction, listener) { + var a = 0, a1 = 0; + if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) { + do { + listener.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0); + } while ((a = (a + direction + 4) % 4) !== a1); + } else { + listener.point(to[0], to[1]); + } + } + function pointVisible(x, y) { + return x0 <= x && x <= x1 && y0 <= y && y <= y1; + } + function point(x, y) { + if (pointVisible(x, y)) listener.point(x, y); + } + var x__, y__, v__, x_, y_, v_, first, clean; + function lineStart() { + clip.point = linePoint; + if (polygon) polygon.push(ring = []); + first = true; + v_ = false; + x_ = y_ = NaN; + } + function lineEnd() { + if (segments) { + linePoint(x__, y__); + if (v__ && v_) bufferListener.rejoin(); + segments.push(bufferListener.buffer()); + } + clip.point = point; + if (v_) listener.lineEnd(); + } + function linePoint(x, y) { + x = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, x)); + y = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, y)); + var v = pointVisible(x, y); + if (polygon) ring.push([ x, y ]); + if (first) { + x__ = x, y__ = y, v__ = v; + first = false; + if (v) { + listener.lineStart(); + listener.point(x, y); + } + } else { + if (v && v_) listener.point(x, y); else { + var l = { + a: { + x: x_, + y: y_ + }, + b: { + x: x, + y: y + } + }; + if (clipLine(l)) { + if (!v_) { + listener.lineStart(); + listener.point(l.a.x, l.a.y); + } + listener.point(l.b.x, l.b.y); + if (!v) listener.lineEnd(); + clean = false; + } else if (v) { + listener.lineStart(); + listener.point(x, y); + clean = false; + } + } + } + x_ = x, y_ = y, v_ = v; + } + return clip; + }; + function corner(p, direction) { + return abs(p[0] - x0) < ε ? direction > 0 ? 0 : 3 : abs(p[0] - x1) < ε ? direction > 0 ? 2 : 1 : abs(p[1] - y0) < ε ? direction > 0 ? 1 : 0 : direction > 0 ? 3 : 2; + } + function compare(a, b) { + return comparePoints(a.x, b.x); + } + function comparePoints(a, b) { + var ca = corner(a, 1), cb = corner(b, 1); + return ca !== cb ? ca - cb : ca === 0 ? b[1] - a[1] : ca === 1 ? a[0] - b[0] : ca === 2 ? a[1] - b[1] : b[0] - a[0]; + } + } + function d3_geo_conic(projectAt) { + var φ0 = 0, φ1 = π / 3, m = d3_geo_projectionMutator(projectAt), p = m(φ0, φ1); + p.parallels = function(_) { + if (!arguments.length) return [ φ0 / π * 180, φ1 / π * 180 ]; + return m(φ0 = _[0] * π / 180, φ1 = _[1] * π / 180); + }; + return p; + } + function d3_geo_conicEqualArea(φ0, φ1) { + var sinφ0 = Math.sin(φ0), n = (sinφ0 + Math.sin(φ1)) / 2, C = 1 + sinφ0 * (2 * n - sinφ0), ρ0 = Math.sqrt(C) / n; + function forward(λ, φ) { + var ρ = Math.sqrt(C - 2 * n * Math.sin(φ)) / n; + return [ ρ * Math.sin(λ *= n), ρ0 - ρ * Math.cos(λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = ρ0 - y; + return [ Math.atan2(x, ρ0_y) / n, d3_asin((C - (x * x + ρ0_y * ρ0_y) * n * n) / (2 * n)) ]; + }; + return forward; + } + (d3.geo.conicEqualArea = function() { + return d3_geo_conic(d3_geo_conicEqualArea); + }).raw = d3_geo_conicEqualArea; + d3.geo.albers = function() { + return d3.geo.conicEqualArea().rotate([ 96, 0 ]).center([ -.6, 38.7 ]).parallels([ 29.5, 45.5 ]).scale(1070); + }; + d3.geo.albersUsa = function() { + var lower48 = d3.geo.albers(); + var alaska = d3.geo.conicEqualArea().rotate([ 154, 0 ]).center([ -2, 58.5 ]).parallels([ 55, 65 ]); + var hawaii = d3.geo.conicEqualArea().rotate([ 157, 0 ]).center([ -3, 19.9 ]).parallels([ 8, 18 ]); + var point, pointStream = { + point: function(x, y) { + point = [ x, y ]; + } + }, lower48Point, alaskaPoint, hawaiiPoint; + function albersUsa(coordinates) { + var x = coordinates[0], y = coordinates[1]; + point = null; + (lower48Point(x, y), point) || (alaskaPoint(x, y), point) || hawaiiPoint(x, y); + return point; + } + albersUsa.invert = function(coordinates) { + var k = lower48.scale(), t = lower48.translate(), x = (coordinates[0] - t[0]) / k, y = (coordinates[1] - t[1]) / k; + return (y >= .12 && y < .234 && x >= -.425 && x < -.214 ? alaska : y >= .166 && y < .234 && x >= -.214 && x < -.115 ? hawaii : lower48).invert(coordinates); + }; + albersUsa.stream = function(stream) { + var lower48Stream = lower48.stream(stream), alaskaStream = alaska.stream(stream), hawaiiStream = hawaii.stream(stream); + return { + point: function(x, y) { + lower48Stream.point(x, y); + alaskaStream.point(x, y); + hawaiiStream.point(x, y); + }, + sphere: function() { + lower48Stream.sphere(); + alaskaStream.sphere(); + hawaiiStream.sphere(); + }, + lineStart: function() { + lower48Stream.lineStart(); + alaskaStream.lineStart(); + hawaiiStream.lineStart(); + }, + lineEnd: function() { + lower48Stream.lineEnd(); + alaskaStream.lineEnd(); + hawaiiStream.lineEnd(); + }, + polygonStart: function() { + lower48Stream.polygonStart(); + alaskaStream.polygonStart(); + hawaiiStream.polygonStart(); + }, + polygonEnd: function() { + lower48Stream.polygonEnd(); + alaskaStream.polygonEnd(); + hawaiiStream.polygonEnd(); + } + }; + }; + albersUsa.precision = function(_) { + if (!arguments.length) return lower48.precision(); + lower48.precision(_); + alaska.precision(_); + hawaii.precision(_); + return albersUsa; + }; + albersUsa.scale = function(_) { + if (!arguments.length) return lower48.scale(); + lower48.scale(_); + alaska.scale(_ * .35); + hawaii.scale(_); + return albersUsa.translate(lower48.translate()); + }; + albersUsa.translate = function(_) { + if (!arguments.length) return lower48.translate(); + var k = lower48.scale(), x = +_[0], y = +_[1]; + lower48Point = lower48.translate(_).clipExtent([ [ x - .455 * k, y - .238 * k ], [ x + .455 * k, y + .238 * k ] ]).stream(pointStream).point; + alaskaPoint = alaska.translate([ x - .307 * k, y + .201 * k ]).clipExtent([ [ x - .425 * k + ε, y + .12 * k + ε ], [ x - .214 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point; + hawaiiPoint = hawaii.translate([ x - .205 * k, y + .212 * k ]).clipExtent([ [ x - .214 * k + ε, y + .166 * k + ε ], [ x - .115 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point; + return albersUsa; + }; + return albersUsa.scale(1070); + }; + var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = { + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_pathAreaPolygon = 0; + d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart; + }, + polygonEnd: function() { + d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop; + d3_geo_pathAreaSum += abs(d3_geo_pathAreaPolygon / 2); + } + }; + function d3_geo_pathAreaRingStart() { + var x00, y00, x0, y0; + d3_geo_pathArea.point = function(x, y) { + d3_geo_pathArea.point = nextPoint; + x00 = x0 = x, y00 = y0 = y; + }; + function nextPoint(x, y) { + d3_geo_pathAreaPolygon += y0 * x - x0 * y; + x0 = x, y0 = y; + } + d3_geo_pathArea.lineEnd = function() { + nextPoint(x00, y00); + }; + } + var d3_geo_pathBoundsX0, d3_geo_pathBoundsY0, d3_geo_pathBoundsX1, d3_geo_pathBoundsY1; + var d3_geo_pathBounds = { + point: d3_geo_pathBoundsPoint, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: d3_noop, + polygonEnd: d3_noop + }; + function d3_geo_pathBoundsPoint(x, y) { + if (x < d3_geo_pathBoundsX0) d3_geo_pathBoundsX0 = x; + if (x > d3_geo_pathBoundsX1) d3_geo_pathBoundsX1 = x; + if (y < d3_geo_pathBoundsY0) d3_geo_pathBoundsY0 = y; + if (y > d3_geo_pathBoundsY1) d3_geo_pathBoundsY1 = y; + } + function d3_geo_pathBuffer() { + var pointCircle = d3_geo_pathBufferCircle(4.5), buffer = []; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointCircle = d3_geo_pathBufferCircle(_); + return stream; + }, + result: function() { + if (buffer.length) { + var result = buffer.join(""); + buffer = []; + return result; + } + } + }; + function point(x, y) { + buffer.push("M", x, ",", y, pointCircle); + } + function pointLineStart(x, y) { + buffer.push("M", x, ",", y); + stream.point = pointLine; + } + function pointLine(x, y) { + buffer.push("L", x, ",", y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + buffer.push("Z"); + } + return stream; + } + function d3_geo_pathBufferCircle(radius) { + return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius + "z"; + } + var d3_geo_pathCentroid = { + point: d3_geo_pathCentroidPoint, + lineStart: d3_geo_pathCentroidLineStart, + lineEnd: d3_geo_pathCentroidLineEnd, + polygonStart: function() { + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart; + }, + polygonEnd: function() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart; + d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd; + } + }; + function d3_geo_pathCentroidPoint(x, y) { + d3_geo_centroidX0 += x; + d3_geo_centroidY0 += y; + ++d3_geo_centroidZ0; + } + function d3_geo_pathCentroidLineStart() { + var x0, y0; + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + }; + function nextPoint(x, y) { + var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); + d3_geo_centroidX1 += z * (x0 + x) / 2; + d3_geo_centroidY1 += z * (y0 + y) / 2; + d3_geo_centroidZ1 += z; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + } + } + function d3_geo_pathCentroidLineEnd() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + } + function d3_geo_pathCentroidRingStart() { + var x00, y00, x0, y0; + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + d3_geo_pathCentroidPoint(x00 = x0 = x, y00 = y0 = y); + }; + function nextPoint(x, y) { + var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); + d3_geo_centroidX1 += z * (x0 + x) / 2; + d3_geo_centroidY1 += z * (y0 + y) / 2; + d3_geo_centroidZ1 += z; + z = y0 * x - x0 * y; + d3_geo_centroidX2 += z * (x0 + x); + d3_geo_centroidY2 += z * (y0 + y); + d3_geo_centroidZ2 += z * 3; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + } + d3_geo_pathCentroid.lineEnd = function() { + nextPoint(x00, y00); + }; + } + function d3_geo_pathContext(context) { + var pointRadius = 4.5; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointRadius = _; + return stream; + }, + result: d3_noop + }; + function point(x, y) { + context.moveTo(x + pointRadius, y); + context.arc(x, y, pointRadius, 0, τ); + } + function pointLineStart(x, y) { + context.moveTo(x, y); + stream.point = pointLine; + } + function pointLine(x, y) { + context.lineTo(x, y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + context.closePath(); + } + return stream; + } + function d3_geo_resample(project) { + var δ2 = .5, cosMinDistance = Math.cos(30 * d3_radians), maxDepth = 16; + function resample(stream) { + return (maxDepth ? resampleRecursive : resampleNone)(stream); + } + function resampleNone(stream) { + return d3_geo_transformPoint(stream, function(x, y) { + x = project(x, y); + stream.point(x[0], x[1]); + }); + } + function resampleRecursive(stream) { + var λ00, φ00, x00, y00, a00, b00, c00, λ0, x0, y0, a0, b0, c0; + var resample = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + stream.polygonStart(); + resample.lineStart = ringStart; + }, + polygonEnd: function() { + stream.polygonEnd(); + resample.lineStart = lineStart; + } + }; + function point(x, y) { + x = project(x, y); + stream.point(x[0], x[1]); + } + function lineStart() { + x0 = NaN; + resample.point = linePoint; + stream.lineStart(); + } + function linePoint(λ, φ) { + var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ); + resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream); + stream.point(x0, y0); + } + function lineEnd() { + resample.point = point; + stream.lineEnd(); + } + function ringStart() { + lineStart(); + resample.point = ringPoint; + resample.lineEnd = ringEnd; + } + function ringPoint(λ, φ) { + linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0; + resample.point = linePoint; + } + function ringEnd() { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream); + resample.lineEnd = lineEnd; + lineEnd(); + } + return resample; + } + function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) { + var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy; + if (d2 > 4 * δ2 && depth--) { + var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = abs(abs(c) - 1) < ε || abs(λ0 - λ1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2; + if (dz * dz / d2 > δ2 || abs((dx * dx2 + dy * dy2) / d2 - .5) > .3 || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream); + stream.point(x2, y2); + resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream); + } + } + } + resample.precision = function(_) { + if (!arguments.length) return Math.sqrt(δ2); + maxDepth = (δ2 = _ * _) > 0 && 16; + return resample; + }; + return resample; + } + d3.geo.path = function() { + var pointRadius = 4.5, projection, context, projectStream, contextStream, cacheStream; + function path(object) { + if (object) { + if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments)); + if (!cacheStream || !cacheStream.valid) cacheStream = projectStream(contextStream); + d3.geo.stream(object, cacheStream); + } + return contextStream.result(); + } + path.area = function(object) { + d3_geo_pathAreaSum = 0; + d3.geo.stream(object, projectStream(d3_geo_pathArea)); + return d3_geo_pathAreaSum; + }; + path.centroid = function(object) { + d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0; + d3.geo.stream(object, projectStream(d3_geo_pathCentroid)); + return d3_geo_centroidZ2 ? [ d3_geo_centroidX2 / d3_geo_centroidZ2, d3_geo_centroidY2 / d3_geo_centroidZ2 ] : d3_geo_centroidZ1 ? [ d3_geo_centroidX1 / d3_geo_centroidZ1, d3_geo_centroidY1 / d3_geo_centroidZ1 ] : d3_geo_centroidZ0 ? [ d3_geo_centroidX0 / d3_geo_centroidZ0, d3_geo_centroidY0 / d3_geo_centroidZ0 ] : [ NaN, NaN ]; + }; + path.bounds = function(object) { + d3_geo_pathBoundsX1 = d3_geo_pathBoundsY1 = -(d3_geo_pathBoundsX0 = d3_geo_pathBoundsY0 = Infinity); + d3.geo.stream(object, projectStream(d3_geo_pathBounds)); + return [ [ d3_geo_pathBoundsX0, d3_geo_pathBoundsY0 ], [ d3_geo_pathBoundsX1, d3_geo_pathBoundsY1 ] ]; + }; + path.projection = function(_) { + if (!arguments.length) return projection; + projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity; + return reset(); + }; + path.context = function(_) { + if (!arguments.length) return context; + contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_); + if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius); + return reset(); + }; + path.pointRadius = function(_) { + if (!arguments.length) return pointRadius; + pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_); + return path; + }; + function reset() { + cacheStream = null; + return path; + } + return path.projection(d3.geo.albersUsa()).context(null); + }; + function d3_geo_pathProjectStream(project) { + var resample = d3_geo_resample(function(x, y) { + return project([ x * d3_degrees, y * d3_degrees ]); + }); + return function(stream) { + return d3_geo_projectionRadians(resample(stream)); + }; + } + d3.geo.transform = function(methods) { + return { + stream: function(stream) { + var transform = new d3_geo_transform(stream); + for (var k in methods) transform[k] = methods[k]; + return transform; + } + }; + }; + function d3_geo_transform(stream) { + this.stream = stream; + } + d3_geo_transform.prototype = { + point: function(x, y) { + this.stream.point(x, y); + }, + sphere: function() { + this.stream.sphere(); + }, + lineStart: function() { + this.stream.lineStart(); + }, + lineEnd: function() { + this.stream.lineEnd(); + }, + polygonStart: function() { + this.stream.polygonStart(); + }, + polygonEnd: function() { + this.stream.polygonEnd(); + } + }; + function d3_geo_transformPoint(stream, point) { + return { + point: point, + sphere: function() { + stream.sphere(); + }, + lineStart: function() { + stream.lineStart(); + }, + lineEnd: function() { + stream.lineEnd(); + }, + polygonStart: function() { + stream.polygonStart(); + }, + polygonEnd: function() { + stream.polygonEnd(); + } + }; + } + d3.geo.projection = d3_geo_projection; + d3.geo.projectionMutator = d3_geo_projectionMutator; + function d3_geo_projection(project) { + return d3_geo_projectionMutator(function() { + return project; + })(); + } + function d3_geo_projectionMutator(projectAt) { + var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) { + x = project(x, y); + return [ x[0] * k + δx, δy - x[1] * k ]; + }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, preclip = d3_geo_clipAntimeridian, postclip = d3_identity, clipAngle = null, clipExtent = null, stream; + function projection(point) { + point = projectRotate(point[0] * d3_radians, point[1] * d3_radians); + return [ point[0] * k + δx, δy - point[1] * k ]; + } + function invert(point) { + point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k); + return point && [ point[0] * d3_degrees, point[1] * d3_degrees ]; + } + projection.stream = function(output) { + if (stream) stream.valid = false; + stream = d3_geo_projectionRadians(preclip(rotate, projectResample(postclip(output)))); + stream.valid = true; + return stream; + }; + projection.clipAngle = function(_) { + if (!arguments.length) return clipAngle; + preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians); + return invalidate(); + }; + projection.clipExtent = function(_) { + if (!arguments.length) return clipExtent; + clipExtent = _; + postclip = _ ? d3_geo_clipExtent(_[0][0], _[0][1], _[1][0], _[1][1]) : d3_identity; + return invalidate(); + }; + projection.scale = function(_) { + if (!arguments.length) return k; + k = +_; + return reset(); + }; + projection.translate = function(_) { + if (!arguments.length) return [ x, y ]; + x = +_[0]; + y = +_[1]; + return reset(); + }; + projection.center = function(_) { + if (!arguments.length) return [ λ * d3_degrees, φ * d3_degrees ]; + λ = _[0] % 360 * d3_radians; + φ = _[1] % 360 * d3_radians; + return reset(); + }; + projection.rotate = function(_) { + if (!arguments.length) return [ δλ * d3_degrees, δφ * d3_degrees, δγ * d3_degrees ]; + δλ = _[0] % 360 * d3_radians; + δφ = _[1] % 360 * d3_radians; + δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0; + return reset(); + }; + d3.rebind(projection, projectResample, "precision"); + function reset() { + projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project); + var center = project(λ, φ); + δx = x - center[0] * k; + δy = y + center[1] * k; + return invalidate(); + } + function invalidate() { + if (stream) stream.valid = false, stream = null; + return projection; + } + return function() { + project = projectAt.apply(this, arguments); + projection.invert = project.invert && invert; + return reset(); + }; + } + function d3_geo_projectionRadians(stream) { + return d3_geo_transformPoint(stream, function(x, y) { + stream.point(x * d3_radians, y * d3_radians); + }); + } + function d3_geo_equirectangular(λ, φ) { + return [ λ, φ ]; + } + (d3.geo.equirectangular = function() { + return d3_geo_projection(d3_geo_equirectangular); + }).raw = d3_geo_equirectangular.invert = d3_geo_equirectangular; + d3.geo.rotation = function(rotate) { + rotate = d3_geo_rotation(rotate[0] % 360 * d3_radians, rotate[1] * d3_radians, rotate.length > 2 ? rotate[2] * d3_radians : 0); + function forward(coordinates) { + coordinates = rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians); + return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates; + } + forward.invert = function(coordinates) { + coordinates = rotate.invert(coordinates[0] * d3_radians, coordinates[1] * d3_radians); + return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates; + }; + return forward; + }; + function d3_geo_identityRotation(λ, φ) { + return [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ]; + } + d3_geo_identityRotation.invert = d3_geo_equirectangular; + function d3_geo_rotation(δλ, δφ, δγ) { + return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation; + } + function d3_geo_forwardRotationλ(δλ) { + return function(λ, φ) { + return λ += δλ, [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ]; + }; + } + function d3_geo_rotationλ(δλ) { + var rotation = d3_geo_forwardRotationλ(δλ); + rotation.invert = d3_geo_forwardRotationλ(-δλ); + return rotation; + } + function d3_geo_rotationφγ(δφ, δγ) { + var cosδφ = Math.cos(δφ), sinδφ = Math.sin(δφ), cosδγ = Math.cos(δγ), sinδγ = Math.sin(δγ); + function rotation(λ, φ) { + var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδφ + x * sinδφ; + return [ Math.atan2(y * cosδγ - k * sinδγ, x * cosδφ - z * sinδφ), d3_asin(k * cosδγ + y * sinδγ) ]; + } + rotation.invert = function(λ, φ) { + var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδγ - y * sinδγ; + return [ Math.atan2(y * cosδγ + z * sinδγ, x * cosδφ + k * sinδφ), d3_asin(k * cosδφ - x * sinδφ) ]; + }; + return rotation; + } + d3.geo.circle = function() { + var origin = [ 0, 0 ], angle, precision = 6, interpolate; + function circle() { + var center = typeof origin === "function" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = []; + interpolate(null, null, 1, { + point: function(x, y) { + ring.push(x = rotate(x, y)); + x[0] *= d3_degrees, x[1] *= d3_degrees; + } + }); + return { + type: "Polygon", + coordinates: [ ring ] + }; + } + circle.origin = function(x) { + if (!arguments.length) return origin; + origin = x; + return circle; + }; + circle.angle = function(x) { + if (!arguments.length) return angle; + interpolate = d3_geo_circleInterpolate((angle = +x) * d3_radians, precision * d3_radians); + return circle; + }; + circle.precision = function(_) { + if (!arguments.length) return precision; + interpolate = d3_geo_circleInterpolate(angle * d3_radians, (precision = +_) * d3_radians); + return circle; + }; + return circle.angle(90); + }; + function d3_geo_circleInterpolate(radius, precision) { + var cr = Math.cos(radius), sr = Math.sin(radius); + return function(from, to, direction, listener) { + var step = direction * precision; + if (from != null) { + from = d3_geo_circleAngle(cr, from); + to = d3_geo_circleAngle(cr, to); + if (direction > 0 ? from < to : from > to) from += direction * τ; + } else { + from = radius + direction * τ; + to = radius - .5 * step; + } + for (var point, t = from; direction > 0 ? t > to : t < to; t -= step) { + listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]); + } + }; + } + function d3_geo_circleAngle(cr, point) { + var a = d3_geo_cartesian(point); + a[0] -= cr; + d3_geo_cartesianNormalize(a); + var angle = d3_acos(-a[1]); + return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI); + } + d3.geo.distance = function(a, b) { + var Δλ = (b[0] - a[0]) * d3_radians, φ0 = a[1] * d3_radians, φ1 = b[1] * d3_radians, sinΔλ = Math.sin(Δλ), cosΔλ = Math.cos(Δλ), sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1), t; + return Math.atan2(Math.sqrt((t = cosφ1 * sinΔλ) * t + (t = cosφ0 * sinφ1 - sinφ0 * cosφ1 * cosΔλ) * t), sinφ0 * sinφ1 + cosφ0 * cosφ1 * cosΔλ); + }; + d3.geo.graticule = function() { + var x1, x0, X1, X0, y1, y0, Y1, Y0, dx = 10, dy = dx, DX = 90, DY = 360, x, y, X, Y, precision = 2.5; + function graticule() { + return { + type: "MultiLineString", + coordinates: lines() + }; + } + function lines() { + return d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X).concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y)).concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) { + return abs(x % DX) > ε; + }).map(x)).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).filter(function(y) { + return abs(y % DY) > ε; + }).map(y)); + } + graticule.lines = function() { + return lines().map(function(coordinates) { + return { + type: "LineString", + coordinates: coordinates + }; + }); + }; + graticule.outline = function() { + return { + type: "Polygon", + coordinates: [ X(X0).concat(Y(Y1).slice(1), X(X1).reverse().slice(1), Y(Y0).reverse().slice(1)) ] + }; + }; + graticule.extent = function(_) { + if (!arguments.length) return graticule.minorExtent(); + return graticule.majorExtent(_).minorExtent(_); + }; + graticule.majorExtent = function(_) { + if (!arguments.length) return [ [ X0, Y0 ], [ X1, Y1 ] ]; + X0 = +_[0][0], X1 = +_[1][0]; + Y0 = +_[0][1], Y1 = +_[1][1]; + if (X0 > X1) _ = X0, X0 = X1, X1 = _; + if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _; + return graticule.precision(precision); + }; + graticule.minorExtent = function(_) { + if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ]; + x0 = +_[0][0], x1 = +_[1][0]; + y0 = +_[0][1], y1 = +_[1][1]; + if (x0 > x1) _ = x0, x0 = x1, x1 = _; + if (y0 > y1) _ = y0, y0 = y1, y1 = _; + return graticule.precision(precision); + }; + graticule.step = function(_) { + if (!arguments.length) return graticule.minorStep(); + return graticule.majorStep(_).minorStep(_); + }; + graticule.majorStep = function(_) { + if (!arguments.length) return [ DX, DY ]; + DX = +_[0], DY = +_[1]; + return graticule; + }; + graticule.minorStep = function(_) { + if (!arguments.length) return [ dx, dy ]; + dx = +_[0], dy = +_[1]; + return graticule; + }; + graticule.precision = function(_) { + if (!arguments.length) return precision; + precision = +_; + x = d3_geo_graticuleX(y0, y1, 90); + y = d3_geo_graticuleY(x0, x1, precision); + X = d3_geo_graticuleX(Y0, Y1, 90); + Y = d3_geo_graticuleY(X0, X1, precision); + return graticule; + }; + return graticule.majorExtent([ [ -180, -90 + ε ], [ 180, 90 - ε ] ]).minorExtent([ [ -180, -80 - ε ], [ 180, 80 + ε ] ]); + }; + function d3_geo_graticuleX(y0, y1, dy) { + var y = d3.range(y0, y1 - ε, dy).concat(y1); + return function(x) { + return y.map(function(y) { + return [ x, y ]; + }); + }; + } + function d3_geo_graticuleY(x0, x1, dx) { + var x = d3.range(x0, x1 - ε, dx).concat(x1); + return function(y) { + return x.map(function(x) { + return [ x, y ]; + }); + }; + } + function d3_source(d) { + return d.source; + } + function d3_target(d) { + return d.target; + } + d3.geo.greatArc = function() { + var source = d3_source, source_, target = d3_target, target_; + function greatArc() { + return { + type: "LineString", + coordinates: [ source_ || source.apply(this, arguments), target_ || target.apply(this, arguments) ] + }; + } + greatArc.distance = function() { + return d3.geo.distance(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments)); + }; + greatArc.source = function(_) { + if (!arguments.length) return source; + source = _, source_ = typeof _ === "function" ? null : _; + return greatArc; + }; + greatArc.target = function(_) { + if (!arguments.length) return target; + target = _, target_ = typeof _ === "function" ? null : _; + return greatArc; + }; + greatArc.precision = function() { + return arguments.length ? greatArc : 0; + }; + return greatArc; + }; + d3.geo.interpolate = function(source, target) { + return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians); + }; + function d3_geo_interpolate(x0, y0, x1, y1) { + var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = 2 * Math.asin(Math.sqrt(d3_haversin(y1 - y0) + cy0 * cy1 * d3_haversin(x1 - x0))), k = 1 / Math.sin(d); + var interpolate = d ? function(t) { + var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1; + return [ Math.atan2(y, x) * d3_degrees, Math.atan2(z, Math.sqrt(x * x + y * y)) * d3_degrees ]; + } : function() { + return [ x0 * d3_degrees, y0 * d3_degrees ]; + }; + interpolate.distance = d; + return interpolate; + } + d3.geo.length = function(object) { + d3_geo_lengthSum = 0; + d3.geo.stream(object, d3_geo_length); + return d3_geo_lengthSum; + }; + var d3_geo_lengthSum; + var d3_geo_length = { + sphere: d3_noop, + point: d3_noop, + lineStart: d3_geo_lengthLineStart, + lineEnd: d3_noop, + polygonStart: d3_noop, + polygonEnd: d3_noop + }; + function d3_geo_lengthLineStart() { + var λ0, sinφ0, cosφ0; + d3_geo_length.point = function(λ, φ) { + λ0 = λ * d3_radians, sinφ0 = Math.sin(φ *= d3_radians), cosφ0 = Math.cos(φ); + d3_geo_length.point = nextPoint; + }; + d3_geo_length.lineEnd = function() { + d3_geo_length.point = d3_geo_length.lineEnd = d3_noop; + }; + function nextPoint(λ, φ) { + var sinφ = Math.sin(φ *= d3_radians), cosφ = Math.cos(φ), t = abs((λ *= d3_radians) - λ0), cosΔλ = Math.cos(t); + d3_geo_lengthSum += Math.atan2(Math.sqrt((t = cosφ * Math.sin(t)) * t + (t = cosφ0 * sinφ - sinφ0 * cosφ * cosΔλ) * t), sinφ0 * sinφ + cosφ0 * cosφ * cosΔλ); + λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ; + } + } + function d3_geo_azimuthal(scale, angle) { + function azimuthal(λ, φ) { + var cosλ = Math.cos(λ), cosφ = Math.cos(φ), k = scale(cosλ * cosφ); + return [ k * cosφ * Math.sin(λ), k * Math.sin(φ) ]; + } + azimuthal.invert = function(x, y) { + var ρ = Math.sqrt(x * x + y * y), c = angle(ρ), sinc = Math.sin(c), cosc = Math.cos(c); + return [ Math.atan2(x * sinc, ρ * cosc), Math.asin(ρ && y * sinc / ρ) ]; + }; + return azimuthal; + } + var d3_geo_azimuthalEqualArea = d3_geo_azimuthal(function(cosλcosφ) { + return Math.sqrt(2 / (1 + cosλcosφ)); + }, function(ρ) { + return 2 * Math.asin(ρ / 2); + }); + (d3.geo.azimuthalEqualArea = function() { + return d3_geo_projection(d3_geo_azimuthalEqualArea); + }).raw = d3_geo_azimuthalEqualArea; + var d3_geo_azimuthalEquidistant = d3_geo_azimuthal(function(cosλcosφ) { + var c = Math.acos(cosλcosφ); + return c && c / Math.sin(c); + }, d3_identity); + (d3.geo.azimuthalEquidistant = function() { + return d3_geo_projection(d3_geo_azimuthalEquidistant); + }).raw = d3_geo_azimuthalEquidistant; + function d3_geo_conicConformal(φ0, φ1) { + var cosφ0 = Math.cos(φ0), t = function(φ) { + return Math.tan(π / 4 + φ / 2); + }, n = φ0 === φ1 ? Math.sin(φ0) : Math.log(cosφ0 / Math.cos(φ1)) / Math.log(t(φ1) / t(φ0)), F = cosφ0 * Math.pow(t(φ0), n) / n; + if (!n) return d3_geo_mercator; + function forward(λ, φ) { + if (F > 0) { + if (φ < -halfπ + ε) φ = -halfπ + ε; + } else { + if (φ > halfπ - ε) φ = halfπ - ε; + } + var ρ = F / Math.pow(t(φ), n); + return [ ρ * Math.sin(n * λ), F - ρ * Math.cos(n * λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = F - y, ρ = d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y); + return [ Math.atan2(x, ρ0_y) / n, 2 * Math.atan(Math.pow(F / ρ, 1 / n)) - halfπ ]; + }; + return forward; + } + (d3.geo.conicConformal = function() { + return d3_geo_conic(d3_geo_conicConformal); + }).raw = d3_geo_conicConformal; + function d3_geo_conicEquidistant(φ0, φ1) { + var cosφ0 = Math.cos(φ0), n = φ0 === φ1 ? Math.sin(φ0) : (cosφ0 - Math.cos(φ1)) / (φ1 - φ0), G = cosφ0 / n + φ0; + if (abs(n) < ε) return d3_geo_equirectangular; + function forward(λ, φ) { + var ρ = G - φ; + return [ ρ * Math.sin(n * λ), G - ρ * Math.cos(n * λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = G - y; + return [ Math.atan2(x, ρ0_y) / n, G - d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y) ]; + }; + return forward; + } + (d3.geo.conicEquidistant = function() { + return d3_geo_conic(d3_geo_conicEquidistant); + }).raw = d3_geo_conicEquidistant; + var d3_geo_gnomonic = d3_geo_azimuthal(function(cosλcosφ) { + return 1 / cosλcosφ; + }, Math.atan); + (d3.geo.gnomonic = function() { + return d3_geo_projection(d3_geo_gnomonic); + }).raw = d3_geo_gnomonic; + function d3_geo_mercator(λ, φ) { + return [ λ, Math.log(Math.tan(π / 4 + φ / 2)) ]; + } + d3_geo_mercator.invert = function(x, y) { + return [ x, 2 * Math.atan(Math.exp(y)) - halfπ ]; + }; + function d3_geo_mercatorProjection(project) { + var m = d3_geo_projection(project), scale = m.scale, translate = m.translate, clipExtent = m.clipExtent, clipAuto; + m.scale = function() { + var v = scale.apply(m, arguments); + return v === m ? clipAuto ? m.clipExtent(null) : m : v; + }; + m.translate = function() { + var v = translate.apply(m, arguments); + return v === m ? clipAuto ? m.clipExtent(null) : m : v; + }; + m.clipExtent = function(_) { + var v = clipExtent.apply(m, arguments); + if (v === m) { + if (clipAuto = _ == null) { + var k = π * scale(), t = translate(); + clipExtent([ [ t[0] - k, t[1] - k ], [ t[0] + k, t[1] + k ] ]); + } + } else if (clipAuto) { + v = null; + } + return v; + }; + return m.clipExtent(null); + } + (d3.geo.mercator = function() { + return d3_geo_mercatorProjection(d3_geo_mercator); + }).raw = d3_geo_mercator; + var d3_geo_orthographic = d3_geo_azimuthal(function() { + return 1; + }, Math.asin); + (d3.geo.orthographic = function() { + return d3_geo_projection(d3_geo_orthographic); + }).raw = d3_geo_orthographic; + var d3_geo_stereographic = d3_geo_azimuthal(function(cosλcosφ) { + return 1 / (1 + cosλcosφ); + }, function(ρ) { + return 2 * Math.atan(ρ); + }); + (d3.geo.stereographic = function() { + return d3_geo_projection(d3_geo_stereographic); + }).raw = d3_geo_stereographic; + function d3_geo_transverseMercator(λ, φ) { + return [ Math.log(Math.tan(π / 4 + φ / 2)), -λ ]; + } + d3_geo_transverseMercator.invert = function(x, y) { + return [ -y, 2 * Math.atan(Math.exp(x)) - halfπ ]; + }; + (d3.geo.transverseMercator = function() { + var projection = d3_geo_mercatorProjection(d3_geo_transverseMercator), center = projection.center, rotate = projection.rotate; + projection.center = function(_) { + return _ ? center([ -_[1], _[0] ]) : (_ = center(), [ _[1], -_[0] ]); + }; + projection.rotate = function(_) { + return _ ? rotate([ _[0], _[1], _.length > 2 ? _[2] + 90 : 90 ]) : (_ = rotate(), + [ _[0], _[1], _[2] - 90 ]); + }; + return rotate([ 0, 0, 90 ]); + }).raw = d3_geo_transverseMercator; + d3.geom = {}; + function d3_geom_pointX(d) { + return d[0]; + } + function d3_geom_pointY(d) { + return d[1]; + } + d3.geom.hull = function(vertices) { + var x = d3_geom_pointX, y = d3_geom_pointY; + if (arguments.length) return hull(vertices); + function hull(data) { + if (data.length < 3) return []; + var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = []; + for (i = 0; i < n; i++) { + points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]); + } + points.sort(d3_geom_hullOrder); + for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]); + var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints); + var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = []; + for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]); + for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]); + return polygon; + } + hull.x = function(_) { + return arguments.length ? (x = _, hull) : x; + }; + hull.y = function(_) { + return arguments.length ? (y = _, hull) : y; + }; + return hull; + }; + function d3_geom_hullUpper(points) { + var n = points.length, hull = [ 0, 1 ], hs = 2; + for (var i = 2; i < n; i++) { + while (hs > 1 && d3_cross2d(points[hull[hs - 2]], points[hull[hs - 1]], points[i]) <= 0) --hs; + hull[hs++] = i; + } + return hull.slice(0, hs); + } + function d3_geom_hullOrder(a, b) { + return a[0] - b[0] || a[1] - b[1]; + } + d3.geom.polygon = function(coordinates) { + d3_subclass(coordinates, d3_geom_polygonPrototype); + return coordinates; + }; + var d3_geom_polygonPrototype = d3.geom.polygon.prototype = []; + d3_geom_polygonPrototype.area = function() { + var i = -1, n = this.length, a, b = this[n - 1], area = 0; + while (++i < n) { + a = b; + b = this[i]; + area += a[1] * b[0] - a[0] * b[1]; + } + return area * .5; + }; + d3_geom_polygonPrototype.centroid = function(k) { + var i = -1, n = this.length, x = 0, y = 0, a, b = this[n - 1], c; + if (!arguments.length) k = -1 / (6 * this.area()); + while (++i < n) { + a = b; + b = this[i]; + c = a[0] * b[1] - b[0] * a[1]; + x += (a[0] + b[0]) * c; + y += (a[1] + b[1]) * c; + } + return [ x * k, y * k ]; + }; + d3_geom_polygonPrototype.clip = function(subject) { + var input, closed = d3_geom_polygonClosed(subject), i = -1, n = this.length - d3_geom_polygonClosed(this), j, m, a = this[n - 1], b, c, d; + while (++i < n) { + input = subject.slice(); + subject.length = 0; + b = this[i]; + c = input[(m = input.length - closed) - 1]; + j = -1; + while (++j < m) { + d = input[j]; + if (d3_geom_polygonInside(d, a, b)) { + if (!d3_geom_polygonInside(c, a, b)) { + subject.push(d3_geom_polygonIntersect(c, d, a, b)); + } + subject.push(d); + } else if (d3_geom_polygonInside(c, a, b)) { + subject.push(d3_geom_polygonIntersect(c, d, a, b)); + } + c = d; + } + if (closed) subject.push(subject[0]); + a = b; + } + return subject; + }; + function d3_geom_polygonInside(p, a, b) { + return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]); + } + function d3_geom_polygonIntersect(c, d, a, b) { + var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3, y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3, ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21); + return [ x1 + ua * x21, y1 + ua * y21 ]; + } + function d3_geom_polygonClosed(coordinates) { + var a = coordinates[0], b = coordinates[coordinates.length - 1]; + return !(a[0] - b[0] || a[1] - b[1]); + } + var d3_geom_voronoiEdges, d3_geom_voronoiCells, d3_geom_voronoiBeaches, d3_geom_voronoiBeachPool = [], d3_geom_voronoiFirstCircle, d3_geom_voronoiCircles, d3_geom_voronoiCirclePool = []; + function d3_geom_voronoiBeach() { + d3_geom_voronoiRedBlackNode(this); + this.edge = this.site = this.circle = null; + } + function d3_geom_voronoiCreateBeach(site) { + var beach = d3_geom_voronoiBeachPool.pop() || new d3_geom_voronoiBeach(); + beach.site = site; + return beach; + } + function d3_geom_voronoiDetachBeach(beach) { + d3_geom_voronoiDetachCircle(beach); + d3_geom_voronoiBeaches.remove(beach); + d3_geom_voronoiBeachPool.push(beach); + d3_geom_voronoiRedBlackNode(beach); + } + function d3_geom_voronoiRemoveBeach(beach) { + var circle = beach.circle, x = circle.x, y = circle.cy, vertex = { + x: x, + y: y + }, previous = beach.P, next = beach.N, disappearing = [ beach ]; + d3_geom_voronoiDetachBeach(beach); + var lArc = previous; + while (lArc.circle && abs(x - lArc.circle.x) < ε && abs(y - lArc.circle.cy) < ε) { + previous = lArc.P; + disappearing.unshift(lArc); + d3_geom_voronoiDetachBeach(lArc); + lArc = previous; + } + disappearing.unshift(lArc); + d3_geom_voronoiDetachCircle(lArc); + var rArc = next; + while (rArc.circle && abs(x - rArc.circle.x) < ε && abs(y - rArc.circle.cy) < ε) { + next = rArc.N; + disappearing.push(rArc); + d3_geom_voronoiDetachBeach(rArc); + rArc = next; + } + disappearing.push(rArc); + d3_geom_voronoiDetachCircle(rArc); + var nArcs = disappearing.length, iArc; + for (iArc = 1; iArc < nArcs; ++iArc) { + rArc = disappearing[iArc]; + lArc = disappearing[iArc - 1]; + d3_geom_voronoiSetEdgeEnd(rArc.edge, lArc.site, rArc.site, vertex); + } + lArc = disappearing[0]; + rArc = disappearing[nArcs - 1]; + rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, rArc.site, null, vertex); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + } + function d3_geom_voronoiAddBeach(site) { + var x = site.x, directrix = site.y, lArc, rArc, dxl, dxr, node = d3_geom_voronoiBeaches._; + while (node) { + dxl = d3_geom_voronoiLeftBreakPoint(node, directrix) - x; + if (dxl > ε) node = node.L; else { + dxr = x - d3_geom_voronoiRightBreakPoint(node, directrix); + if (dxr > ε) { + if (!node.R) { + lArc = node; + break; + } + node = node.R; + } else { + if (dxl > -ε) { + lArc = node.P; + rArc = node; + } else if (dxr > -ε) { + lArc = node; + rArc = node.N; + } else { + lArc = rArc = node; + } + break; + } + } + } + var newArc = d3_geom_voronoiCreateBeach(site); + d3_geom_voronoiBeaches.insert(lArc, newArc); + if (!lArc && !rArc) return; + if (lArc === rArc) { + d3_geom_voronoiDetachCircle(lArc); + rArc = d3_geom_voronoiCreateBeach(lArc.site); + d3_geom_voronoiBeaches.insert(newArc, rArc); + newArc.edge = rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + return; + } + if (!rArc) { + newArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site); + return; + } + d3_geom_voronoiDetachCircle(lArc); + d3_geom_voronoiDetachCircle(rArc); + var lSite = lArc.site, ax = lSite.x, ay = lSite.y, bx = site.x - ax, by = site.y - ay, rSite = rArc.site, cx = rSite.x - ax, cy = rSite.y - ay, d = 2 * (bx * cy - by * cx), hb = bx * bx + by * by, hc = cx * cx + cy * cy, vertex = { + x: (cy * hb - by * hc) / d + ax, + y: (bx * hc - cx * hb) / d + ay + }; + d3_geom_voronoiSetEdgeEnd(rArc.edge, lSite, rSite, vertex); + newArc.edge = d3_geom_voronoiCreateEdge(lSite, site, null, vertex); + rArc.edge = d3_geom_voronoiCreateEdge(site, rSite, null, vertex); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + } + function d3_geom_voronoiLeftBreakPoint(arc, directrix) { + var site = arc.site, rfocx = site.x, rfocy = site.y, pby2 = rfocy - directrix; + if (!pby2) return rfocx; + var lArc = arc.P; + if (!lArc) return -Infinity; + site = lArc.site; + var lfocx = site.x, lfocy = site.y, plby2 = lfocy - directrix; + if (!plby2) return lfocx; + var hl = lfocx - rfocx, aby2 = 1 / pby2 - 1 / plby2, b = hl / plby2; + if (aby2) return (-b + Math.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx; + return (rfocx + lfocx) / 2; + } + function d3_geom_voronoiRightBreakPoint(arc, directrix) { + var rArc = arc.N; + if (rArc) return d3_geom_voronoiLeftBreakPoint(rArc, directrix); + var site = arc.site; + return site.y === directrix ? site.x : Infinity; + } + function d3_geom_voronoiCell(site) { + this.site = site; + this.edges = []; + } + d3_geom_voronoiCell.prototype.prepare = function() { + var halfEdges = this.edges, iHalfEdge = halfEdges.length, edge; + while (iHalfEdge--) { + edge = halfEdges[iHalfEdge].edge; + if (!edge.b || !edge.a) halfEdges.splice(iHalfEdge, 1); + } + halfEdges.sort(d3_geom_voronoiHalfEdgeOrder); + return halfEdges.length; + }; + function d3_geom_voronoiCloseCells(extent) { + var x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], x2, y2, x3, y3, cells = d3_geom_voronoiCells, iCell = cells.length, cell, iHalfEdge, halfEdges, nHalfEdges, start, end; + while (iCell--) { + cell = cells[iCell]; + if (!cell || !cell.prepare()) continue; + halfEdges = cell.edges; + nHalfEdges = halfEdges.length; + iHalfEdge = 0; + while (iHalfEdge < nHalfEdges) { + end = halfEdges[iHalfEdge].end(), x3 = end.x, y3 = end.y; + start = halfEdges[++iHalfEdge % nHalfEdges].start(), x2 = start.x, y2 = start.y; + if (abs(x3 - x2) > ε || abs(y3 - y2) > ε) { + halfEdges.splice(iHalfEdge, 0, new d3_geom_voronoiHalfEdge(d3_geom_voronoiCreateBorderEdge(cell.site, end, abs(x3 - x0) < ε && y1 - y3 > ε ? { + x: x0, + y: abs(x2 - x0) < ε ? y2 : y1 + } : abs(y3 - y1) < ε && x1 - x3 > ε ? { + x: abs(y2 - y1) < ε ? x2 : x1, + y: y1 + } : abs(x3 - x1) < ε && y3 - y0 > ε ? { + x: x1, + y: abs(x2 - x1) < ε ? y2 : y0 + } : abs(y3 - y0) < ε && x3 - x0 > ε ? { + x: abs(y2 - y0) < ε ? x2 : x0, + y: y0 + } : null), cell.site, null)); + ++nHalfEdges; + } + } + } + } + function d3_geom_voronoiHalfEdgeOrder(a, b) { + return b.angle - a.angle; + } + function d3_geom_voronoiCircle() { + d3_geom_voronoiRedBlackNode(this); + this.x = this.y = this.arc = this.site = this.cy = null; + } + function d3_geom_voronoiAttachCircle(arc) { + var lArc = arc.P, rArc = arc.N; + if (!lArc || !rArc) return; + var lSite = lArc.site, cSite = arc.site, rSite = rArc.site; + if (lSite === rSite) return; + var bx = cSite.x, by = cSite.y, ax = lSite.x - bx, ay = lSite.y - by, cx = rSite.x - bx, cy = rSite.y - by; + var d = 2 * (ax * cy - ay * cx); + if (d >= -ε2) return; + var ha = ax * ax + ay * ay, hc = cx * cx + cy * cy, x = (cy * ha - ay * hc) / d, y = (ax * hc - cx * ha) / d, cy = y + by; + var circle = d3_geom_voronoiCirclePool.pop() || new d3_geom_voronoiCircle(); + circle.arc = arc; + circle.site = cSite; + circle.x = x + bx; + circle.y = cy + Math.sqrt(x * x + y * y); + circle.cy = cy; + arc.circle = circle; + var before = null, node = d3_geom_voronoiCircles._; + while (node) { + if (circle.y < node.y || circle.y === node.y && circle.x <= node.x) { + if (node.L) node = node.L; else { + before = node.P; + break; + } + } else { + if (node.R) node = node.R; else { + before = node; + break; + } + } + } + d3_geom_voronoiCircles.insert(before, circle); + if (!before) d3_geom_voronoiFirstCircle = circle; + } + function d3_geom_voronoiDetachCircle(arc) { + var circle = arc.circle; + if (circle) { + if (!circle.P) d3_geom_voronoiFirstCircle = circle.N; + d3_geom_voronoiCircles.remove(circle); + d3_geom_voronoiCirclePool.push(circle); + d3_geom_voronoiRedBlackNode(circle); + arc.circle = null; + } + } + function d3_geom_voronoiClipEdges(extent) { + var edges = d3_geom_voronoiEdges, clip = d3_geom_clipLine(extent[0][0], extent[0][1], extent[1][0], extent[1][1]), i = edges.length, e; + while (i--) { + e = edges[i]; + if (!d3_geom_voronoiConnectEdge(e, extent) || !clip(e) || abs(e.a.x - e.b.x) < ε && abs(e.a.y - e.b.y) < ε) { + e.a = e.b = null; + edges.splice(i, 1); + } + } + } + function d3_geom_voronoiConnectEdge(edge, extent) { + var vb = edge.b; + if (vb) return true; + var va = edge.a, x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], lSite = edge.l, rSite = edge.r, lx = lSite.x, ly = lSite.y, rx = rSite.x, ry = rSite.y, fx = (lx + rx) / 2, fy = (ly + ry) / 2, fm, fb; + if (ry === ly) { + if (fx < x0 || fx >= x1) return; + if (lx > rx) { + if (!va) va = { + x: fx, + y: y0 + }; else if (va.y >= y1) return; + vb = { + x: fx, + y: y1 + }; + } else { + if (!va) va = { + x: fx, + y: y1 + }; else if (va.y < y0) return; + vb = { + x: fx, + y: y0 + }; + } + } else { + fm = (lx - rx) / (ry - ly); + fb = fy - fm * fx; + if (fm < -1 || fm > 1) { + if (lx > rx) { + if (!va) va = { + x: (y0 - fb) / fm, + y: y0 + }; else if (va.y >= y1) return; + vb = { + x: (y1 - fb) / fm, + y: y1 + }; + } else { + if (!va) va = { + x: (y1 - fb) / fm, + y: y1 + }; else if (va.y < y0) return; + vb = { + x: (y0 - fb) / fm, + y: y0 + }; + } + } else { + if (ly < ry) { + if (!va) va = { + x: x0, + y: fm * x0 + fb + }; else if (va.x >= x1) return; + vb = { + x: x1, + y: fm * x1 + fb + }; + } else { + if (!va) va = { + x: x1, + y: fm * x1 + fb + }; else if (va.x < x0) return; + vb = { + x: x0, + y: fm * x0 + fb + }; + } + } + } + edge.a = va; + edge.b = vb; + return true; + } + function d3_geom_voronoiEdge(lSite, rSite) { + this.l = lSite; + this.r = rSite; + this.a = this.b = null; + } + function d3_geom_voronoiCreateEdge(lSite, rSite, va, vb) { + var edge = new d3_geom_voronoiEdge(lSite, rSite); + d3_geom_voronoiEdges.push(edge); + if (va) d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, va); + if (vb) d3_geom_voronoiSetEdgeEnd(edge, rSite, lSite, vb); + d3_geom_voronoiCells[lSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, lSite, rSite)); + d3_geom_voronoiCells[rSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, rSite, lSite)); + return edge; + } + function d3_geom_voronoiCreateBorderEdge(lSite, va, vb) { + var edge = new d3_geom_voronoiEdge(lSite, null); + edge.a = va; + edge.b = vb; + d3_geom_voronoiEdges.push(edge); + return edge; + } + function d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, vertex) { + if (!edge.a && !edge.b) { + edge.a = vertex; + edge.l = lSite; + edge.r = rSite; + } else if (edge.l === rSite) { + edge.b = vertex; + } else { + edge.a = vertex; + } + } + function d3_geom_voronoiHalfEdge(edge, lSite, rSite) { + var va = edge.a, vb = edge.b; + this.edge = edge; + this.site = lSite; + this.angle = rSite ? Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x) : edge.l === lSite ? Math.atan2(vb.x - va.x, va.y - vb.y) : Math.atan2(va.x - vb.x, vb.y - va.y); + } + d3_geom_voronoiHalfEdge.prototype = { + start: function() { + return this.edge.l === this.site ? this.edge.a : this.edge.b; + }, + end: function() { + return this.edge.l === this.site ? this.edge.b : this.edge.a; + } + }; + function d3_geom_voronoiRedBlackTree() { + this._ = null; + } + function d3_geom_voronoiRedBlackNode(node) { + node.U = node.C = node.L = node.R = node.P = node.N = null; + } + d3_geom_voronoiRedBlackTree.prototype = { + insert: function(after, node) { + var parent, grandpa, uncle; + if (after) { + node.P = after; + node.N = after.N; + if (after.N) after.N.P = node; + after.N = node; + if (after.R) { + after = after.R; + while (after.L) after = after.L; + after.L = node; + } else { + after.R = node; + } + parent = after; + } else if (this._) { + after = d3_geom_voronoiRedBlackFirst(this._); + node.P = null; + node.N = after; + after.P = after.L = node; + parent = after; + } else { + node.P = node.N = null; + this._ = node; + parent = null; + } + node.L = node.R = null; + node.U = parent; + node.C = true; + after = node; + while (parent && parent.C) { + grandpa = parent.U; + if (parent === grandpa.L) { + uncle = grandpa.R; + if (uncle && uncle.C) { + parent.C = uncle.C = false; + grandpa.C = true; + after = grandpa; + } else { + if (after === parent.R) { + d3_geom_voronoiRedBlackRotateLeft(this, parent); + after = parent; + parent = after.U; + } + parent.C = false; + grandpa.C = true; + d3_geom_voronoiRedBlackRotateRight(this, grandpa); + } + } else { + uncle = grandpa.L; + if (uncle && uncle.C) { + parent.C = uncle.C = false; + grandpa.C = true; + after = grandpa; + } else { + if (after === parent.L) { + d3_geom_voronoiRedBlackRotateRight(this, parent); + after = parent; + parent = after.U; + } + parent.C = false; + grandpa.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, grandpa); + } + } + parent = after.U; + } + this._.C = false; + }, + remove: function(node) { + if (node.N) node.N.P = node.P; + if (node.P) node.P.N = node.N; + node.N = node.P = null; + var parent = node.U, sibling, left = node.L, right = node.R, next, red; + if (!left) next = right; else if (!right) next = left; else next = d3_geom_voronoiRedBlackFirst(right); + if (parent) { + if (parent.L === node) parent.L = next; else parent.R = next; + } else { + this._ = next; + } + if (left && right) { + red = next.C; + next.C = node.C; + next.L = left; + left.U = next; + if (next !== right) { + parent = next.U; + next.U = node.U; + node = next.R; + parent.L = node; + next.R = right; + right.U = next; + } else { + next.U = parent; + parent = next; + node = next.R; + } + } else { + red = node.C; + node = next; + } + if (node) node.U = parent; + if (red) return; + if (node && node.C) { + node.C = false; + return; + } + do { + if (node === this._) break; + if (node === parent.L) { + sibling = parent.R; + if (sibling.C) { + sibling.C = false; + parent.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, parent); + sibling = parent.R; + } + if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) { + if (!sibling.R || !sibling.R.C) { + sibling.L.C = false; + sibling.C = true; + d3_geom_voronoiRedBlackRotateRight(this, sibling); + sibling = parent.R; + } + sibling.C = parent.C; + parent.C = sibling.R.C = false; + d3_geom_voronoiRedBlackRotateLeft(this, parent); + node = this._; + break; + } + } else { + sibling = parent.L; + if (sibling.C) { + sibling.C = false; + parent.C = true; + d3_geom_voronoiRedBlackRotateRight(this, parent); + sibling = parent.L; + } + if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) { + if (!sibling.L || !sibling.L.C) { + sibling.R.C = false; + sibling.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, sibling); + sibling = parent.L; + } + sibling.C = parent.C; + parent.C = sibling.L.C = false; + d3_geom_voronoiRedBlackRotateRight(this, parent); + node = this._; + break; + } + } + sibling.C = true; + node = parent; + parent = parent.U; + } while (!node.C); + if (node) node.C = false; + } + }; + function d3_geom_voronoiRedBlackRotateLeft(tree, node) { + var p = node, q = node.R, parent = p.U; + if (parent) { + if (parent.L === p) parent.L = q; else parent.R = q; + } else { + tree._ = q; + } + q.U = parent; + p.U = q; + p.R = q.L; + if (p.R) p.R.U = p; + q.L = p; + } + function d3_geom_voronoiRedBlackRotateRight(tree, node) { + var p = node, q = node.L, parent = p.U; + if (parent) { + if (parent.L === p) parent.L = q; else parent.R = q; + } else { + tree._ = q; + } + q.U = parent; + p.U = q; + p.L = q.R; + if (p.L) p.L.U = p; + q.R = p; + } + function d3_geom_voronoiRedBlackFirst(node) { + while (node.L) node = node.L; + return node; + } + function d3_geom_voronoi(sites, bbox) { + var site = sites.sort(d3_geom_voronoiVertexOrder).pop(), x0, y0, circle; + d3_geom_voronoiEdges = []; + d3_geom_voronoiCells = new Array(sites.length); + d3_geom_voronoiBeaches = new d3_geom_voronoiRedBlackTree(); + d3_geom_voronoiCircles = new d3_geom_voronoiRedBlackTree(); + while (true) { + circle = d3_geom_voronoiFirstCircle; + if (site && (!circle || site.y < circle.y || site.y === circle.y && site.x < circle.x)) { + if (site.x !== x0 || site.y !== y0) { + d3_geom_voronoiCells[site.i] = new d3_geom_voronoiCell(site); + d3_geom_voronoiAddBeach(site); + x0 = site.x, y0 = site.y; + } + site = sites.pop(); + } else if (circle) { + d3_geom_voronoiRemoveBeach(circle.arc); + } else { + break; + } + } + if (bbox) d3_geom_voronoiClipEdges(bbox), d3_geom_voronoiCloseCells(bbox); + var diagram = { + cells: d3_geom_voronoiCells, + edges: d3_geom_voronoiEdges + }; + d3_geom_voronoiBeaches = d3_geom_voronoiCircles = d3_geom_voronoiEdges = d3_geom_voronoiCells = null; + return diagram; + } + function d3_geom_voronoiVertexOrder(a, b) { + return b.y - a.y || b.x - a.x; + } + d3.geom.voronoi = function(points) { + var x = d3_geom_pointX, y = d3_geom_pointY, fx = x, fy = y, clipExtent = d3_geom_voronoiClipExtent; + if (points) return voronoi(points); + function voronoi(data) { + var polygons = new Array(data.length), x0 = clipExtent[0][0], y0 = clipExtent[0][1], x1 = clipExtent[1][0], y1 = clipExtent[1][1]; + d3_geom_voronoi(sites(data), clipExtent).cells.forEach(function(cell, i) { + var edges = cell.edges, site = cell.site, polygon = polygons[i] = edges.length ? edges.map(function(e) { + var s = e.start(); + return [ s.x, s.y ]; + }) : site.x >= x0 && site.x <= x1 && site.y >= y0 && site.y <= y1 ? [ [ x0, y1 ], [ x1, y1 ], [ x1, y0 ], [ x0, y0 ] ] : []; + polygon.point = data[i]; + }); + return polygons; + } + function sites(data) { + return data.map(function(d, i) { + return { + x: Math.round(fx(d, i) / ε) * ε, + y: Math.round(fy(d, i) / ε) * ε, + i: i + }; + }); + } + voronoi.links = function(data) { + return d3_geom_voronoi(sites(data)).edges.filter(function(edge) { + return edge.l && edge.r; + }).map(function(edge) { + return { + source: data[edge.l.i], + target: data[edge.r.i] + }; + }); + }; + voronoi.triangles = function(data) { + var triangles = []; + d3_geom_voronoi(sites(data)).cells.forEach(function(cell, i) { + var site = cell.site, edges = cell.edges.sort(d3_geom_voronoiHalfEdgeOrder), j = -1, m = edges.length, e0, s0, e1 = edges[m - 1].edge, s1 = e1.l === site ? e1.r : e1.l; + while (++j < m) { + e0 = e1; + s0 = s1; + e1 = edges[j].edge; + s1 = e1.l === site ? e1.r : e1.l; + if (i < s0.i && i < s1.i && d3_geom_voronoiTriangleArea(site, s0, s1) < 0) { + triangles.push([ data[i], data[s0.i], data[s1.i] ]); + } + } + }); + return triangles; + }; + voronoi.x = function(_) { + return arguments.length ? (fx = d3_functor(x = _), voronoi) : x; + }; + voronoi.y = function(_) { + return arguments.length ? (fy = d3_functor(y = _), voronoi) : y; + }; + voronoi.clipExtent = function(_) { + if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent; + clipExtent = _ == null ? d3_geom_voronoiClipExtent : _; + return voronoi; + }; + voronoi.size = function(_) { + if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent && clipExtent[1]; + return voronoi.clipExtent(_ && [ [ 0, 0 ], _ ]); + }; + return voronoi; + }; + var d3_geom_voronoiClipExtent = [ [ -1e6, -1e6 ], [ 1e6, 1e6 ] ]; + function d3_geom_voronoiTriangleArea(a, b, c) { + return (a.x - c.x) * (b.y - a.y) - (a.x - b.x) * (c.y - a.y); + } + d3.geom.delaunay = function(vertices) { + return d3.geom.voronoi().triangles(vertices); + }; + d3.geom.quadtree = function(points, x1, y1, x2, y2) { + var x = d3_geom_pointX, y = d3_geom_pointY, compat; + if (compat = arguments.length) { + x = d3_geom_quadtreeCompatX; + y = d3_geom_quadtreeCompatY; + if (compat === 3) { + y2 = y1; + x2 = x1; + y1 = x1 = 0; + } + return quadtree(points); + } + function quadtree(data) { + var d, fx = d3_functor(x), fy = d3_functor(y), xs, ys, i, n, x1_, y1_, x2_, y2_; + if (x1 != null) { + x1_ = x1, y1_ = y1, x2_ = x2, y2_ = y2; + } else { + x2_ = y2_ = -(x1_ = y1_ = Infinity); + xs = [], ys = []; + n = data.length; + if (compat) for (i = 0; i < n; ++i) { + d = data[i]; + if (d.x < x1_) x1_ = d.x; + if (d.y < y1_) y1_ = d.y; + if (d.x > x2_) x2_ = d.x; + if (d.y > y2_) y2_ = d.y; + xs.push(d.x); + ys.push(d.y); + } else for (i = 0; i < n; ++i) { + var x_ = +fx(d = data[i], i), y_ = +fy(d, i); + if (x_ < x1_) x1_ = x_; + if (y_ < y1_) y1_ = y_; + if (x_ > x2_) x2_ = x_; + if (y_ > y2_) y2_ = y_; + xs.push(x_); + ys.push(y_); + } + } + var dx = x2_ - x1_, dy = y2_ - y1_; + if (dx > dy) y2_ = y1_ + dx; else x2_ = x1_ + dy; + function insert(n, d, x, y, x1, y1, x2, y2) { + if (isNaN(x) || isNaN(y)) return; + if (n.leaf) { + var nx = n.x, ny = n.y; + if (nx != null) { + if (abs(nx - x) + abs(ny - y) < .01) { + insertChild(n, d, x, y, x1, y1, x2, y2); + } else { + var nPoint = n.point; + n.x = n.y = n.point = null; + insertChild(n, nPoint, nx, ny, x1, y1, x2, y2); + insertChild(n, d, x, y, x1, y1, x2, y2); + } + } else { + n.x = x, n.y = y, n.point = d; + } + } else { + insertChild(n, d, x, y, x1, y1, x2, y2); + } + } + function insertChild(n, d, x, y, x1, y1, x2, y2) { + var xm = (x1 + x2) * .5, ym = (y1 + y2) * .5, right = x >= xm, below = y >= ym, i = below << 1 | right; + n.leaf = false; + n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode()); + if (right) x1 = xm; else x2 = xm; + if (below) y1 = ym; else y2 = ym; + insert(n, d, x, y, x1, y1, x2, y2); + } + var root = d3_geom_quadtreeNode(); + root.add = function(d) { + insert(root, d, +fx(d, ++i), +fy(d, i), x1_, y1_, x2_, y2_); + }; + root.visit = function(f) { + d3_geom_quadtreeVisit(f, root, x1_, y1_, x2_, y2_); + }; + root.find = function(point) { + return d3_geom_quadtreeFind(root, point[0], point[1], x1_, y1_, x2_, y2_); + }; + i = -1; + if (x1 == null) { + while (++i < n) { + insert(root, data[i], xs[i], ys[i], x1_, y1_, x2_, y2_); + } + --i; + } else data.forEach(root.add); + xs = ys = data = d = null; + return root; + } + quadtree.x = function(_) { + return arguments.length ? (x = _, quadtree) : x; + }; + quadtree.y = function(_) { + return arguments.length ? (y = _, quadtree) : y; + }; + quadtree.extent = function(_) { + if (!arguments.length) return x1 == null ? null : [ [ x1, y1 ], [ x2, y2 ] ]; + if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = +_[0][0], y1 = +_[0][1], x2 = +_[1][0], + y2 = +_[1][1]; + return quadtree; + }; + quadtree.size = function(_) { + if (!arguments.length) return x1 == null ? null : [ x2 - x1, y2 - y1 ]; + if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = y1 = 0, x2 = +_[0], y2 = +_[1]; + return quadtree; + }; + return quadtree; + }; + function d3_geom_quadtreeCompatX(d) { + return d.x; + } + function d3_geom_quadtreeCompatY(d) { + return d.y; + } + function d3_geom_quadtreeNode() { + return { + leaf: true, + nodes: [], + point: null, + x: null, + y: null + }; + } + function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) { + if (!f(node, x1, y1, x2, y2)) { + var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, children = node.nodes; + if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy); + if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy); + if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2); + if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2); + } + } + function d3_geom_quadtreeFind(root, x, y, x0, y0, x3, y3) { + var minDistance2 = Infinity, closestPoint; + (function find(node, x1, y1, x2, y2) { + if (x1 > x3 || y1 > y3 || x2 < x0 || y2 < y0) return; + if (point = node.point) { + var point, dx = x - node.x, dy = y - node.y, distance2 = dx * dx + dy * dy; + if (distance2 < minDistance2) { + var distance = Math.sqrt(minDistance2 = distance2); + x0 = x - distance, y0 = y - distance; + x3 = x + distance, y3 = y + distance; + closestPoint = point; + } + } + var children = node.nodes, xm = (x1 + x2) * .5, ym = (y1 + y2) * .5, right = x >= xm, below = y >= ym; + for (var i = below << 1 | right, j = i + 4; i < j; ++i) { + if (node = children[i & 3]) switch (i & 3) { + case 0: + find(node, x1, y1, xm, ym); + break; + + case 1: + find(node, xm, y1, x2, ym); + break; + + case 2: + find(node, x1, ym, xm, y2); + break; + + case 3: + find(node, xm, ym, x2, y2); + break; + } + } + })(root, x0, y0, x3, y3); + return closestPoint; + } + d3.interpolateRgb = d3_interpolateRgb; + function d3_interpolateRgb(a, b) { + a = d3.rgb(a); + b = d3.rgb(b); + var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab; + return function(t) { + return "#" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t)); + }; + } + d3.interpolateObject = d3_interpolateObject; + function d3_interpolateObject(a, b) { + var i = {}, c = {}, k; + for (k in a) { + if (k in b) { + i[k] = d3_interpolate(a[k], b[k]); + } else { + c[k] = a[k]; + } + } + for (k in b) { + if (!(k in a)) { + c[k] = b[k]; + } + } + return function(t) { + for (k in i) c[k] = i[k](t); + return c; + }; + } + d3.interpolateNumber = d3_interpolateNumber; + function d3_interpolateNumber(a, b) { + a = +a, b = +b; + return function(t) { + return a * (1 - t) + b * t; + }; + } + d3.interpolateString = d3_interpolateString; + function d3_interpolateString(a, b) { + var bi = d3_interpolate_numberA.lastIndex = d3_interpolate_numberB.lastIndex = 0, am, bm, bs, i = -1, s = [], q = []; + a = a + "", b = b + ""; + while ((am = d3_interpolate_numberA.exec(a)) && (bm = d3_interpolate_numberB.exec(b))) { + if ((bs = bm.index) > bi) { + bs = b.slice(bi, bs); + if (s[i]) s[i] += bs; else s[++i] = bs; + } + if ((am = am[0]) === (bm = bm[0])) { + if (s[i]) s[i] += bm; else s[++i] = bm; + } else { + s[++i] = null; + q.push({ + i: i, + x: d3_interpolateNumber(am, bm) + }); + } + bi = d3_interpolate_numberB.lastIndex; + } + if (bi < b.length) { + bs = b.slice(bi); + if (s[i]) s[i] += bs; else s[++i] = bs; + } + return s.length < 2 ? q[0] ? (b = q[0].x, function(t) { + return b(t) + ""; + }) : function() { + return b; + } : (b = q.length, function(t) { + for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t); + return s.join(""); + }); + } + var d3_interpolate_numberA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, d3_interpolate_numberB = new RegExp(d3_interpolate_numberA.source, "g"); + d3.interpolate = d3_interpolate; + function d3_interpolate(a, b) { + var i = d3.interpolators.length, f; + while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ; + return f; + } + d3.interpolators = [ function(a, b) { + var t = typeof b; + return (t === "string" ? d3_rgb_names.has(b.toLowerCase()) || /^(#|rgb\(|hsl\()/i.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_color ? d3_interpolateRgb : Array.isArray(b) ? d3_interpolateArray : t === "object" && isNaN(b) ? d3_interpolateObject : d3_interpolateNumber)(a, b); + } ]; + d3.interpolateArray = d3_interpolateArray; + function d3_interpolateArray(a, b) { + var x = [], c = [], na = a.length, nb = b.length, n0 = Math.min(a.length, b.length), i; + for (i = 0; i < n0; ++i) x.push(d3_interpolate(a[i], b[i])); + for (;i < na; ++i) c[i] = a[i]; + for (;i < nb; ++i) c[i] = b[i]; + return function(t) { + for (i = 0; i < n0; ++i) c[i] = x[i](t); + return c; + }; + } + var d3_ease_default = function() { + return d3_identity; + }; + var d3_ease = d3.map({ + linear: d3_ease_default, + poly: d3_ease_poly, + quad: function() { + return d3_ease_quad; + }, + cubic: function() { + return d3_ease_cubic; + }, + sin: function() { + return d3_ease_sin; + }, + exp: function() { + return d3_ease_exp; + }, + circle: function() { + return d3_ease_circle; + }, + elastic: d3_ease_elastic, + back: d3_ease_back, + bounce: function() { + return d3_ease_bounce; + } + }); + var d3_ease_mode = d3.map({ + "in": d3_identity, + out: d3_ease_reverse, + "in-out": d3_ease_reflect, + "out-in": function(f) { + return d3_ease_reflect(d3_ease_reverse(f)); + } + }); + d3.ease = function(name) { + var i = name.indexOf("-"), t = i >= 0 ? name.slice(0, i) : name, m = i >= 0 ? name.slice(i + 1) : "in"; + t = d3_ease.get(t) || d3_ease_default; + m = d3_ease_mode.get(m) || d3_identity; + return d3_ease_clamp(m(t.apply(null, d3_arraySlice.call(arguments, 1)))); + }; + function d3_ease_clamp(f) { + return function(t) { + return t <= 0 ? 0 : t >= 1 ? 1 : f(t); + }; + } + function d3_ease_reverse(f) { + return function(t) { + return 1 - f(1 - t); + }; + } + function d3_ease_reflect(f) { + return function(t) { + return .5 * (t < .5 ? f(2 * t) : 2 - f(2 - 2 * t)); + }; + } + function d3_ease_quad(t) { + return t * t; + } + function d3_ease_cubic(t) { + return t * t * t; + } + function d3_ease_cubicInOut(t) { + if (t <= 0) return 0; + if (t >= 1) return 1; + var t2 = t * t, t3 = t2 * t; + return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); + } + function d3_ease_poly(e) { + return function(t) { + return Math.pow(t, e); + }; + } + function d3_ease_sin(t) { + return 1 - Math.cos(t * halfπ); + } + function d3_ease_exp(t) { + return Math.pow(2, 10 * (t - 1)); + } + function d3_ease_circle(t) { + return 1 - Math.sqrt(1 - t * t); + } + function d3_ease_elastic(a, p) { + var s; + if (arguments.length < 2) p = .45; + if (arguments.length) s = p / τ * Math.asin(1 / a); else a = 1, s = p / 4; + return function(t) { + return 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * τ / p); + }; + } + function d3_ease_back(s) { + if (!s) s = 1.70158; + return function(t) { + return t * t * ((s + 1) * t - s); + }; + } + function d3_ease_bounce(t) { + return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375; + } + d3.interpolateHcl = d3_interpolateHcl; + function d3_interpolateHcl(a, b) { + a = d3.hcl(a); + b = d3.hcl(b); + var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al; + if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac; + if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; + return function(t) { + return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + ""; + }; + } + d3.interpolateHsl = d3_interpolateHsl; + function d3_interpolateHsl(a, b) { + a = d3.hsl(a); + b = d3.hsl(b); + var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al; + if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as; + if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; + return function(t) { + return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + ""; + }; + } + d3.interpolateLab = d3_interpolateLab; + function d3_interpolateLab(a, b) { + a = d3.lab(a); + b = d3.lab(b); + var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab; + return function(t) { + return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + ""; + }; + } + d3.interpolateRound = d3_interpolateRound; + function d3_interpolateRound(a, b) { + b -= a; + return function(t) { + return Math.round(a + b * t); + }; + } + d3.transform = function(string) { + var g = d3_document.createElementNS(d3.ns.prefix.svg, "g"); + return (d3.transform = function(string) { + if (string != null) { + g.setAttribute("transform", string); + var t = g.transform.baseVal.consolidate(); + } + return new d3_transform(t ? t.matrix : d3_transformIdentity); + })(string); + }; + function d3_transform(m) { + var r0 = [ m.a, m.b ], r1 = [ m.c, m.d ], kx = d3_transformNormalize(r0), kz = d3_transformDot(r0, r1), ky = d3_transformNormalize(d3_transformCombine(r1, r0, -kz)) || 0; + if (r0[0] * r1[1] < r1[0] * r0[1]) { + r0[0] *= -1; + r0[1] *= -1; + kx *= -1; + kz *= -1; + } + this.rotate = (kx ? Math.atan2(r0[1], r0[0]) : Math.atan2(-r1[0], r1[1])) * d3_degrees; + this.translate = [ m.e, m.f ]; + this.scale = [ kx, ky ]; + this.skew = ky ? Math.atan2(kz, ky) * d3_degrees : 0; + } + d3_transform.prototype.toString = function() { + return "translate(" + this.translate + ")rotate(" + this.rotate + ")skewX(" + this.skew + ")scale(" + this.scale + ")"; + }; + function d3_transformDot(a, b) { + return a[0] * b[0] + a[1] * b[1]; + } + function d3_transformNormalize(a) { + var k = Math.sqrt(d3_transformDot(a, a)); + if (k) { + a[0] /= k; + a[1] /= k; + } + return k; + } + function d3_transformCombine(a, b, k) { + a[0] += k * b[0]; + a[1] += k * b[1]; + return a; + } + var d3_transformIdentity = { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0 + }; + d3.interpolateTransform = d3_interpolateTransform; + function d3_interpolateTransformPop(s) { + return s.length ? s.pop() + "," : ""; + } + function d3_interpolateTranslate(ta, tb, s, q) { + if (ta[0] !== tb[0] || ta[1] !== tb[1]) { + var i = s.push("translate(", null, ",", null, ")"); + q.push({ + i: i - 4, + x: d3_interpolateNumber(ta[0], tb[0]) + }, { + i: i - 2, + x: d3_interpolateNumber(ta[1], tb[1]) + }); + } else if (tb[0] || tb[1]) { + s.push("translate(" + tb + ")"); + } + } + function d3_interpolateRotate(ra, rb, s, q) { + if (ra !== rb) { + if (ra - rb > 180) rb += 360; else if (rb - ra > 180) ra += 360; + q.push({ + i: s.push(d3_interpolateTransformPop(s) + "rotate(", null, ")") - 2, + x: d3_interpolateNumber(ra, rb) + }); + } else if (rb) { + s.push(d3_interpolateTransformPop(s) + "rotate(" + rb + ")"); + } + } + function d3_interpolateSkew(wa, wb, s, q) { + if (wa !== wb) { + q.push({ + i: s.push(d3_interpolateTransformPop(s) + "skewX(", null, ")") - 2, + x: d3_interpolateNumber(wa, wb) + }); + } else if (wb) { + s.push(d3_interpolateTransformPop(s) + "skewX(" + wb + ")"); + } + } + function d3_interpolateScale(ka, kb, s, q) { + if (ka[0] !== kb[0] || ka[1] !== kb[1]) { + var i = s.push(d3_interpolateTransformPop(s) + "scale(", null, ",", null, ")"); + q.push({ + i: i - 4, + x: d3_interpolateNumber(ka[0], kb[0]) + }, { + i: i - 2, + x: d3_interpolateNumber(ka[1], kb[1]) + }); + } else if (kb[0] !== 1 || kb[1] !== 1) { + s.push(d3_interpolateTransformPop(s) + "scale(" + kb + ")"); + } + } + function d3_interpolateTransform(a, b) { + var s = [], q = []; + a = d3.transform(a), b = d3.transform(b); + d3_interpolateTranslate(a.translate, b.translate, s, q); + d3_interpolateRotate(a.rotate, b.rotate, s, q); + d3_interpolateSkew(a.skew, b.skew, s, q); + d3_interpolateScale(a.scale, b.scale, s, q); + a = b = null; + return function(t) { + var i = -1, n = q.length, o; + while (++i < n) s[(o = q[i]).i] = o.x(t); + return s.join(""); + }; + } + function d3_uninterpolateNumber(a, b) { + b = (b -= a = +a) || 1 / b; + return function(x) { + return (x - a) / b; + }; + } + function d3_uninterpolateClamp(a, b) { + b = (b -= a = +a) || 1 / b; + return function(x) { + return Math.max(0, Math.min(1, (x - a) / b)); + }; + } + d3.layout = {}; + d3.layout.bundle = function() { + return function(links) { + var paths = [], i = -1, n = links.length; + while (++i < n) paths.push(d3_layout_bundlePath(links[i])); + return paths; + }; + }; + function d3_layout_bundlePath(link) { + var start = link.source, end = link.target, lca = d3_layout_bundleLeastCommonAncestor(start, end), points = [ start ]; + while (start !== lca) { + start = start.parent; + points.push(start); + } + var k = points.length; + while (end !== lca) { + points.splice(k, 0, end); + end = end.parent; + } + return points; + } + function d3_layout_bundleAncestors(node) { + var ancestors = [], parent = node.parent; + while (parent != null) { + ancestors.push(node); + node = parent; + parent = parent.parent; + } + ancestors.push(node); + return ancestors; + } + function d3_layout_bundleLeastCommonAncestor(a, b) { + if (a === b) return a; + var aNodes = d3_layout_bundleAncestors(a), bNodes = d3_layout_bundleAncestors(b), aNode = aNodes.pop(), bNode = bNodes.pop(), sharedNode = null; + while (aNode === bNode) { + sharedNode = aNode; + aNode = aNodes.pop(); + bNode = bNodes.pop(); + } + return sharedNode; + } + d3.layout.chord = function() { + var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords; + function relayout() { + var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j; + chords = []; + groups = []; + k = 0, i = -1; + while (++i < n) { + x = 0, j = -1; + while (++j < n) { + x += matrix[i][j]; + } + groupSums.push(x); + subgroupIndex.push(d3.range(n)); + k += x; + } + if (sortGroups) { + groupIndex.sort(function(a, b) { + return sortGroups(groupSums[a], groupSums[b]); + }); + } + if (sortSubgroups) { + subgroupIndex.forEach(function(d, i) { + d.sort(function(a, b) { + return sortSubgroups(matrix[i][a], matrix[i][b]); + }); + }); + } + k = (τ - padding * n) / k; + x = 0, i = -1; + while (++i < n) { + x0 = x, j = -1; + while (++j < n) { + var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k; + subgroups[di + "-" + dj] = { + index: di, + subindex: dj, + startAngle: a0, + endAngle: a1, + value: v + }; + } + groups[di] = { + index: di, + startAngle: x0, + endAngle: x, + value: groupSums[di] + }; + x += padding; + } + i = -1; + while (++i < n) { + j = i - 1; + while (++j < n) { + var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i]; + if (source.value || target.value) { + chords.push(source.value < target.value ? { + source: target, + target: source + } : { + source: source, + target: target + }); + } + } + } + if (sortChords) resort(); + } + function resort() { + chords.sort(function(a, b) { + return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2); + }); + } + chord.matrix = function(x) { + if (!arguments.length) return matrix; + n = (matrix = x) && matrix.length; + chords = groups = null; + return chord; + }; + chord.padding = function(x) { + if (!arguments.length) return padding; + padding = x; + chords = groups = null; + return chord; + }; + chord.sortGroups = function(x) { + if (!arguments.length) return sortGroups; + sortGroups = x; + chords = groups = null; + return chord; + }; + chord.sortSubgroups = function(x) { + if (!arguments.length) return sortSubgroups; + sortSubgroups = x; + chords = null; + return chord; + }; + chord.sortChords = function(x) { + if (!arguments.length) return sortChords; + sortChords = x; + if (chords) resort(); + return chord; + }; + chord.chords = function() { + if (!chords) relayout(); + return chords; + }; + chord.groups = function() { + if (!groups) relayout(); + return groups; + }; + return chord; + }; + d3.layout.force = function() { + var force = {}, event = d3.dispatch("start", "tick", "end"), timer, size = [ 1, 1 ], drag, alpha, friction = .9, linkDistance = d3_layout_forceLinkDistance, linkStrength = d3_layout_forceLinkStrength, charge = -30, chargeDistance2 = d3_layout_forceChargeDistance2, gravity = .1, theta2 = .64, nodes = [], links = [], distances, strengths, charges; + function repulse(node) { + return function(quad, x1, _, x2) { + if (quad.point !== node) { + var dx = quad.cx - node.x, dy = quad.cy - node.y, dw = x2 - x1, dn = dx * dx + dy * dy; + if (dw * dw / theta2 < dn) { + if (dn < chargeDistance2) { + var k = quad.charge / dn; + node.px -= dx * k; + node.py -= dy * k; + } + return true; + } + if (quad.point && dn && dn < chargeDistance2) { + var k = quad.pointCharge / dn; + node.px -= dx * k; + node.py -= dy * k; + } + } + return !quad.charge; + }; + } + force.tick = function() { + if ((alpha *= .99) < .005) { + timer = null; + event.end({ + type: "end", + alpha: alpha = 0 + }); + return true; + } + var n = nodes.length, m = links.length, q, i, o, s, t, l, k, x, y; + for (i = 0; i < m; ++i) { + o = links[i]; + s = o.source; + t = o.target; + x = t.x - s.x; + y = t.y - s.y; + if (l = x * x + y * y) { + l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l; + x *= l; + y *= l; + t.x -= x * (k = s.weight + t.weight ? s.weight / (s.weight + t.weight) : .5); + t.y -= y * k; + s.x += x * (k = 1 - k); + s.y += y * k; + } + } + if (k = alpha * gravity) { + x = size[0] / 2; + y = size[1] / 2; + i = -1; + if (k) while (++i < n) { + o = nodes[i]; + o.x += (x - o.x) * k; + o.y += (y - o.y) * k; + } + } + if (charge) { + d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges); + i = -1; + while (++i < n) { + if (!(o = nodes[i]).fixed) { + q.visit(repulse(o)); + } + } + } + i = -1; + while (++i < n) { + o = nodes[i]; + if (o.fixed) { + o.x = o.px; + o.y = o.py; + } else { + o.x -= (o.px - (o.px = o.x)) * friction; + o.y -= (o.py - (o.py = o.y)) * friction; + } + } + event.tick({ + type: "tick", + alpha: alpha + }); + }; + force.nodes = function(x) { + if (!arguments.length) return nodes; + nodes = x; + return force; + }; + force.links = function(x) { + if (!arguments.length) return links; + links = x; + return force; + }; + force.size = function(x) { + if (!arguments.length) return size; + size = x; + return force; + }; + force.linkDistance = function(x) { + if (!arguments.length) return linkDistance; + linkDistance = typeof x === "function" ? x : +x; + return force; + }; + force.distance = force.linkDistance; + force.linkStrength = function(x) { + if (!arguments.length) return linkStrength; + linkStrength = typeof x === "function" ? x : +x; + return force; + }; + force.friction = function(x) { + if (!arguments.length) return friction; + friction = +x; + return force; + }; + force.charge = function(x) { + if (!arguments.length) return charge; + charge = typeof x === "function" ? x : +x; + return force; + }; + force.chargeDistance = function(x) { + if (!arguments.length) return Math.sqrt(chargeDistance2); + chargeDistance2 = x * x; + return force; + }; + force.gravity = function(x) { + if (!arguments.length) return gravity; + gravity = +x; + return force; + }; + force.theta = function(x) { + if (!arguments.length) return Math.sqrt(theta2); + theta2 = x * x; + return force; + }; + force.alpha = function(x) { + if (!arguments.length) return alpha; + x = +x; + if (alpha) { + if (x > 0) { + alpha = x; + } else { + timer.c = null, timer.t = NaN, timer = null; + event.end({ + type: "end", + alpha: alpha = 0 + }); + } + } else if (x > 0) { + event.start({ + type: "start", + alpha: alpha = x + }); + timer = d3_timer(force.tick); + } + return force; + }; + force.start = function() { + var i, n = nodes.length, m = links.length, w = size[0], h = size[1], neighbors, o; + for (i = 0; i < n; ++i) { + (o = nodes[i]).index = i; + o.weight = 0; + } + for (i = 0; i < m; ++i) { + o = links[i]; + if (typeof o.source == "number") o.source = nodes[o.source]; + if (typeof o.target == "number") o.target = nodes[o.target]; + ++o.source.weight; + ++o.target.weight; + } + for (i = 0; i < n; ++i) { + o = nodes[i]; + if (isNaN(o.x)) o.x = position("x", w); + if (isNaN(o.y)) o.y = position("y", h); + if (isNaN(o.px)) o.px = o.x; + if (isNaN(o.py)) o.py = o.y; + } + distances = []; + if (typeof linkDistance === "function") for (i = 0; i < m; ++i) distances[i] = +linkDistance.call(this, links[i], i); else for (i = 0; i < m; ++i) distances[i] = linkDistance; + strengths = []; + if (typeof linkStrength === "function") for (i = 0; i < m; ++i) strengths[i] = +linkStrength.call(this, links[i], i); else for (i = 0; i < m; ++i) strengths[i] = linkStrength; + charges = []; + if (typeof charge === "function") for (i = 0; i < n; ++i) charges[i] = +charge.call(this, nodes[i], i); else for (i = 0; i < n; ++i) charges[i] = charge; + function position(dimension, size) { + if (!neighbors) { + neighbors = new Array(n); + for (j = 0; j < n; ++j) { + neighbors[j] = []; + } + for (j = 0; j < m; ++j) { + var o = links[j]; + neighbors[o.source.index].push(o.target); + neighbors[o.target.index].push(o.source); + } + } + var candidates = neighbors[i], j = -1, l = candidates.length, x; + while (++j < l) if (!isNaN(x = candidates[j][dimension])) return x; + return Math.random() * size; + } + return force.resume(); + }; + force.resume = function() { + return force.alpha(.1); + }; + force.stop = function() { + return force.alpha(0); + }; + force.drag = function() { + if (!drag) drag = d3.behavior.drag().origin(d3_identity).on("dragstart.force", d3_layout_forceDragstart).on("drag.force", dragmove).on("dragend.force", d3_layout_forceDragend); + if (!arguments.length) return drag; + this.on("mouseover.force", d3_layout_forceMouseover).on("mouseout.force", d3_layout_forceMouseout).call(drag); + }; + function dragmove(d) { + d.px = d3.event.x, d.py = d3.event.y; + force.resume(); + } + return d3.rebind(force, event, "on"); + }; + function d3_layout_forceDragstart(d) { + d.fixed |= 2; + } + function d3_layout_forceDragend(d) { + d.fixed &= ~6; + } + function d3_layout_forceMouseover(d) { + d.fixed |= 4; + d.px = d.x, d.py = d.y; + } + function d3_layout_forceMouseout(d) { + d.fixed &= ~4; + } + function d3_layout_forceAccumulate(quad, alpha, charges) { + var cx = 0, cy = 0; + quad.charge = 0; + if (!quad.leaf) { + var nodes = quad.nodes, n = nodes.length, i = -1, c; + while (++i < n) { + c = nodes[i]; + if (c == null) continue; + d3_layout_forceAccumulate(c, alpha, charges); + quad.charge += c.charge; + cx += c.charge * c.cx; + cy += c.charge * c.cy; + } + } + if (quad.point) { + if (!quad.leaf) { + quad.point.x += Math.random() - .5; + quad.point.y += Math.random() - .5; + } + var k = alpha * charges[quad.point.index]; + quad.charge += quad.pointCharge = k; + cx += k * quad.point.x; + cy += k * quad.point.y; + } + quad.cx = cx / quad.charge; + quad.cy = cy / quad.charge; + } + var d3_layout_forceLinkDistance = 20, d3_layout_forceLinkStrength = 1, d3_layout_forceChargeDistance2 = Infinity; + d3.layout.hierarchy = function() { + var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue; + function hierarchy(root) { + var stack = [ root ], nodes = [], node; + root.depth = 0; + while ((node = stack.pop()) != null) { + nodes.push(node); + if ((childs = children.call(hierarchy, node, node.depth)) && (n = childs.length)) { + var n, childs, child; + while (--n >= 0) { + stack.push(child = childs[n]); + child.parent = node; + child.depth = node.depth + 1; + } + if (value) node.value = 0; + node.children = childs; + } else { + if (value) node.value = +value.call(hierarchy, node, node.depth) || 0; + delete node.children; + } + } + d3_layout_hierarchyVisitAfter(root, function(node) { + var childs, parent; + if (sort && (childs = node.children)) childs.sort(sort); + if (value && (parent = node.parent)) parent.value += node.value; + }); + return nodes; + } + hierarchy.sort = function(x) { + if (!arguments.length) return sort; + sort = x; + return hierarchy; + }; + hierarchy.children = function(x) { + if (!arguments.length) return children; + children = x; + return hierarchy; + }; + hierarchy.value = function(x) { + if (!arguments.length) return value; + value = x; + return hierarchy; + }; + hierarchy.revalue = function(root) { + if (value) { + d3_layout_hierarchyVisitBefore(root, function(node) { + if (node.children) node.value = 0; + }); + d3_layout_hierarchyVisitAfter(root, function(node) { + var parent; + if (!node.children) node.value = +value.call(hierarchy, node, node.depth) || 0; + if (parent = node.parent) parent.value += node.value; + }); + } + return root; + }; + return hierarchy; + }; + function d3_layout_hierarchyRebind(object, hierarchy) { + d3.rebind(object, hierarchy, "sort", "children", "value"); + object.nodes = object; + object.links = d3_layout_hierarchyLinks; + return object; + } + function d3_layout_hierarchyVisitBefore(node, callback) { + var nodes = [ node ]; + while ((node = nodes.pop()) != null) { + callback(node); + if ((children = node.children) && (n = children.length)) { + var n, children; + while (--n >= 0) nodes.push(children[n]); + } + } + } + function d3_layout_hierarchyVisitAfter(node, callback) { + var nodes = [ node ], nodes2 = []; + while ((node = nodes.pop()) != null) { + nodes2.push(node); + if ((children = node.children) && (n = children.length)) { + var i = -1, n, children; + while (++i < n) nodes.push(children[i]); + } + } + while ((node = nodes2.pop()) != null) { + callback(node); + } + } + function d3_layout_hierarchyChildren(d) { + return d.children; + } + function d3_layout_hierarchyValue(d) { + return d.value; + } + function d3_layout_hierarchySort(a, b) { + return b.value - a.value; + } + function d3_layout_hierarchyLinks(nodes) { + return d3.merge(nodes.map(function(parent) { + return (parent.children || []).map(function(child) { + return { + source: parent, + target: child + }; + }); + })); + } + d3.layout.partition = function() { + var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ]; + function position(node, x, dx, dy) { + var children = node.children; + node.x = x; + node.y = node.depth * dy; + node.dx = dx; + node.dy = dy; + if (children && (n = children.length)) { + var i = -1, n, c, d; + dx = node.value ? dx / node.value : 0; + while (++i < n) { + position(c = children[i], x, d = c.value * dx, dy); + x += d; + } + } + } + function depth(node) { + var children = node.children, d = 0; + if (children && (n = children.length)) { + var i = -1, n; + while (++i < n) d = Math.max(d, depth(children[i])); + } + return 1 + d; + } + function partition(d, i) { + var nodes = hierarchy.call(this, d, i); + position(nodes[0], 0, size[0], size[1] / depth(nodes[0])); + return nodes; + } + partition.size = function(x) { + if (!arguments.length) return size; + size = x; + return partition; + }; + return d3_layout_hierarchyRebind(partition, hierarchy); + }; + d3.layout.pie = function() { + var value = Number, sort = d3_layout_pieSortByValue, startAngle = 0, endAngle = τ, padAngle = 0; + function pie(data) { + var n = data.length, values = data.map(function(d, i) { + return +value.call(pie, d, i); + }), a = +(typeof startAngle === "function" ? startAngle.apply(this, arguments) : startAngle), da = (typeof endAngle === "function" ? endAngle.apply(this, arguments) : endAngle) - a, p = Math.min(Math.abs(da) / n, +(typeof padAngle === "function" ? padAngle.apply(this, arguments) : padAngle)), pa = p * (da < 0 ? -1 : 1), sum = d3.sum(values), k = sum ? (da - n * pa) / sum : 0, index = d3.range(n), arcs = [], v; + if (sort != null) index.sort(sort === d3_layout_pieSortByValue ? function(i, j) { + return values[j] - values[i]; + } : function(i, j) { + return sort(data[i], data[j]); + }); + index.forEach(function(i) { + arcs[i] = { + data: data[i], + value: v = values[i], + startAngle: a, + endAngle: a += v * k + pa, + padAngle: p + }; + }); + return arcs; + } + pie.value = function(_) { + if (!arguments.length) return value; + value = _; + return pie; + }; + pie.sort = function(_) { + if (!arguments.length) return sort; + sort = _; + return pie; + }; + pie.startAngle = function(_) { + if (!arguments.length) return startAngle; + startAngle = _; + return pie; + }; + pie.endAngle = function(_) { + if (!arguments.length) return endAngle; + endAngle = _; + return pie; + }; + pie.padAngle = function(_) { + if (!arguments.length) return padAngle; + padAngle = _; + return pie; + }; + return pie; + }; + var d3_layout_pieSortByValue = {}; + d3.layout.stack = function() { + var values = d3_identity, order = d3_layout_stackOrderDefault, offset = d3_layout_stackOffsetZero, out = d3_layout_stackOut, x = d3_layout_stackX, y = d3_layout_stackY; + function stack(data, index) { + if (!(n = data.length)) return data; + var series = data.map(function(d, i) { + return values.call(stack, d, i); + }); + var points = series.map(function(d) { + return d.map(function(v, i) { + return [ x.call(stack, v, i), y.call(stack, v, i) ]; + }); + }); + var orders = order.call(stack, points, index); + series = d3.permute(series, orders); + points = d3.permute(points, orders); + var offsets = offset.call(stack, points, index); + var m = series[0].length, n, i, j, o; + for (j = 0; j < m; ++j) { + out.call(stack, series[0][j], o = offsets[j], points[0][j][1]); + for (i = 1; i < n; ++i) { + out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]); + } + } + return data; + } + stack.values = function(x) { + if (!arguments.length) return values; + values = x; + return stack; + }; + stack.order = function(x) { + if (!arguments.length) return order; + order = typeof x === "function" ? x : d3_layout_stackOrders.get(x) || d3_layout_stackOrderDefault; + return stack; + }; + stack.offset = function(x) { + if (!arguments.length) return offset; + offset = typeof x === "function" ? x : d3_layout_stackOffsets.get(x) || d3_layout_stackOffsetZero; + return stack; + }; + stack.x = function(z) { + if (!arguments.length) return x; + x = z; + return stack; + }; + stack.y = function(z) { + if (!arguments.length) return y; + y = z; + return stack; + }; + stack.out = function(z) { + if (!arguments.length) return out; + out = z; + return stack; + }; + return stack; + }; + function d3_layout_stackX(d) { + return d.x; + } + function d3_layout_stackY(d) { + return d.y; + } + function d3_layout_stackOut(d, y0, y) { + d.y0 = y0; + d.y = y; + } + var d3_layout_stackOrders = d3.map({ + "inside-out": function(data) { + var n = data.length, i, j, max = data.map(d3_layout_stackMaxIndex), sums = data.map(d3_layout_stackReduceSum), index = d3.range(n).sort(function(a, b) { + return max[a] - max[b]; + }), top = 0, bottom = 0, tops = [], bottoms = []; + for (i = 0; i < n; ++i) { + j = index[i]; + if (top < bottom) { + top += sums[j]; + tops.push(j); + } else { + bottom += sums[j]; + bottoms.push(j); + } + } + return bottoms.reverse().concat(tops); + }, + reverse: function(data) { + return d3.range(data.length).reverse(); + }, + "default": d3_layout_stackOrderDefault + }); + var d3_layout_stackOffsets = d3.map({ + silhouette: function(data) { + var n = data.length, m = data[0].length, sums = [], max = 0, i, j, o, y0 = []; + for (j = 0; j < m; ++j) { + for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; + if (o > max) max = o; + sums.push(o); + } + for (j = 0; j < m; ++j) { + y0[j] = (max - sums[j]) / 2; + } + return y0; + }, + wiggle: function(data) { + var n = data.length, x = data[0], m = x.length, i, j, k, s1, s2, s3, dx, o, o0, y0 = []; + y0[0] = o = o0 = 0; + for (j = 1; j < m; ++j) { + for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1]; + for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) { + for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) { + s3 += (data[k][j][1] - data[k][j - 1][1]) / dx; + } + s2 += s3 * data[i][j][1]; + } + y0[j] = o -= s1 ? s2 / s1 * dx : 0; + if (o < o0) o0 = o; + } + for (j = 0; j < m; ++j) y0[j] -= o0; + return y0; + }, + expand: function(data) { + var n = data.length, m = data[0].length, k = 1 / n, i, j, o, y0 = []; + for (j = 0; j < m; ++j) { + for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; + if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; else for (i = 0; i < n; i++) data[i][j][1] = k; + } + for (j = 0; j < m; ++j) y0[j] = 0; + return y0; + }, + zero: d3_layout_stackOffsetZero + }); + function d3_layout_stackOrderDefault(data) { + return d3.range(data.length); + } + function d3_layout_stackOffsetZero(data) { + var j = -1, m = data[0].length, y0 = []; + while (++j < m) y0[j] = 0; + return y0; + } + function d3_layout_stackMaxIndex(array) { + var i = 1, j = 0, v = array[0][1], k, n = array.length; + for (;i < n; ++i) { + if ((k = array[i][1]) > v) { + j = i; + v = k; + } + } + return j; + } + function d3_layout_stackReduceSum(d) { + return d.reduce(d3_layout_stackSum, 0); + } + function d3_layout_stackSum(p, d) { + return p + d[1]; + } + d3.layout.histogram = function() { + var frequency = true, valuer = Number, ranger = d3_layout_histogramRange, binner = d3_layout_histogramBinSturges; + function histogram(data, i) { + var bins = [], values = data.map(valuer, this), range = ranger.call(this, values, i), thresholds = binner.call(this, range, values, i), bin, i = -1, n = values.length, m = thresholds.length - 1, k = frequency ? 1 : 1 / n, x; + while (++i < m) { + bin = bins[i] = []; + bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]); + bin.y = 0; + } + if (m > 0) { + i = -1; + while (++i < n) { + x = values[i]; + if (x >= range[0] && x <= range[1]) { + bin = bins[d3.bisect(thresholds, x, 1, m) - 1]; + bin.y += k; + bin.push(data[i]); + } + } + } + return bins; + } + histogram.value = function(x) { + if (!arguments.length) return valuer; + valuer = x; + return histogram; + }; + histogram.range = function(x) { + if (!arguments.length) return ranger; + ranger = d3_functor(x); + return histogram; + }; + histogram.bins = function(x) { + if (!arguments.length) return binner; + binner = typeof x === "number" ? function(range) { + return d3_layout_histogramBinFixed(range, x); + } : d3_functor(x); + return histogram; + }; + histogram.frequency = function(x) { + if (!arguments.length) return frequency; + frequency = !!x; + return histogram; + }; + return histogram; + }; + function d3_layout_histogramBinSturges(range, values) { + return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1)); + } + function d3_layout_histogramBinFixed(range, n) { + var x = -1, b = +range[0], m = (range[1] - b) / n, f = []; + while (++x <= n) f[x] = m * x + b; + return f; + } + function d3_layout_histogramRange(values) { + return [ d3.min(values), d3.max(values) ]; + } + d3.layout.pack = function() { + var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ], radius; + function pack(d, i) { + var nodes = hierarchy.call(this, d, i), root = nodes[0], w = size[0], h = size[1], r = radius == null ? Math.sqrt : typeof radius === "function" ? radius : function() { + return radius; + }; + root.x = root.y = 0; + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r = +r(d.value); + }); + d3_layout_hierarchyVisitAfter(root, d3_layout_packSiblings); + if (padding) { + var dr = padding * (radius ? 1 : Math.max(2 * root.r / w, 2 * root.r / h)) / 2; + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r += dr; + }); + d3_layout_hierarchyVisitAfter(root, d3_layout_packSiblings); + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r -= dr; + }); + } + d3_layout_packTransform(root, w / 2, h / 2, radius ? 1 : 1 / Math.max(2 * root.r / w, 2 * root.r / h)); + return nodes; + } + pack.size = function(_) { + if (!arguments.length) return size; + size = _; + return pack; + }; + pack.radius = function(_) { + if (!arguments.length) return radius; + radius = _ == null || typeof _ === "function" ? _ : +_; + return pack; + }; + pack.padding = function(_) { + if (!arguments.length) return padding; + padding = +_; + return pack; + }; + return d3_layout_hierarchyRebind(pack, hierarchy); + }; + function d3_layout_packSort(a, b) { + return a.value - b.value; + } + function d3_layout_packInsert(a, b) { + var c = a._pack_next; + a._pack_next = b; + b._pack_prev = a; + b._pack_next = c; + c._pack_prev = b; + } + function d3_layout_packSplice(a, b) { + a._pack_next = b; + b._pack_prev = a; + } + function d3_layout_packIntersects(a, b) { + var dx = b.x - a.x, dy = b.y - a.y, dr = a.r + b.r; + return .999 * dr * dr > dx * dx + dy * dy; + } + function d3_layout_packSiblings(node) { + if (!(nodes = node.children) || !(n = nodes.length)) return; + var nodes, xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, a, b, c, i, j, k, n; + function bound(node) { + xMin = Math.min(node.x - node.r, xMin); + xMax = Math.max(node.x + node.r, xMax); + yMin = Math.min(node.y - node.r, yMin); + yMax = Math.max(node.y + node.r, yMax); + } + nodes.forEach(d3_layout_packLink); + a = nodes[0]; + a.x = -a.r; + a.y = 0; + bound(a); + if (n > 1) { + b = nodes[1]; + b.x = b.r; + b.y = 0; + bound(b); + if (n > 2) { + c = nodes[2]; + d3_layout_packPlace(a, b, c); + bound(c); + d3_layout_packInsert(a, c); + a._pack_prev = c; + d3_layout_packInsert(c, b); + b = a._pack_next; + for (i = 3; i < n; i++) { + d3_layout_packPlace(a, b, c = nodes[i]); + var isect = 0, s1 = 1, s2 = 1; + for (j = b._pack_next; j !== b; j = j._pack_next, s1++) { + if (d3_layout_packIntersects(j, c)) { + isect = 1; + break; + } + } + if (isect == 1) { + for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) { + if (d3_layout_packIntersects(k, c)) { + break; + } + } + } + if (isect) { + if (s1 < s2 || s1 == s2 && b.r < a.r) d3_layout_packSplice(a, b = j); else d3_layout_packSplice(a = k, b); + i--; + } else { + d3_layout_packInsert(a, c); + b = c; + bound(c); + } + } + } + } + var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cr = 0; + for (i = 0; i < n; i++) { + c = nodes[i]; + c.x -= cx; + c.y -= cy; + cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y)); + } + node.r = cr; + nodes.forEach(d3_layout_packUnlink); + } + function d3_layout_packLink(node) { + node._pack_next = node._pack_prev = node; + } + function d3_layout_packUnlink(node) { + delete node._pack_next; + delete node._pack_prev; + } + function d3_layout_packTransform(node, x, y, k) { + var children = node.children; + node.x = x += k * node.x; + node.y = y += k * node.y; + node.r *= k; + if (children) { + var i = -1, n = children.length; + while (++i < n) d3_layout_packTransform(children[i], x, y, k); + } + } + function d3_layout_packPlace(a, b, c) { + var db = a.r + c.r, dx = b.x - a.x, dy = b.y - a.y; + if (db && (dx || dy)) { + var da = b.r + c.r, dc = dx * dx + dy * dy; + da *= da; + db *= db; + var x = .5 + (db - da) / (2 * dc), y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc); + c.x = a.x + x * dx + y * dy; + c.y = a.y + x * dy - y * dx; + } else { + c.x = a.x + db; + c.y = a.y; + } + } + d3.layout.tree = function() { + var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = null; + function tree(d, i) { + var nodes = hierarchy.call(this, d, i), root0 = nodes[0], root1 = wrapTree(root0); + d3_layout_hierarchyVisitAfter(root1, firstWalk), root1.parent.m = -root1.z; + d3_layout_hierarchyVisitBefore(root1, secondWalk); + if (nodeSize) d3_layout_hierarchyVisitBefore(root0, sizeNode); else { + var left = root0, right = root0, bottom = root0; + d3_layout_hierarchyVisitBefore(root0, function(node) { + if (node.x < left.x) left = node; + if (node.x > right.x) right = node; + if (node.depth > bottom.depth) bottom = node; + }); + var tx = separation(left, right) / 2 - left.x, kx = size[0] / (right.x + separation(right, left) / 2 + tx), ky = size[1] / (bottom.depth || 1); + d3_layout_hierarchyVisitBefore(root0, function(node) { + node.x = (node.x + tx) * kx; + node.y = node.depth * ky; + }); + } + return nodes; + } + function wrapTree(root0) { + var root1 = { + A: null, + children: [ root0 ] + }, queue = [ root1 ], node1; + while ((node1 = queue.pop()) != null) { + for (var children = node1.children, child, i = 0, n = children.length; i < n; ++i) { + queue.push((children[i] = child = { + _: children[i], + parent: node1, + children: (child = children[i].children) && child.slice() || [], + A: null, + a: null, + z: 0, + m: 0, + c: 0, + s: 0, + t: null, + i: i + }).a = child); + } + } + return root1.children[0]; + } + function firstWalk(v) { + var children = v.children, siblings = v.parent.children, w = v.i ? siblings[v.i - 1] : null; + if (children.length) { + d3_layout_treeShift(v); + var midpoint = (children[0].z + children[children.length - 1].z) / 2; + if (w) { + v.z = w.z + separation(v._, w._); + v.m = v.z - midpoint; + } else { + v.z = midpoint; + } + } else if (w) { + v.z = w.z + separation(v._, w._); + } + v.parent.A = apportion(v, w, v.parent.A || siblings[0]); + } + function secondWalk(v) { + v._.x = v.z + v.parent.m; + v.m += v.parent.m; + } + function apportion(v, w, ancestor) { + if (w) { + var vip = v, vop = v, vim = w, vom = vip.parent.children[0], sip = vip.m, sop = vop.m, sim = vim.m, som = vom.m, shift; + while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) { + vom = d3_layout_treeLeft(vom); + vop = d3_layout_treeRight(vop); + vop.a = v; + shift = vim.z + sim - vip.z - sip + separation(vim._, vip._); + if (shift > 0) { + d3_layout_treeMove(d3_layout_treeAncestor(vim, v, ancestor), v, shift); + sip += shift; + sop += shift; + } + sim += vim.m; + sip += vip.m; + som += vom.m; + sop += vop.m; + } + if (vim && !d3_layout_treeRight(vop)) { + vop.t = vim; + vop.m += sim - sop; + } + if (vip && !d3_layout_treeLeft(vom)) { + vom.t = vip; + vom.m += sip - som; + ancestor = v; + } + } + return ancestor; + } + function sizeNode(node) { + node.x *= size[0]; + node.y = node.depth * size[1]; + } + tree.separation = function(x) { + if (!arguments.length) return separation; + separation = x; + return tree; + }; + tree.size = function(x) { + if (!arguments.length) return nodeSize ? null : size; + nodeSize = (size = x) == null ? sizeNode : null; + return tree; + }; + tree.nodeSize = function(x) { + if (!arguments.length) return nodeSize ? size : null; + nodeSize = (size = x) == null ? null : sizeNode; + return tree; + }; + return d3_layout_hierarchyRebind(tree, hierarchy); + }; + function d3_layout_treeSeparation(a, b) { + return a.parent == b.parent ? 1 : 2; + } + function d3_layout_treeLeft(v) { + var children = v.children; + return children.length ? children[0] : v.t; + } + function d3_layout_treeRight(v) { + var children = v.children, n; + return (n = children.length) ? children[n - 1] : v.t; + } + function d3_layout_treeMove(wm, wp, shift) { + var change = shift / (wp.i - wm.i); + wp.c -= change; + wp.s += shift; + wm.c += change; + wp.z += shift; + wp.m += shift; + } + function d3_layout_treeShift(v) { + var shift = 0, change = 0, children = v.children, i = children.length, w; + while (--i >= 0) { + w = children[i]; + w.z += shift; + w.m += shift; + shift += w.s + (change += w.c); + } + } + function d3_layout_treeAncestor(vim, v, ancestor) { + return vim.a.parent === v.parent ? vim.a : ancestor; + } + d3.layout.cluster = function() { + var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false; + function cluster(d, i) { + var nodes = hierarchy.call(this, d, i), root = nodes[0], previousNode, x = 0; + d3_layout_hierarchyVisitAfter(root, function(node) { + var children = node.children; + if (children && children.length) { + node.x = d3_layout_clusterX(children); + node.y = d3_layout_clusterY(children); + } else { + node.x = previousNode ? x += separation(node, previousNode) : 0; + node.y = 0; + previousNode = node; + } + }); + var left = d3_layout_clusterLeft(root), right = d3_layout_clusterRight(root), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2; + d3_layout_hierarchyVisitAfter(root, nodeSize ? function(node) { + node.x = (node.x - root.x) * size[0]; + node.y = (root.y - node.y) * size[1]; + } : function(node) { + node.x = (node.x - x0) / (x1 - x0) * size[0]; + node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1]; + }); + return nodes; + } + cluster.separation = function(x) { + if (!arguments.length) return separation; + separation = x; + return cluster; + }; + cluster.size = function(x) { + if (!arguments.length) return nodeSize ? null : size; + nodeSize = (size = x) == null; + return cluster; + }; + cluster.nodeSize = function(x) { + if (!arguments.length) return nodeSize ? size : null; + nodeSize = (size = x) != null; + return cluster; + }; + return d3_layout_hierarchyRebind(cluster, hierarchy); + }; + function d3_layout_clusterY(children) { + return 1 + d3.max(children, function(child) { + return child.y; + }); + } + function d3_layout_clusterX(children) { + return children.reduce(function(x, child) { + return x + child.x; + }, 0) / children.length; + } + function d3_layout_clusterLeft(node) { + var children = node.children; + return children && children.length ? d3_layout_clusterLeft(children[0]) : node; + } + function d3_layout_clusterRight(node) { + var children = node.children, n; + return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node; + } + d3.layout.treemap = function() { + var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, mode = "squarify", ratio = .5 * (1 + Math.sqrt(5)); + function scale(children, k) { + var i = -1, n = children.length, child, area; + while (++i < n) { + area = (child = children[i]).value * (k < 0 ? 0 : k); + child.area = isNaN(area) || area <= 0 ? 0 : area; + } + } + function squarify(node) { + var children = node.children; + if (children && children.length) { + var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n; + scale(remaining, rect.dx * rect.dy / node.value); + row.area = 0; + while ((n = remaining.length) > 0) { + row.push(child = remaining[n - 1]); + row.area += child.area; + if (mode !== "squarify" || (score = worst(row, u)) <= best) { + remaining.pop(); + best = score; + } else { + row.area -= row.pop().area; + position(row, u, rect, false); + u = Math.min(rect.dx, rect.dy); + row.length = row.area = 0; + best = Infinity; + } + } + if (row.length) { + position(row, u, rect, true); + row.length = row.area = 0; + } + children.forEach(squarify); + } + } + function stickify(node) { + var children = node.children; + if (children && children.length) { + var rect = pad(node), remaining = children.slice(), child, row = []; + scale(remaining, rect.dx * rect.dy / node.value); + row.area = 0; + while (child = remaining.pop()) { + row.push(child); + row.area += child.area; + if (child.z != null) { + position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length); + row.length = row.area = 0; + } + } + children.forEach(stickify); + } + } + function worst(row, u) { + var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length; + while (++i < n) { + if (!(r = row[i].area)) continue; + if (r < rmin) rmin = r; + if (r > rmax) rmax = r; + } + s *= s; + u *= u; + return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity; + } + function position(row, u, rect, flush) { + var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o; + if (u == rect.dx) { + if (flush || v > rect.dy) v = rect.dy; + while (++i < n) { + o = row[i]; + o.x = x; + o.y = y; + o.dy = v; + x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0); + } + o.z = true; + o.dx += rect.x + rect.dx - x; + rect.y += v; + rect.dy -= v; + } else { + if (flush || v > rect.dx) v = rect.dx; + while (++i < n) { + o = row[i]; + o.x = x; + o.y = y; + o.dx = v; + y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0); + } + o.z = false; + o.dy += rect.y + rect.dy - y; + rect.x += v; + rect.dx -= v; + } + } + function treemap(d) { + var nodes = stickies || hierarchy(d), root = nodes[0]; + root.x = root.y = 0; + if (root.value) root.dx = size[0], root.dy = size[1]; else root.dx = root.dy = 0; + if (stickies) hierarchy.revalue(root); + scale([ root ], root.dx * root.dy / root.value); + (stickies ? stickify : squarify)(root); + if (sticky) stickies = nodes; + return nodes; + } + treemap.size = function(x) { + if (!arguments.length) return size; + size = x; + return treemap; + }; + treemap.padding = function(x) { + if (!arguments.length) return padding; + function padFunction(node) { + var p = x.call(treemap, node, node.depth); + return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === "number" ? [ p, p, p, p ] : p); + } + function padConstant(node) { + return d3_layout_treemapPad(node, x); + } + var type; + pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === "function" ? padFunction : type === "number" ? (x = [ x, x, x, x ], + padConstant) : padConstant; + return treemap; + }; + treemap.round = function(x) { + if (!arguments.length) return round != Number; + round = x ? Math.round : Number; + return treemap; + }; + treemap.sticky = function(x) { + if (!arguments.length) return sticky; + sticky = x; + stickies = null; + return treemap; + }; + treemap.ratio = function(x) { + if (!arguments.length) return ratio; + ratio = x; + return treemap; + }; + treemap.mode = function(x) { + if (!arguments.length) return mode; + mode = x + ""; + return treemap; + }; + return d3_layout_hierarchyRebind(treemap, hierarchy); + }; + function d3_layout_treemapPadNull(node) { + return { + x: node.x, + y: node.y, + dx: node.dx, + dy: node.dy + }; + } + function d3_layout_treemapPad(node, padding) { + var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2]; + if (dx < 0) { + x += dx / 2; + dx = 0; + } + if (dy < 0) { + y += dy / 2; + dy = 0; + } + return { + x: x, + y: y, + dx: dx, + dy: dy + }; + } + d3.random = { + normal: function(µ, σ) { + var n = arguments.length; + if (n < 2) σ = 1; + if (n < 1) µ = 0; + return function() { + var x, y, r; + do { + x = Math.random() * 2 - 1; + y = Math.random() * 2 - 1; + r = x * x + y * y; + } while (!r || r > 1); + return µ + σ * x * Math.sqrt(-2 * Math.log(r) / r); + }; + }, + logNormal: function() { + var random = d3.random.normal.apply(d3, arguments); + return function() { + return Math.exp(random()); + }; + }, + bates: function(m) { + var random = d3.random.irwinHall(m); + return function() { + return random() / m; + }; + }, + irwinHall: function(m) { + return function() { + for (var s = 0, j = 0; j < m; j++) s += Math.random(); + return s; + }; + } + }; + d3.scale = {}; + function d3_scaleExtent(domain) { + var start = domain[0], stop = domain[domain.length - 1]; + return start < stop ? [ start, stop ] : [ stop, start ]; + } + function d3_scaleRange(scale) { + return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range()); + } + function d3_scale_bilinear(domain, range, uninterpolate, interpolate) { + var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]); + return function(x) { + return i(u(x)); + }; + } + function d3_scale_nice(domain, nice) { + var i0 = 0, i1 = domain.length - 1, x0 = domain[i0], x1 = domain[i1], dx; + if (x1 < x0) { + dx = i0, i0 = i1, i1 = dx; + dx = x0, x0 = x1, x1 = dx; + } + domain[i0] = nice.floor(x0); + domain[i1] = nice.ceil(x1); + return domain; + } + function d3_scale_niceStep(step) { + return step ? { + floor: function(x) { + return Math.floor(x / step) * step; + }, + ceil: function(x) { + return Math.ceil(x / step) * step; + } + } : d3_scale_niceIdentity; + } + var d3_scale_niceIdentity = { + floor: d3_identity, + ceil: d3_identity + }; + function d3_scale_polylinear(domain, range, uninterpolate, interpolate) { + var u = [], i = [], j = 0, k = Math.min(domain.length, range.length) - 1; + if (domain[k] < domain[0]) { + domain = domain.slice().reverse(); + range = range.slice().reverse(); + } + while (++j <= k) { + u.push(uninterpolate(domain[j - 1], domain[j])); + i.push(interpolate(range[j - 1], range[j])); + } + return function(x) { + var j = d3.bisect(domain, x, 1, k) - 1; + return i[j](u[j](x)); + }; + } + d3.scale.linear = function() { + return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3_interpolate, false); + }; + function d3_scale_linear(domain, range, interpolate, clamp) { + var output, input; + function rescale() { + var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber; + output = linear(domain, range, uninterpolate, interpolate); + input = linear(range, domain, uninterpolate, d3_interpolate); + return scale; + } + function scale(x) { + return output(x); + } + scale.invert = function(y) { + return input(y); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = x.map(Number); + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.rangeRound = function(x) { + return scale.range(x).interpolate(d3_interpolateRound); + }; + scale.clamp = function(x) { + if (!arguments.length) return clamp; + clamp = x; + return rescale(); + }; + scale.interpolate = function(x) { + if (!arguments.length) return interpolate; + interpolate = x; + return rescale(); + }; + scale.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + scale.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + scale.nice = function(m) { + d3_scale_linearNice(domain, m); + return rescale(); + }; + scale.copy = function() { + return d3_scale_linear(domain, range, interpolate, clamp); + }; + return rescale(); + } + function d3_scale_linearRebind(scale, linear) { + return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp"); + } + function d3_scale_linearNice(domain, m) { + d3_scale_nice(domain, d3_scale_niceStep(d3_scale_linearTickRange(domain, m)[2])); + d3_scale_nice(domain, d3_scale_niceStep(d3_scale_linearTickRange(domain, m)[2])); + return domain; + } + function d3_scale_linearTickRange(domain, m) { + if (m == null) m = 10; + var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step; + if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2; + extent[0] = Math.ceil(extent[0] / step) * step; + extent[1] = Math.floor(extent[1] / step) * step + step * .5; + extent[2] = step; + return extent; + } + function d3_scale_linearTicks(domain, m) { + return d3.range.apply(d3, d3_scale_linearTickRange(domain, m)); + } + function d3_scale_linearTickFormat(domain, m, format) { + var range = d3_scale_linearTickRange(domain, m); + if (format) { + var match = d3_format_re.exec(format); + match.shift(); + if (match[8] === "s") { + var prefix = d3.formatPrefix(Math.max(abs(range[0]), abs(range[1]))); + if (!match[7]) match[7] = "." + d3_scale_linearPrecision(prefix.scale(range[2])); + match[8] = "f"; + format = d3.format(match.join("")); + return function(d) { + return format(prefix.scale(d)) + prefix.symbol; + }; + } + if (!match[7]) match[7] = "." + d3_scale_linearFormatPrecision(match[8], range); + format = match.join(""); + } else { + format = ",." + d3_scale_linearPrecision(range[2]) + "f"; + } + return d3.format(format); + } + var d3_scale_linearFormatSignificant = { + s: 1, + g: 1, + p: 1, + r: 1, + e: 1 + }; + function d3_scale_linearPrecision(value) { + return -Math.floor(Math.log(value) / Math.LN10 + .01); + } + function d3_scale_linearFormatPrecision(type, range) { + var p = d3_scale_linearPrecision(range[2]); + return type in d3_scale_linearFormatSignificant ? Math.abs(p - d3_scale_linearPrecision(Math.max(abs(range[0]), abs(range[1])))) + +(type !== "e") : p - (type === "%") * 2; + } + d3.scale.log = function() { + return d3_scale_log(d3.scale.linear().domain([ 0, 1 ]), 10, true, [ 1, 10 ]); + }; + function d3_scale_log(linear, base, positive, domain) { + function log(x) { + return (positive ? Math.log(x < 0 ? 0 : x) : -Math.log(x > 0 ? 0 : -x)) / Math.log(base); + } + function pow(x) { + return positive ? Math.pow(base, x) : -Math.pow(base, -x); + } + function scale(x) { + return linear(log(x)); + } + scale.invert = function(x) { + return pow(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + positive = x[0] >= 0; + linear.domain((domain = x.map(Number)).map(log)); + return scale; + }; + scale.base = function(_) { + if (!arguments.length) return base; + base = +_; + linear.domain(domain.map(log)); + return scale; + }; + scale.nice = function() { + var niced = d3_scale_nice(domain.map(log), positive ? Math : d3_scale_logNiceNegative); + linear.domain(niced); + domain = niced.map(pow); + return scale; + }; + scale.ticks = function() { + var extent = d3_scaleExtent(domain), ticks = [], u = extent[0], v = extent[1], i = Math.floor(log(u)), j = Math.ceil(log(v)), n = base % 1 ? 2 : base; + if (isFinite(j - i)) { + if (positive) { + for (;i < j; i++) for (var k = 1; k < n; k++) ticks.push(pow(i) * k); + ticks.push(pow(i)); + } else { + ticks.push(pow(i)); + for (;i++ < j; ) for (var k = n - 1; k > 0; k--) ticks.push(pow(i) * k); + } + for (i = 0; ticks[i] < u; i++) {} + for (j = ticks.length; ticks[j - 1] > v; j--) {} + ticks = ticks.slice(i, j); + } + return ticks; + }; + scale.tickFormat = function(n, format) { + if (!arguments.length) return d3_scale_logFormat; + if (arguments.length < 2) format = d3_scale_logFormat; else if (typeof format !== "function") format = d3.format(format); + var k = Math.max(1, base * n / scale.ticks().length); + return function(d) { + var i = d / pow(Math.round(log(d))); + if (i * base < base - .5) i *= base; + return i <= k ? format(d) : ""; + }; + }; + scale.copy = function() { + return d3_scale_log(linear.copy(), base, positive, domain); + }; + return d3_scale_linearRebind(scale, linear); + } + var d3_scale_logFormat = d3.format(".0e"), d3_scale_logNiceNegative = { + floor: function(x) { + return -Math.ceil(-x); + }, + ceil: function(x) { + return -Math.floor(-x); + } + }; + d3.scale.pow = function() { + return d3_scale_pow(d3.scale.linear(), 1, [ 0, 1 ]); + }; + function d3_scale_pow(linear, exponent, domain) { + var powp = d3_scale_powPow(exponent), powb = d3_scale_powPow(1 / exponent); + function scale(x) { + return linear(powp(x)); + } + scale.invert = function(x) { + return powb(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + linear.domain((domain = x.map(Number)).map(powp)); + return scale; + }; + scale.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + scale.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + scale.nice = function(m) { + return scale.domain(d3_scale_linearNice(domain, m)); + }; + scale.exponent = function(x) { + if (!arguments.length) return exponent; + powp = d3_scale_powPow(exponent = x); + powb = d3_scale_powPow(1 / exponent); + linear.domain(domain.map(powp)); + return scale; + }; + scale.copy = function() { + return d3_scale_pow(linear.copy(), exponent, domain); + }; + return d3_scale_linearRebind(scale, linear); + } + function d3_scale_powPow(e) { + return function(x) { + return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e); + }; + } + d3.scale.sqrt = function() { + return d3.scale.pow().exponent(.5); + }; + d3.scale.ordinal = function() { + return d3_scale_ordinal([], { + t: "range", + a: [ [] ] + }); + }; + function d3_scale_ordinal(domain, ranger) { + var index, range, rangeBand; + function scale(x) { + return range[((index.get(x) || (ranger.t === "range" ? index.set(x, domain.push(x)) : NaN)) - 1) % range.length]; + } + function steps(start, step) { + return d3.range(domain.length).map(function(i) { + return start + step * i; + }); + } + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = []; + index = new d3_Map(); + var i = -1, n = x.length, xi; + while (++i < n) if (!index.has(xi = x[i])) index.set(xi, domain.push(xi)); + return scale[ranger.t].apply(scale, ranger.a); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + rangeBand = 0; + ranger = { + t: "range", + a: arguments + }; + return scale; + }; + scale.rangePoints = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], stop = x[1], step = domain.length < 2 ? (start = (start + stop) / 2, + 0) : (stop - start) / (domain.length - 1 + padding); + range = steps(start + step * padding / 2, step); + rangeBand = 0; + ranger = { + t: "rangePoints", + a: arguments + }; + return scale; + }; + scale.rangeRoundPoints = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], stop = x[1], step = domain.length < 2 ? (start = stop = Math.round((start + stop) / 2), + 0) : (stop - start) / (domain.length - 1 + padding) | 0; + range = steps(start + Math.round(step * padding / 2 + (stop - start - (domain.length - 1 + padding) * step) / 2), step); + rangeBand = 0; + ranger = { + t: "rangeRoundPoints", + a: arguments + }; + return scale; + }; + scale.rangeBands = function(x, padding, outerPadding) { + if (arguments.length < 2) padding = 0; + if (arguments.length < 3) outerPadding = padding; + var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = (stop - start) / (domain.length - padding + 2 * outerPadding); + range = steps(start + step * outerPadding, step); + if (reverse) range.reverse(); + rangeBand = step * (1 - padding); + ranger = { + t: "rangeBands", + a: arguments + }; + return scale; + }; + scale.rangeRoundBands = function(x, padding, outerPadding) { + if (arguments.length < 2) padding = 0; + if (arguments.length < 3) outerPadding = padding; + var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = Math.floor((stop - start) / (domain.length - padding + 2 * outerPadding)); + range = steps(start + Math.round((stop - start - (domain.length - padding) * step) / 2), step); + if (reverse) range.reverse(); + rangeBand = Math.round(step * (1 - padding)); + ranger = { + t: "rangeRoundBands", + a: arguments + }; + return scale; + }; + scale.rangeBand = function() { + return rangeBand; + }; + scale.rangeExtent = function() { + return d3_scaleExtent(ranger.a[0]); + }; + scale.copy = function() { + return d3_scale_ordinal(domain, ranger); + }; + return scale.domain(domain); + } + d3.scale.category10 = function() { + return d3.scale.ordinal().range(d3_category10); + }; + d3.scale.category20 = function() { + return d3.scale.ordinal().range(d3_category20); + }; + d3.scale.category20b = function() { + return d3.scale.ordinal().range(d3_category20b); + }; + d3.scale.category20c = function() { + return d3.scale.ordinal().range(d3_category20c); + }; + var d3_category10 = [ 2062260, 16744206, 2924588, 14034728, 9725885, 9197131, 14907330, 8355711, 12369186, 1556175 ].map(d3_rgbString); + var d3_category20 = [ 2062260, 11454440, 16744206, 16759672, 2924588, 10018698, 14034728, 16750742, 9725885, 12955861, 9197131, 12885140, 14907330, 16234194, 8355711, 13092807, 12369186, 14408589, 1556175, 10410725 ].map(d3_rgbString); + var d3_category20b = [ 3750777, 5395619, 7040719, 10264286, 6519097, 9216594, 11915115, 13556636, 9202993, 12426809, 15186514, 15190932, 8666169, 11356490, 14049643, 15177372, 8077683, 10834324, 13528509, 14589654 ].map(d3_rgbString); + var d3_category20c = [ 3244733, 7057110, 10406625, 13032431, 15095053, 16616764, 16625259, 16634018, 3253076, 7652470, 10607003, 13101504, 7695281, 10394312, 12369372, 14342891, 6513507, 9868950, 12434877, 14277081 ].map(d3_rgbString); + d3.scale.quantile = function() { + return d3_scale_quantile([], []); + }; + function d3_scale_quantile(domain, range) { + var thresholds; + function rescale() { + var k = 0, q = range.length; + thresholds = []; + while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q); + return scale; + } + function scale(x) { + if (!isNaN(x = +x)) return range[d3.bisect(thresholds, x)]; + } + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = x.map(d3_number).filter(d3_numeric).sort(d3_ascending); + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.quantiles = function() { + return thresholds; + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + return y < 0 ? [ NaN, NaN ] : [ y > 0 ? thresholds[y - 1] : domain[0], y < thresholds.length ? thresholds[y] : domain[domain.length - 1] ]; + }; + scale.copy = function() { + return d3_scale_quantile(domain, range); + }; + return rescale(); + } + d3.scale.quantize = function() { + return d3_scale_quantize(0, 1, [ 0, 1 ]); + }; + function d3_scale_quantize(x0, x1, range) { + var kx, i; + function scale(x) { + return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))]; + } + function rescale() { + kx = range.length / (x1 - x0); + i = range.length - 1; + return scale; + } + scale.domain = function(x) { + if (!arguments.length) return [ x0, x1 ]; + x0 = +x[0]; + x1 = +x[x.length - 1]; + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + y = y < 0 ? NaN : y / kx + x0; + return [ y, y + 1 / kx ]; + }; + scale.copy = function() { + return d3_scale_quantize(x0, x1, range); + }; + return rescale(); + } + d3.scale.threshold = function() { + return d3_scale_threshold([ .5 ], [ 0, 1 ]); + }; + function d3_scale_threshold(domain, range) { + function scale(x) { + if (x <= x) return range[d3.bisect(domain, x)]; + } + scale.domain = function(_) { + if (!arguments.length) return domain; + domain = _; + return scale; + }; + scale.range = function(_) { + if (!arguments.length) return range; + range = _; + return scale; + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + return [ domain[y - 1], domain[y] ]; + }; + scale.copy = function() { + return d3_scale_threshold(domain, range); + }; + return scale; + } + d3.scale.identity = function() { + return d3_scale_identity([ 0, 1 ]); + }; + function d3_scale_identity(domain) { + function identity(x) { + return +x; + } + identity.invert = identity; + identity.domain = identity.range = function(x) { + if (!arguments.length) return domain; + domain = x.map(identity); + return identity; + }; + identity.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + identity.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + identity.copy = function() { + return d3_scale_identity(domain); + }; + return identity; + } + d3.svg = {}; + function d3_zero() { + return 0; + } + d3.svg.arc = function() { + var innerRadius = d3_svg_arcInnerRadius, outerRadius = d3_svg_arcOuterRadius, cornerRadius = d3_zero, padRadius = d3_svg_arcAuto, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle, padAngle = d3_svg_arcPadAngle; + function arc() { + var r0 = Math.max(0, +innerRadius.apply(this, arguments)), r1 = Math.max(0, +outerRadius.apply(this, arguments)), a0 = startAngle.apply(this, arguments) - halfπ, a1 = endAngle.apply(this, arguments) - halfπ, da = Math.abs(a1 - a0), cw = a0 > a1 ? 0 : 1; + if (r1 < r0) rc = r1, r1 = r0, r0 = rc; + if (da >= τε) return circleSegment(r1, cw) + (r0 ? circleSegment(r0, 1 - cw) : "") + "Z"; + var rc, cr, rp, ap, p0 = 0, p1 = 0, x0, y0, x1, y1, x2, y2, x3, y3, path = []; + if (ap = (+padAngle.apply(this, arguments) || 0) / 2) { + rp = padRadius === d3_svg_arcAuto ? Math.sqrt(r0 * r0 + r1 * r1) : +padRadius.apply(this, arguments); + if (!cw) p1 *= -1; + if (r1) p1 = d3_asin(rp / r1 * Math.sin(ap)); + if (r0) p0 = d3_asin(rp / r0 * Math.sin(ap)); + } + if (r1) { + x0 = r1 * Math.cos(a0 + p1); + y0 = r1 * Math.sin(a0 + p1); + x1 = r1 * Math.cos(a1 - p1); + y1 = r1 * Math.sin(a1 - p1); + var l1 = Math.abs(a1 - a0 - 2 * p1) <= π ? 0 : 1; + if (p1 && d3_svg_arcSweep(x0, y0, x1, y1) === cw ^ l1) { + var h1 = (a0 + a1) / 2; + x0 = r1 * Math.cos(h1); + y0 = r1 * Math.sin(h1); + x1 = y1 = null; + } + } else { + x0 = y0 = 0; + } + if (r0) { + x2 = r0 * Math.cos(a1 - p0); + y2 = r0 * Math.sin(a1 - p0); + x3 = r0 * Math.cos(a0 + p0); + y3 = r0 * Math.sin(a0 + p0); + var l0 = Math.abs(a0 - a1 + 2 * p0) <= π ? 0 : 1; + if (p0 && d3_svg_arcSweep(x2, y2, x3, y3) === 1 - cw ^ l0) { + var h0 = (a0 + a1) / 2; + x2 = r0 * Math.cos(h0); + y2 = r0 * Math.sin(h0); + x3 = y3 = null; + } + } else { + x2 = y2 = 0; + } + if (da > ε && (rc = Math.min(Math.abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments))) > .001) { + cr = r0 < r1 ^ cw ? 0 : 1; + var rc1 = rc, rc0 = rc; + if (da < π) { + var oc = x3 == null ? [ x2, y2 ] : x1 == null ? [ x0, y0 ] : d3_geom_polygonIntersect([ x0, y0 ], [ x3, y3 ], [ x1, y1 ], [ x2, y2 ]), ax = x0 - oc[0], ay = y0 - oc[1], bx = x1 - oc[0], by = y1 - oc[1], kc = 1 / Math.sin(Math.acos((ax * bx + ay * by) / (Math.sqrt(ax * ax + ay * ay) * Math.sqrt(bx * bx + by * by))) / 2), lc = Math.sqrt(oc[0] * oc[0] + oc[1] * oc[1]); + rc0 = Math.min(rc, (r0 - lc) / (kc - 1)); + rc1 = Math.min(rc, (r1 - lc) / (kc + 1)); + } + if (x1 != null) { + var t30 = d3_svg_arcCornerTangents(x3 == null ? [ x2, y2 ] : [ x3, y3 ], [ x0, y0 ], r1, rc1, cw), t12 = d3_svg_arcCornerTangents([ x1, y1 ], [ x2, y2 ], r1, rc1, cw); + if (rc === rc1) { + path.push("M", t30[0], "A", rc1, ",", rc1, " 0 0,", cr, " ", t30[1], "A", r1, ",", r1, " 0 ", 1 - cw ^ d3_svg_arcSweep(t30[1][0], t30[1][1], t12[1][0], t12[1][1]), ",", cw, " ", t12[1], "A", rc1, ",", rc1, " 0 0,", cr, " ", t12[0]); + } else { + path.push("M", t30[0], "A", rc1, ",", rc1, " 0 1,", cr, " ", t12[0]); + } + } else { + path.push("M", x0, ",", y0); + } + if (x3 != null) { + var t03 = d3_svg_arcCornerTangents([ x0, y0 ], [ x3, y3 ], r0, -rc0, cw), t21 = d3_svg_arcCornerTangents([ x2, y2 ], x1 == null ? [ x0, y0 ] : [ x1, y1 ], r0, -rc0, cw); + if (rc === rc0) { + path.push("L", t21[0], "A", rc0, ",", rc0, " 0 0,", cr, " ", t21[1], "A", r0, ",", r0, " 0 ", cw ^ d3_svg_arcSweep(t21[1][0], t21[1][1], t03[1][0], t03[1][1]), ",", 1 - cw, " ", t03[1], "A", rc0, ",", rc0, " 0 0,", cr, " ", t03[0]); + } else { + path.push("L", t21[0], "A", rc0, ",", rc0, " 0 0,", cr, " ", t03[0]); + } + } else { + path.push("L", x2, ",", y2); + } + } else { + path.push("M", x0, ",", y0); + if (x1 != null) path.push("A", r1, ",", r1, " 0 ", l1, ",", cw, " ", x1, ",", y1); + path.push("L", x2, ",", y2); + if (x3 != null) path.push("A", r0, ",", r0, " 0 ", l0, ",", 1 - cw, " ", x3, ",", y3); + } + path.push("Z"); + return path.join(""); + } + function circleSegment(r1, cw) { + return "M0," + r1 + "A" + r1 + "," + r1 + " 0 1," + cw + " 0," + -r1 + "A" + r1 + "," + r1 + " 0 1," + cw + " 0," + r1; + } + arc.innerRadius = function(v) { + if (!arguments.length) return innerRadius; + innerRadius = d3_functor(v); + return arc; + }; + arc.outerRadius = function(v) { + if (!arguments.length) return outerRadius; + outerRadius = d3_functor(v); + return arc; + }; + arc.cornerRadius = function(v) { + if (!arguments.length) return cornerRadius; + cornerRadius = d3_functor(v); + return arc; + }; + arc.padRadius = function(v) { + if (!arguments.length) return padRadius; + padRadius = v == d3_svg_arcAuto ? d3_svg_arcAuto : d3_functor(v); + return arc; + }; + arc.startAngle = function(v) { + if (!arguments.length) return startAngle; + startAngle = d3_functor(v); + return arc; + }; + arc.endAngle = function(v) { + if (!arguments.length) return endAngle; + endAngle = d3_functor(v); + return arc; + }; + arc.padAngle = function(v) { + if (!arguments.length) return padAngle; + padAngle = d3_functor(v); + return arc; + }; + arc.centroid = function() { + var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2, a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - halfπ; + return [ Math.cos(a) * r, Math.sin(a) * r ]; + }; + return arc; + }; + var d3_svg_arcAuto = "auto"; + function d3_svg_arcInnerRadius(d) { + return d.innerRadius; + } + function d3_svg_arcOuterRadius(d) { + return d.outerRadius; + } + function d3_svg_arcStartAngle(d) { + return d.startAngle; + } + function d3_svg_arcEndAngle(d) { + return d.endAngle; + } + function d3_svg_arcPadAngle(d) { + return d && d.padAngle; + } + function d3_svg_arcSweep(x0, y0, x1, y1) { + return (x0 - x1) * y0 - (y0 - y1) * x0 > 0 ? 0 : 1; + } + function d3_svg_arcCornerTangents(p0, p1, r1, rc, cw) { + var x01 = p0[0] - p1[0], y01 = p0[1] - p1[1], lo = (cw ? rc : -rc) / Math.sqrt(x01 * x01 + y01 * y01), ox = lo * y01, oy = -lo * x01, x1 = p0[0] + ox, y1 = p0[1] + oy, x2 = p1[0] + ox, y2 = p1[1] + oy, x3 = (x1 + x2) / 2, y3 = (y1 + y2) / 2, dx = x2 - x1, dy = y2 - y1, d2 = dx * dx + dy * dy, r = r1 - rc, D = x1 * y2 - x2 * y1, d = (dy < 0 ? -1 : 1) * Math.sqrt(Math.max(0, r * r * d2 - D * D)), cx0 = (D * dy - dx * d) / d2, cy0 = (-D * dx - dy * d) / d2, cx1 = (D * dy + dx * d) / d2, cy1 = (-D * dx + dy * d) / d2, dx0 = cx0 - x3, dy0 = cy0 - y3, dx1 = cx1 - x3, dy1 = cy1 - y3; + if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1; + return [ [ cx0 - ox, cy0 - oy ], [ cx0 * r1 / r, cy0 * r1 / r ] ]; + } + function d3_svg_line(projection) { + var x = d3_geom_pointX, y = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7; + function line(data) { + var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y); + function segment() { + segments.push("M", interpolate(projection(points), tension)); + } + while (++i < n) { + if (defined.call(this, d = data[i], i)) { + points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]); + } else if (points.length) { + segment(); + points = []; + } + } + if (points.length) segment(); + return segments.length ? segments.join("") : null; + } + line.x = function(_) { + if (!arguments.length) return x; + x = _; + return line; + }; + line.y = function(_) { + if (!arguments.length) return y; + y = _; + return line; + }; + line.defined = function(_) { + if (!arguments.length) return defined; + defined = _; + return line; + }; + line.interpolate = function(_) { + if (!arguments.length) return interpolateKey; + if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key; + return line; + }; + line.tension = function(_) { + if (!arguments.length) return tension; + tension = _; + return line; + }; + return line; + } + d3.svg.line = function() { + return d3_svg_line(d3_identity); + }; + var d3_svg_lineInterpolators = d3.map({ + linear: d3_svg_lineLinear, + "linear-closed": d3_svg_lineLinearClosed, + step: d3_svg_lineStep, + "step-before": d3_svg_lineStepBefore, + "step-after": d3_svg_lineStepAfter, + basis: d3_svg_lineBasis, + "basis-open": d3_svg_lineBasisOpen, + "basis-closed": d3_svg_lineBasisClosed, + bundle: d3_svg_lineBundle, + cardinal: d3_svg_lineCardinal, + "cardinal-open": d3_svg_lineCardinalOpen, + "cardinal-closed": d3_svg_lineCardinalClosed, + monotone: d3_svg_lineMonotone + }); + d3_svg_lineInterpolators.forEach(function(key, value) { + value.key = key; + value.closed = /-closed$/.test(key); + }); + function d3_svg_lineLinear(points) { + return points.length > 1 ? points.join("L") : points + "Z"; + } + function d3_svg_lineLinearClosed(points) { + return points.join("L") + "Z"; + } + function d3_svg_lineStep(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("H", (p[0] + (p = points[i])[0]) / 2, "V", p[1]); + if (n > 1) path.push("H", p[0]); + return path.join(""); + } + function d3_svg_lineStepBefore(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("V", (p = points[i])[1], "H", p[0]); + return path.join(""); + } + function d3_svg_lineStepAfter(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("H", (p = points[i])[0], "V", p[1]); + return path.join(""); + } + function d3_svg_lineCardinalOpen(points, tension) { + return points.length < 4 ? d3_svg_lineLinear(points) : points[1] + d3_svg_lineHermite(points.slice(1, -1), d3_svg_lineCardinalTangents(points, tension)); + } + function d3_svg_lineCardinalClosed(points, tension) { + return points.length < 3 ? d3_svg_lineLinearClosed(points) : points[0] + d3_svg_lineHermite((points.push(points[0]), + points), d3_svg_lineCardinalTangents([ points[points.length - 2] ].concat(points, [ points[1] ]), tension)); + } + function d3_svg_lineCardinal(points, tension) { + return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineCardinalTangents(points, tension)); + } + function d3_svg_lineHermite(points, tangents) { + if (tangents.length < 1 || points.length != tangents.length && points.length != tangents.length + 2) { + return d3_svg_lineLinear(points); + } + var quad = points.length != tangents.length, path = "", p0 = points[0], p = points[1], t0 = tangents[0], t = t0, pi = 1; + if (quad) { + path += "Q" + (p[0] - t0[0] * 2 / 3) + "," + (p[1] - t0[1] * 2 / 3) + "," + p[0] + "," + p[1]; + p0 = points[1]; + pi = 2; + } + if (tangents.length > 1) { + t = tangents[1]; + p = points[pi]; + pi++; + path += "C" + (p0[0] + t0[0]) + "," + (p0[1] + t0[1]) + "," + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1]; + for (var i = 2; i < tangents.length; i++, pi++) { + p = points[pi]; + t = tangents[i]; + path += "S" + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1]; + } + } + if (quad) { + var lp = points[pi]; + path += "Q" + (p[0] + t[0] * 2 / 3) + "," + (p[1] + t[1] * 2 / 3) + "," + lp[0] + "," + lp[1]; + } + return path; + } + function d3_svg_lineCardinalTangents(points, tension) { + var tangents = [], a = (1 - tension) / 2, p0, p1 = points[0], p2 = points[1], i = 1, n = points.length; + while (++i < n) { + p0 = p1; + p1 = p2; + p2 = points[i]; + tangents.push([ a * (p2[0] - p0[0]), a * (p2[1] - p0[1]) ]); + } + return tangents; + } + function d3_svg_lineBasis(points) { + if (points.length < 3) return d3_svg_lineLinear(points); + var i = 1, n = points.length, pi = points[0], x0 = pi[0], y0 = pi[1], px = [ x0, x0, x0, (pi = points[1])[0] ], py = [ y0, y0, y0, pi[1] ], path = [ x0, ",", y0, "L", d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ]; + points.push(points[n - 1]); + while (++i <= n) { + pi = points[i]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + points.pop(); + path.push("L", pi); + return path.join(""); + } + function d3_svg_lineBasisOpen(points) { + if (points.length < 4) return d3_svg_lineLinear(points); + var path = [], i = -1, n = points.length, pi, px = [ 0 ], py = [ 0 ]; + while (++i < 3) { + pi = points[i]; + px.push(pi[0]); + py.push(pi[1]); + } + path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + "," + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py)); + --i; + while (++i < n) { + pi = points[i]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + function d3_svg_lineBasisClosed(points) { + var path, i = -1, n = points.length, m = n + 4, pi, px = [], py = []; + while (++i < 4) { + pi = points[i % n]; + px.push(pi[0]); + py.push(pi[1]); + } + path = [ d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ]; + --i; + while (++i < m) { + pi = points[i % n]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + function d3_svg_lineBundle(points, tension) { + var n = points.length - 1; + if (n) { + var x0 = points[0][0], y0 = points[0][1], dx = points[n][0] - x0, dy = points[n][1] - y0, i = -1, p, t; + while (++i <= n) { + p = points[i]; + t = i / n; + p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx); + p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy); + } + } + return d3_svg_lineBasis(points); + } + function d3_svg_lineDot4(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; + } + var d3_svg_lineBasisBezier1 = [ 0, 2 / 3, 1 / 3, 0 ], d3_svg_lineBasisBezier2 = [ 0, 1 / 3, 2 / 3, 0 ], d3_svg_lineBasisBezier3 = [ 0, 1 / 6, 2 / 3, 1 / 6 ]; + function d3_svg_lineBasisBezier(path, x, y) { + path.push("C", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y)); + } + function d3_svg_lineSlope(p0, p1) { + return (p1[1] - p0[1]) / (p1[0] - p0[0]); + } + function d3_svg_lineFiniteDifferences(points) { + var i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = d3_svg_lineSlope(p0, p1); + while (++i < j) { + m[i] = (d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2; + } + m[i] = d; + return m; + } + function d3_svg_lineMonotoneTangents(points) { + var tangents = [], d, a, b, s, m = d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1; + while (++i < j) { + d = d3_svg_lineSlope(points[i], points[i + 1]); + if (abs(d) < ε) { + m[i] = m[i + 1] = 0; + } else { + a = m[i] / d; + b = m[i + 1] / d; + s = a * a + b * b; + if (s > 9) { + s = d * 3 / Math.sqrt(s); + m[i] = s * a; + m[i + 1] = s * b; + } + } + } + i = -1; + while (++i <= j) { + s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m[i] * m[i])); + tangents.push([ s || 0, m[i] * s || 0 ]); + } + return tangents; + } + function d3_svg_lineMonotone(points) { + return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points)); + } + d3.svg.line.radial = function() { + var line = d3_svg_line(d3_svg_lineRadial); + line.radius = line.x, delete line.x; + line.angle = line.y, delete line.y; + return line; + }; + function d3_svg_lineRadial(points) { + var point, i = -1, n = points.length, r, a; + while (++i < n) { + point = points[i]; + r = point[0]; + a = point[1] - halfπ; + point[0] = r * Math.cos(a); + point[1] = r * Math.sin(a); + } + return points; + } + function d3_svg_area(projection) { + var x0 = d3_geom_pointX, x1 = d3_geom_pointX, y0 = 0, y1 = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, interpolateReverse = interpolate, L = "L", tension = .7; + function area(data) { + var segments = [], points0 = [], points1 = [], i = -1, n = data.length, d, fx0 = d3_functor(x0), fy0 = d3_functor(y0), fx1 = x0 === x1 ? function() { + return x; + } : d3_functor(x1), fy1 = y0 === y1 ? function() { + return y; + } : d3_functor(y1), x, y; + function segment() { + segments.push("M", interpolate(projection(points1), tension), L, interpolateReverse(projection(points0.reverse()), tension), "Z"); + } + while (++i < n) { + if (defined.call(this, d = data[i], i)) { + points0.push([ x = +fx0.call(this, d, i), y = +fy0.call(this, d, i) ]); + points1.push([ +fx1.call(this, d, i), +fy1.call(this, d, i) ]); + } else if (points0.length) { + segment(); + points0 = []; + points1 = []; + } + } + if (points0.length) segment(); + return segments.length ? segments.join("") : null; + } + area.x = function(_) { + if (!arguments.length) return x1; + x0 = x1 = _; + return area; + }; + area.x0 = function(_) { + if (!arguments.length) return x0; + x0 = _; + return area; + }; + area.x1 = function(_) { + if (!arguments.length) return x1; + x1 = _; + return area; + }; + area.y = function(_) { + if (!arguments.length) return y1; + y0 = y1 = _; + return area; + }; + area.y0 = function(_) { + if (!arguments.length) return y0; + y0 = _; + return area; + }; + area.y1 = function(_) { + if (!arguments.length) return y1; + y1 = _; + return area; + }; + area.defined = function(_) { + if (!arguments.length) return defined; + defined = _; + return area; + }; + area.interpolate = function(_) { + if (!arguments.length) return interpolateKey; + if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key; + interpolateReverse = interpolate.reverse || interpolate; + L = interpolate.closed ? "M" : "L"; + return area; + }; + area.tension = function(_) { + if (!arguments.length) return tension; + tension = _; + return area; + }; + return area; + } + d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter; + d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore; + d3.svg.area = function() { + return d3_svg_area(d3_identity); + }; + d3.svg.area.radial = function() { + var area = d3_svg_area(d3_svg_lineRadial); + area.radius = area.x, delete area.x; + area.innerRadius = area.x0, delete area.x0; + area.outerRadius = area.x1, delete area.x1; + area.angle = area.y, delete area.y; + area.startAngle = area.y0, delete area.y0; + area.endAngle = area.y1, delete area.y1; + return area; + }; + d3.svg.chord = function() { + var source = d3_source, target = d3_target, radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle; + function chord(d, i) { + var s = subgroup(this, source, d, i), t = subgroup(this, target, d, i); + return "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + "Z"; + } + function subgroup(self, f, d, i) { + var subgroup = f.call(self, d, i), r = radius.call(self, subgroup, i), a0 = startAngle.call(self, subgroup, i) - halfπ, a1 = endAngle.call(self, subgroup, i) - halfπ; + return { + r: r, + a0: a0, + a1: a1, + p0: [ r * Math.cos(a0), r * Math.sin(a0) ], + p1: [ r * Math.cos(a1), r * Math.sin(a1) ] + }; + } + function equals(a, b) { + return a.a0 == b.a0 && a.a1 == b.a1; + } + function arc(r, p, a) { + return "A" + r + "," + r + " 0 " + +(a > π) + ",1 " + p; + } + function curve(r0, p0, r1, p1) { + return "Q 0,0 " + p1; + } + chord.radius = function(v) { + if (!arguments.length) return radius; + radius = d3_functor(v); + return chord; + }; + chord.source = function(v) { + if (!arguments.length) return source; + source = d3_functor(v); + return chord; + }; + chord.target = function(v) { + if (!arguments.length) return target; + target = d3_functor(v); + return chord; + }; + chord.startAngle = function(v) { + if (!arguments.length) return startAngle; + startAngle = d3_functor(v); + return chord; + }; + chord.endAngle = function(v) { + if (!arguments.length) return endAngle; + endAngle = d3_functor(v); + return chord; + }; + return chord; + }; + function d3_svg_chordRadius(d) { + return d.radius; + } + d3.svg.diagonal = function() { + var source = d3_source, target = d3_target, projection = d3_svg_diagonalProjection; + function diagonal(d, i) { + var p0 = source.call(this, d, i), p3 = target.call(this, d, i), m = (p0.y + p3.y) / 2, p = [ p0, { + x: p0.x, + y: m + }, { + x: p3.x, + y: m + }, p3 ]; + p = p.map(projection); + return "M" + p[0] + "C" + p[1] + " " + p[2] + " " + p[3]; + } + diagonal.source = function(x) { + if (!arguments.length) return source; + source = d3_functor(x); + return diagonal; + }; + diagonal.target = function(x) { + if (!arguments.length) return target; + target = d3_functor(x); + return diagonal; + }; + diagonal.projection = function(x) { + if (!arguments.length) return projection; + projection = x; + return diagonal; + }; + return diagonal; + }; + function d3_svg_diagonalProjection(d) { + return [ d.x, d.y ]; + } + d3.svg.diagonal.radial = function() { + var diagonal = d3.svg.diagonal(), projection = d3_svg_diagonalProjection, projection_ = diagonal.projection; + diagonal.projection = function(x) { + return arguments.length ? projection_(d3_svg_diagonalRadialProjection(projection = x)) : projection; + }; + return diagonal; + }; + function d3_svg_diagonalRadialProjection(projection) { + return function() { + var d = projection.apply(this, arguments), r = d[0], a = d[1] - halfπ; + return [ r * Math.cos(a), r * Math.sin(a) ]; + }; + } + d3.svg.symbol = function() { + var type = d3_svg_symbolType, size = d3_svg_symbolSize; + function symbol(d, i) { + return (d3_svg_symbols.get(type.call(this, d, i)) || d3_svg_symbolCircle)(size.call(this, d, i)); + } + symbol.type = function(x) { + if (!arguments.length) return type; + type = d3_functor(x); + return symbol; + }; + symbol.size = function(x) { + if (!arguments.length) return size; + size = d3_functor(x); + return symbol; + }; + return symbol; + }; + function d3_svg_symbolSize() { + return 64; + } + function d3_svg_symbolType() { + return "circle"; + } + function d3_svg_symbolCircle(size) { + var r = Math.sqrt(size / π); + return "M0," + r + "A" + r + "," + r + " 0 1,1 0," + -r + "A" + r + "," + r + " 0 1,1 0," + r + "Z"; + } + var d3_svg_symbols = d3.map({ + circle: d3_svg_symbolCircle, + cross: function(size) { + var r = Math.sqrt(size / 5) / 2; + return "M" + -3 * r + "," + -r + "H" + -r + "V" + -3 * r + "H" + r + "V" + -r + "H" + 3 * r + "V" + r + "H" + r + "V" + 3 * r + "H" + -r + "V" + r + "H" + -3 * r + "Z"; + }, + diamond: function(size) { + var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), rx = ry * d3_svg_symbolTan30; + return "M0," + -ry + "L" + rx + ",0" + " 0," + ry + " " + -rx + ",0" + "Z"; + }, + square: function(size) { + var r = Math.sqrt(size) / 2; + return "M" + -r + "," + -r + "L" + r + "," + -r + " " + r + "," + r + " " + -r + "," + r + "Z"; + }, + "triangle-down": function(size) { + var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2; + return "M0," + ry + "L" + rx + "," + -ry + " " + -rx + "," + -ry + "Z"; + }, + "triangle-up": function(size) { + var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2; + return "M0," + -ry + "L" + rx + "," + ry + " " + -rx + "," + ry + "Z"; + } + }); + d3.svg.symbolTypes = d3_svg_symbols.keys(); + var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * d3_radians); + d3_selectionPrototype.transition = function(name) { + var id = d3_transitionInheritId || ++d3_transitionId, ns = d3_transitionNamespace(name), subgroups = [], subgroup, node, transition = d3_transitionInherit || { + time: Date.now(), + ease: d3_ease_cubicInOut, + delay: 0, + duration: 250 + }; + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) d3_transitionNode(node, i, ns, id, transition); + subgroup.push(node); + } + } + return d3_transition(subgroups, ns, id); + }; + d3_selectionPrototype.interrupt = function(name) { + return this.each(name == null ? d3_selection_interrupt : d3_selection_interruptNS(d3_transitionNamespace(name))); + }; + var d3_selection_interrupt = d3_selection_interruptNS(d3_transitionNamespace()); + function d3_selection_interruptNS(ns) { + return function() { + var lock, activeId, active; + if ((lock = this[ns]) && (active = lock[activeId = lock.active])) { + active.timer.c = null; + active.timer.t = NaN; + if (--lock.count) delete lock[activeId]; else delete this[ns]; + lock.active += .5; + active.event && active.event.interrupt.call(this, this.__data__, active.index); + } + }; + } + function d3_transition(groups, ns, id) { + d3_subclass(groups, d3_transitionPrototype); + groups.namespace = ns; + groups.id = id; + return groups; + } + var d3_transitionPrototype = [], d3_transitionId = 0, d3_transitionInheritId, d3_transitionInherit; + d3_transitionPrototype.call = d3_selectionPrototype.call; + d3_transitionPrototype.empty = d3_selectionPrototype.empty; + d3_transitionPrototype.node = d3_selectionPrototype.node; + d3_transitionPrototype.size = d3_selectionPrototype.size; + d3.transition = function(selection, name) { + return selection && selection.transition ? d3_transitionInheritId ? selection.transition(name) : selection : d3.selection().transition(selection); + }; + d3.transition.prototype = d3_transitionPrototype; + d3_transitionPrototype.select = function(selector) { + var id = this.id, ns = this.namespace, subgroups = [], subgroup, subnode, node; + selector = d3_selection_selector(selector); + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if ((node = group[i]) && (subnode = selector.call(node, node.__data__, i, j))) { + if ("__data__" in node) subnode.__data__ = node.__data__; + d3_transitionNode(subnode, i, ns, id, node[ns][id]); + subgroup.push(subnode); + } else { + subgroup.push(null); + } + } + } + return d3_transition(subgroups, ns, id); + }; + d3_transitionPrototype.selectAll = function(selector) { + var id = this.id, ns = this.namespace, subgroups = [], subgroup, subnodes, node, subnode, transition; + selector = d3_selection_selectorAll(selector); + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + transition = node[ns][id]; + subnodes = selector.call(node, node.__data__, i, j); + subgroups.push(subgroup = []); + for (var k = -1, o = subnodes.length; ++k < o; ) { + if (subnode = subnodes[k]) d3_transitionNode(subnode, k, ns, id, transition); + subgroup.push(subnode); + } + } + } + } + return d3_transition(subgroups, ns, id); + }; + d3_transitionPrototype.filter = function(filter) { + var subgroups = [], subgroup, group, node; + if (typeof filter !== "function") filter = d3_selection_filter(filter); + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + if ((node = group[i]) && filter.call(node, node.__data__, i, j)) { + subgroup.push(node); + } + } + } + return d3_transition(subgroups, this.namespace, this.id); + }; + d3_transitionPrototype.tween = function(name, tween) { + var id = this.id, ns = this.namespace; + if (arguments.length < 2) return this.node()[ns][id].tween.get(name); + return d3_selection_each(this, tween == null ? function(node) { + node[ns][id].tween.remove(name); + } : function(node) { + node[ns][id].tween.set(name, tween); + }); + }; + function d3_transition_tween(groups, name, value, tween) { + var id = groups.id, ns = groups.namespace; + return d3_selection_each(groups, typeof value === "function" ? function(node, i, j) { + node[ns][id].tween.set(name, tween(value.call(node, node.__data__, i, j))); + } : (value = tween(value), function(node) { + node[ns][id].tween.set(name, value); + })); + } + d3_transitionPrototype.attr = function(nameNS, value) { + if (arguments.length < 2) { + for (value in nameNS) this.attr(value, nameNS[value]); + return this; + } + var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS); + function attrNull() { + this.removeAttribute(name); + } + function attrNullNS() { + this.removeAttributeNS(name.space, name.local); + } + function attrTween(b) { + return b == null ? attrNull : (b += "", function() { + var a = this.getAttribute(name), i; + return a !== b && (i = interpolate(a, b), function(t) { + this.setAttribute(name, i(t)); + }); + }); + } + function attrTweenNS(b) { + return b == null ? attrNullNS : (b += "", function() { + var a = this.getAttributeNS(name.space, name.local), i; + return a !== b && (i = interpolate(a, b), function(t) { + this.setAttributeNS(name.space, name.local, i(t)); + }); + }); + } + return d3_transition_tween(this, "attr." + nameNS, value, name.local ? attrTweenNS : attrTween); + }; + d3_transitionPrototype.attrTween = function(nameNS, tween) { + var name = d3.ns.qualify(nameNS); + function attrTween(d, i) { + var f = tween.call(this, d, i, this.getAttribute(name)); + return f && function(t) { + this.setAttribute(name, f(t)); + }; + } + function attrTweenNS(d, i) { + var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local)); + return f && function(t) { + this.setAttributeNS(name.space, name.local, f(t)); + }; + } + return this.tween("attr." + nameNS, name.local ? attrTweenNS : attrTween); + }; + d3_transitionPrototype.style = function(name, value, priority) { + var n = arguments.length; + if (n < 3) { + if (typeof name !== "string") { + if (n < 2) value = ""; + for (priority in name) this.style(priority, name[priority], value); + return this; + } + priority = ""; + } + function styleNull() { + this.style.removeProperty(name); + } + function styleString(b) { + return b == null ? styleNull : (b += "", function() { + var a = d3_window(this).getComputedStyle(this, null).getPropertyValue(name), i; + return a !== b && (i = d3_interpolate(a, b), function(t) { + this.style.setProperty(name, i(t), priority); + }); + }); + } + return d3_transition_tween(this, "style." + name, value, styleString); + }; + d3_transitionPrototype.styleTween = function(name, tween, priority) { + if (arguments.length < 3) priority = ""; + function styleTween(d, i) { + var f = tween.call(this, d, i, d3_window(this).getComputedStyle(this, null).getPropertyValue(name)); + return f && function(t) { + this.style.setProperty(name, f(t), priority); + }; + } + return this.tween("style." + name, styleTween); + }; + d3_transitionPrototype.text = function(value) { + return d3_transition_tween(this, "text", value, d3_transition_text); + }; + function d3_transition_text(b) { + if (b == null) b = ""; + return function() { + this.textContent = b; + }; + } + d3_transitionPrototype.remove = function() { + var ns = this.namespace; + return this.each("end.transition", function() { + var p; + if (this[ns].count < 2 && (p = this.parentNode)) p.removeChild(this); + }); + }; + d3_transitionPrototype.ease = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].ease; + if (typeof value !== "function") value = d3.ease.apply(d3, arguments); + return d3_selection_each(this, function(node) { + node[ns][id].ease = value; + }); + }; + d3_transitionPrototype.delay = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].delay; + return d3_selection_each(this, typeof value === "function" ? function(node, i, j) { + node[ns][id].delay = +value.call(node, node.__data__, i, j); + } : (value = +value, function(node) { + node[ns][id].delay = value; + })); + }; + d3_transitionPrototype.duration = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].duration; + return d3_selection_each(this, typeof value === "function" ? function(node, i, j) { + node[ns][id].duration = Math.max(1, value.call(node, node.__data__, i, j)); + } : (value = Math.max(1, value), function(node) { + node[ns][id].duration = value; + })); + }; + d3_transitionPrototype.each = function(type, listener) { + var id = this.id, ns = this.namespace; + if (arguments.length < 2) { + var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId; + try { + d3_transitionInheritId = id; + d3_selection_each(this, function(node, i, j) { + d3_transitionInherit = node[ns][id]; + type.call(node, node.__data__, i, j); + }); + } finally { + d3_transitionInherit = inherit; + d3_transitionInheritId = inheritId; + } + } else { + d3_selection_each(this, function(node) { + var transition = node[ns][id]; + (transition.event || (transition.event = d3.dispatch("start", "end", "interrupt"))).on(type, listener); + }); + } + return this; + }; + d3_transitionPrototype.transition = function() { + var id0 = this.id, id1 = ++d3_transitionId, ns = this.namespace, subgroups = [], subgroup, group, node, transition; + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + if (node = group[i]) { + transition = node[ns][id0]; + d3_transitionNode(node, i, ns, id1, { + time: transition.time, + ease: transition.ease, + delay: transition.delay + transition.duration, + duration: transition.duration + }); + } + subgroup.push(node); + } + } + return d3_transition(subgroups, ns, id1); + }; + function d3_transitionNamespace(name) { + return name == null ? "__transition__" : "__transition_" + name + "__"; + } + function d3_transitionNode(node, i, ns, id, inherit) { + var lock = node[ns] || (node[ns] = { + active: 0, + count: 0 + }), transition = lock[id], time, timer, duration, ease, tweens; + function schedule(elapsed) { + var delay = transition.delay; + timer.t = delay + time; + if (delay <= elapsed) return start(elapsed - delay); + timer.c = start; + } + function start(elapsed) { + var activeId = lock.active, active = lock[activeId]; + if (active) { + active.timer.c = null; + active.timer.t = NaN; + --lock.count; + delete lock[activeId]; + active.event && active.event.interrupt.call(node, node.__data__, active.index); + } + for (var cancelId in lock) { + if (+cancelId < id) { + var cancel = lock[cancelId]; + cancel.timer.c = null; + cancel.timer.t = NaN; + --lock.count; + delete lock[cancelId]; + } + } + timer.c = tick; + d3_timer(function() { + if (timer.c && tick(elapsed || 1)) { + timer.c = null; + timer.t = NaN; + } + return 1; + }, 0, time); + lock.active = id; + transition.event && transition.event.start.call(node, node.__data__, i); + tweens = []; + transition.tween.forEach(function(key, value) { + if (value = value.call(node, node.__data__, i)) { + tweens.push(value); + } + }); + ease = transition.ease; + duration = transition.duration; + } + function tick(elapsed) { + var t = elapsed / duration, e = ease(t), n = tweens.length; + while (n > 0) { + tweens[--n].call(node, e); + } + if (t >= 1) { + transition.event && transition.event.end.call(node, node.__data__, i); + if (--lock.count) delete lock[id]; else delete node[ns]; + return 1; + } + } + if (!transition) { + time = inherit.time; + timer = d3_timer(schedule, 0, time); + transition = lock[id] = { + tween: new d3_Map(), + time: time, + timer: timer, + delay: inherit.delay, + duration: inherit.duration, + ease: inherit.ease, + index: i + }; + inherit = null; + ++lock.count; + } + } + d3.svg.axis = function() { + var scale = d3.scale.linear(), orient = d3_svg_axisDefaultOrient, innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_; + function axis(g) { + g.each(function() { + var g = d3.select(this); + var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = scale.copy(); + var ticks = tickValues == null ? scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain() : tickValues, tickFormat = tickFormat_ == null ? scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity : tickFormat_, tick = g.selectAll(".tick").data(ticks, scale1), tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", ε), tickExit = d3.transition(tick.exit()).style("opacity", ε).remove(), tickUpdate = d3.transition(tick.order()).style("opacity", 1), tickSpacing = Math.max(innerTickSize, 0) + tickPadding, tickTransform; + var range = d3_scaleRange(scale1), path = g.selectAll(".domain").data([ 0 ]), pathUpdate = (path.enter().append("path").attr("class", "domain"), + d3.transition(path)); + tickEnter.append("line"); + tickEnter.append("text"); + var lineEnter = tickEnter.select("line"), lineUpdate = tickUpdate.select("line"), text = tick.select("text").text(tickFormat), textEnter = tickEnter.select("text"), textUpdate = tickUpdate.select("text"), sign = orient === "top" || orient === "left" ? -1 : 1, x1, x2, y1, y2; + if (orient === "bottom" || orient === "top") { + tickTransform = d3_svg_axisX, x1 = "x", y1 = "y", x2 = "x2", y2 = "y2"; + text.attr("dy", sign < 0 ? "0em" : ".71em").style("text-anchor", "middle"); + pathUpdate.attr("d", "M" + range[0] + "," + sign * outerTickSize + "V0H" + range[1] + "V" + sign * outerTickSize); + } else { + tickTransform = d3_svg_axisY, x1 = "y", y1 = "x", x2 = "y2", y2 = "x2"; + text.attr("dy", ".32em").style("text-anchor", sign < 0 ? "end" : "start"); + pathUpdate.attr("d", "M" + sign * outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + sign * outerTickSize); + } + lineEnter.attr(y2, sign * innerTickSize); + textEnter.attr(y1, sign * tickSpacing); + lineUpdate.attr(x2, 0).attr(y2, sign * innerTickSize); + textUpdate.attr(x1, 0).attr(y1, sign * tickSpacing); + if (scale1.rangeBand) { + var x = scale1, dx = x.rangeBand() / 2; + scale0 = scale1 = function(d) { + return x(d) + dx; + }; + } else if (scale0.rangeBand) { + scale0 = scale1; + } else { + tickExit.call(tickTransform, scale1, scale0); + } + tickEnter.call(tickTransform, scale0, scale1); + tickUpdate.call(tickTransform, scale1, scale1); + }); + } + axis.scale = function(x) { + if (!arguments.length) return scale; + scale = x; + return axis; + }; + axis.orient = function(x) { + if (!arguments.length) return orient; + orient = x in d3_svg_axisOrients ? x + "" : d3_svg_axisDefaultOrient; + return axis; + }; + axis.ticks = function() { + if (!arguments.length) return tickArguments_; + tickArguments_ = d3_array(arguments); + return axis; + }; + axis.tickValues = function(x) { + if (!arguments.length) return tickValues; + tickValues = x; + return axis; + }; + axis.tickFormat = function(x) { + if (!arguments.length) return tickFormat_; + tickFormat_ = x; + return axis; + }; + axis.tickSize = function(x) { + var n = arguments.length; + if (!n) return innerTickSize; + innerTickSize = +x; + outerTickSize = +arguments[n - 1]; + return axis; + }; + axis.innerTickSize = function(x) { + if (!arguments.length) return innerTickSize; + innerTickSize = +x; + return axis; + }; + axis.outerTickSize = function(x) { + if (!arguments.length) return outerTickSize; + outerTickSize = +x; + return axis; + }; + axis.tickPadding = function(x) { + if (!arguments.length) return tickPadding; + tickPadding = +x; + return axis; + }; + axis.tickSubdivide = function() { + return arguments.length && axis; + }; + return axis; + }; + var d3_svg_axisDefaultOrient = "bottom", d3_svg_axisOrients = { + top: 1, + right: 1, + bottom: 1, + left: 1 + }; + function d3_svg_axisX(selection, x0, x1) { + selection.attr("transform", function(d) { + var v0 = x0(d); + return "translate(" + (isFinite(v0) ? v0 : x1(d)) + ",0)"; + }); + } + function d3_svg_axisY(selection, y0, y1) { + selection.attr("transform", function(d) { + var v0 = y0(d); + return "translate(0," + (isFinite(v0) ? v0 : y1(d)) + ")"; + }); + } + d3.svg.brush = function() { + var event = d3_eventDispatch(brush, "brushstart", "brush", "brushend"), x = null, y = null, xExtent = [ 0, 0 ], yExtent = [ 0, 0 ], xExtentDomain, yExtentDomain, xClamp = true, yClamp = true, resizes = d3_svg_brushResizes[0]; + function brush(g) { + g.each(function() { + var g = d3.select(this).style("pointer-events", "all").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)").on("mousedown.brush", brushstart).on("touchstart.brush", brushstart); + var background = g.selectAll(".background").data([ 0 ]); + background.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair"); + g.selectAll(".extent").data([ 0 ]).enter().append("rect").attr("class", "extent").style("cursor", "move"); + var resize = g.selectAll(".resize").data(resizes, d3_identity); + resize.exit().remove(); + resize.enter().append("g").attr("class", function(d) { + return "resize " + d; + }).style("cursor", function(d) { + return d3_svg_brushCursor[d]; + }).append("rect").attr("x", function(d) { + return /[ew]$/.test(d) ? -3 : null; + }).attr("y", function(d) { + return /^[ns]/.test(d) ? -3 : null; + }).attr("width", 6).attr("height", 6).style("visibility", "hidden"); + resize.style("display", brush.empty() ? "none" : null); + var gUpdate = d3.transition(g), backgroundUpdate = d3.transition(background), range; + if (x) { + range = d3_scaleRange(x); + backgroundUpdate.attr("x", range[0]).attr("width", range[1] - range[0]); + redrawX(gUpdate); + } + if (y) { + range = d3_scaleRange(y); + backgroundUpdate.attr("y", range[0]).attr("height", range[1] - range[0]); + redrawY(gUpdate); + } + redraw(gUpdate); + }); + } + brush.event = function(g) { + g.each(function() { + var event_ = event.of(this, arguments), extent1 = { + x: xExtent, + y: yExtent, + i: xExtentDomain, + j: yExtentDomain + }, extent0 = this.__chart__ || extent1; + this.__chart__ = extent1; + if (d3_transitionInheritId) { + d3.select(this).transition().each("start.brush", function() { + xExtentDomain = extent0.i; + yExtentDomain = extent0.j; + xExtent = extent0.x; + yExtent = extent0.y; + event_({ + type: "brushstart" + }); + }).tween("brush:brush", function() { + var xi = d3_interpolateArray(xExtent, extent1.x), yi = d3_interpolateArray(yExtent, extent1.y); + xExtentDomain = yExtentDomain = null; + return function(t) { + xExtent = extent1.x = xi(t); + yExtent = extent1.y = yi(t); + event_({ + type: "brush", + mode: "resize" + }); + }; + }).each("end.brush", function() { + xExtentDomain = extent1.i; + yExtentDomain = extent1.j; + event_({ + type: "brush", + mode: "resize" + }); + event_({ + type: "brushend" + }); + }); + } else { + event_({ + type: "brushstart" + }); + event_({ + type: "brush", + mode: "resize" + }); + event_({ + type: "brushend" + }); + } + }); + }; + function redraw(g) { + g.selectAll(".resize").attr("transform", function(d) { + return "translate(" + xExtent[+/e$/.test(d)] + "," + yExtent[+/^s/.test(d)] + ")"; + }); + } + function redrawX(g) { + g.select(".extent").attr("x", xExtent[0]); + g.selectAll(".extent,.n>rect,.s>rect").attr("width", xExtent[1] - xExtent[0]); + } + function redrawY(g) { + g.select(".extent").attr("y", yExtent[0]); + g.selectAll(".extent,.e>rect,.w>rect").attr("height", yExtent[1] - yExtent[0]); + } + function brushstart() { + var target = this, eventTarget = d3.select(d3.event.target), event_ = event.of(target, arguments), g = d3.select(target), resizing = eventTarget.datum(), resizingX = !/^(n|s)$/.test(resizing) && x, resizingY = !/^(e|w)$/.test(resizing) && y, dragging = eventTarget.classed("extent"), dragRestore = d3_event_dragSuppress(target), center, origin = d3.mouse(target), offset; + var w = d3.select(d3_window(target)).on("keydown.brush", keydown).on("keyup.brush", keyup); + if (d3.event.changedTouches) { + w.on("touchmove.brush", brushmove).on("touchend.brush", brushend); + } else { + w.on("mousemove.brush", brushmove).on("mouseup.brush", brushend); + } + g.interrupt().selectAll("*").interrupt(); + if (dragging) { + origin[0] = xExtent[0] - origin[0]; + origin[1] = yExtent[0] - origin[1]; + } else if (resizing) { + var ex = +/w$/.test(resizing), ey = +/^n/.test(resizing); + offset = [ xExtent[1 - ex] - origin[0], yExtent[1 - ey] - origin[1] ]; + origin[0] = xExtent[ex]; + origin[1] = yExtent[ey]; + } else if (d3.event.altKey) center = origin.slice(); + g.style("pointer-events", "none").selectAll(".resize").style("display", null); + d3.select("body").style("cursor", eventTarget.style("cursor")); + event_({ + type: "brushstart" + }); + brushmove(); + function keydown() { + if (d3.event.keyCode == 32) { + if (!dragging) { + center = null; + origin[0] -= xExtent[1]; + origin[1] -= yExtent[1]; + dragging = 2; + } + d3_eventPreventDefault(); + } + } + function keyup() { + if (d3.event.keyCode == 32 && dragging == 2) { + origin[0] += xExtent[1]; + origin[1] += yExtent[1]; + dragging = 0; + d3_eventPreventDefault(); + } + } + function brushmove() { + var point = d3.mouse(target), moved = false; + if (offset) { + point[0] += offset[0]; + point[1] += offset[1]; + } + if (!dragging) { + if (d3.event.altKey) { + if (!center) center = [ (xExtent[0] + xExtent[1]) / 2, (yExtent[0] + yExtent[1]) / 2 ]; + origin[0] = xExtent[+(point[0] < center[0])]; + origin[1] = yExtent[+(point[1] < center[1])]; + } else center = null; + } + if (resizingX && move1(point, x, 0)) { + redrawX(g); + moved = true; + } + if (resizingY && move1(point, y, 1)) { + redrawY(g); + moved = true; + } + if (moved) { + redraw(g); + event_({ + type: "brush", + mode: dragging ? "move" : "resize" + }); + } + } + function move1(point, scale, i) { + var range = d3_scaleRange(scale), r0 = range[0], r1 = range[1], position = origin[i], extent = i ? yExtent : xExtent, size = extent[1] - extent[0], min, max; + if (dragging) { + r0 -= position; + r1 -= size + position; + } + min = (i ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[i])) : point[i]; + if (dragging) { + max = (min += position) + size; + } else { + if (center) position = Math.max(r0, Math.min(r1, 2 * center[i] - min)); + if (position < min) { + max = min; + min = position; + } else { + max = position; + } + } + if (extent[0] != min || extent[1] != max) { + if (i) yExtentDomain = null; else xExtentDomain = null; + extent[0] = min; + extent[1] = max; + return true; + } + } + function brushend() { + brushmove(); + g.style("pointer-events", "all").selectAll(".resize").style("display", brush.empty() ? "none" : null); + d3.select("body").style("cursor", null); + w.on("mousemove.brush", null).on("mouseup.brush", null).on("touchmove.brush", null).on("touchend.brush", null).on("keydown.brush", null).on("keyup.brush", null); + dragRestore(); + event_({ + type: "brushend" + }); + } + } + brush.x = function(z) { + if (!arguments.length) return x; + x = z; + resizes = d3_svg_brushResizes[!x << 1 | !y]; + return brush; + }; + brush.y = function(z) { + if (!arguments.length) return y; + y = z; + resizes = d3_svg_brushResizes[!x << 1 | !y]; + return brush; + }; + brush.clamp = function(z) { + if (!arguments.length) return x && y ? [ xClamp, yClamp ] : x ? xClamp : y ? yClamp : null; + if (x && y) xClamp = !!z[0], yClamp = !!z[1]; else if (x) xClamp = !!z; else if (y) yClamp = !!z; + return brush; + }; + brush.extent = function(z) { + var x0, x1, y0, y1, t; + if (!arguments.length) { + if (x) { + if (xExtentDomain) { + x0 = xExtentDomain[0], x1 = xExtentDomain[1]; + } else { + x0 = xExtent[0], x1 = xExtent[1]; + if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1); + if (x1 < x0) t = x0, x0 = x1, x1 = t; + } + } + if (y) { + if (yExtentDomain) { + y0 = yExtentDomain[0], y1 = yExtentDomain[1]; + } else { + y0 = yExtent[0], y1 = yExtent[1]; + if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1); + if (y1 < y0) t = y0, y0 = y1, y1 = t; + } + } + return x && y ? [ [ x0, y0 ], [ x1, y1 ] ] : x ? [ x0, x1 ] : y && [ y0, y1 ]; + } + if (x) { + x0 = z[0], x1 = z[1]; + if (y) x0 = x0[0], x1 = x1[0]; + xExtentDomain = [ x0, x1 ]; + if (x.invert) x0 = x(x0), x1 = x(x1); + if (x1 < x0) t = x0, x0 = x1, x1 = t; + if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [ x0, x1 ]; + } + if (y) { + y0 = z[0], y1 = z[1]; + if (x) y0 = y0[1], y1 = y1[1]; + yExtentDomain = [ y0, y1 ]; + if (y.invert) y0 = y(y0), y1 = y(y1); + if (y1 < y0) t = y0, y0 = y1, y1 = t; + if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [ y0, y1 ]; + } + return brush; + }; + brush.clear = function() { + if (!brush.empty()) { + xExtent = [ 0, 0 ], yExtent = [ 0, 0 ]; + xExtentDomain = yExtentDomain = null; + } + return brush; + }; + brush.empty = function() { + return !!x && xExtent[0] == xExtent[1] || !!y && yExtent[0] == yExtent[1]; + }; + return d3.rebind(brush, event, "on"); + }; + var d3_svg_brushCursor = { + n: "ns-resize", + e: "ew-resize", + s: "ns-resize", + w: "ew-resize", + nw: "nwse-resize", + ne: "nesw-resize", + se: "nwse-resize", + sw: "nesw-resize" + }; + var d3_svg_brushResizes = [ [ "n", "e", "s", "w", "nw", "ne", "se", "sw" ], [ "e", "w" ], [ "n", "s" ], [] ]; + var d3_time_format = d3_time.format = d3_locale_enUS.timeFormat; + var d3_time_formatUtc = d3_time_format.utc; + var d3_time_formatIso = d3_time_formatUtc("%Y-%m-%dT%H:%M:%S.%LZ"); + d3_time_format.iso = Date.prototype.toISOString && +new Date("2000-01-01T00:00:00.000Z") ? d3_time_formatIsoNative : d3_time_formatIso; + function d3_time_formatIsoNative(date) { + return date.toISOString(); + } + d3_time_formatIsoNative.parse = function(string) { + var date = new Date(string); + return isNaN(date) ? null : date; + }; + d3_time_formatIsoNative.toString = d3_time_formatIso.toString; + d3_time.second = d3_time_interval(function(date) { + return new d3_date(Math.floor(date / 1e3) * 1e3); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 1e3); + }, function(date) { + return date.getSeconds(); + }); + d3_time.seconds = d3_time.second.range; + d3_time.seconds.utc = d3_time.second.utc.range; + d3_time.minute = d3_time_interval(function(date) { + return new d3_date(Math.floor(date / 6e4) * 6e4); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 6e4); + }, function(date) { + return date.getMinutes(); + }); + d3_time.minutes = d3_time.minute.range; + d3_time.minutes.utc = d3_time.minute.utc.range; + d3_time.hour = d3_time_interval(function(date) { + var timezone = date.getTimezoneOffset() / 60; + return new d3_date((Math.floor(date / 36e5 - timezone) + timezone) * 36e5); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 36e5); + }, function(date) { + return date.getHours(); + }); + d3_time.hours = d3_time.hour.range; + d3_time.hours.utc = d3_time.hour.utc.range; + d3_time.month = d3_time_interval(function(date) { + date = d3_time.day(date); + date.setDate(1); + return date; + }, function(date, offset) { + date.setMonth(date.getMonth() + offset); + }, function(date) { + return date.getMonth(); + }); + d3_time.months = d3_time.month.range; + d3_time.months.utc = d3_time.month.utc.range; + function d3_time_scale(linear, methods, format) { + function scale(x) { + return linear(x); + } + scale.invert = function(x) { + return d3_time_scaleDate(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return linear.domain().map(d3_time_scaleDate); + linear.domain(x); + return scale; + }; + function tickMethod(extent, count) { + var span = extent[1] - extent[0], target = span / count, i = d3.bisect(d3_time_scaleSteps, target); + return i == d3_time_scaleSteps.length ? [ methods.year, d3_scale_linearTickRange(extent.map(function(d) { + return d / 31536e6; + }), count)[2] ] : !i ? [ d3_time_scaleMilliseconds, d3_scale_linearTickRange(extent, count)[2] ] : methods[target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target ? i - 1 : i]; + } + scale.nice = function(interval, skip) { + var domain = scale.domain(), extent = d3_scaleExtent(domain), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" && tickMethod(extent, interval); + if (method) interval = method[0], skip = method[1]; + function skipped(date) { + return !isNaN(date) && !interval.range(date, d3_time_scaleDate(+date + 1), skip).length; + } + return scale.domain(d3_scale_nice(domain, skip > 1 ? { + floor: function(date) { + while (skipped(date = interval.floor(date))) date = d3_time_scaleDate(date - 1); + return date; + }, + ceil: function(date) { + while (skipped(date = interval.ceil(date))) date = d3_time_scaleDate(+date + 1); + return date; + } + } : interval)); + }; + scale.ticks = function(interval, skip) { + var extent = d3_scaleExtent(scale.domain()), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" ? tickMethod(extent, interval) : !interval.range && [ { + range: interval + }, skip ]; + if (method) interval = method[0], skip = method[1]; + return interval.range(extent[0], d3_time_scaleDate(+extent[1] + 1), skip < 1 ? 1 : skip); + }; + scale.tickFormat = function() { + return format; + }; + scale.copy = function() { + return d3_time_scale(linear.copy(), methods, format); + }; + return d3_scale_linearRebind(scale, linear); + } + function d3_time_scaleDate(t) { + return new Date(t); + } + var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ]; + var d3_time_scaleLocalMethods = [ [ d3_time.second, 1 ], [ d3_time.second, 5 ], [ d3_time.second, 15 ], [ d3_time.second, 30 ], [ d3_time.minute, 1 ], [ d3_time.minute, 5 ], [ d3_time.minute, 15 ], [ d3_time.minute, 30 ], [ d3_time.hour, 1 ], [ d3_time.hour, 3 ], [ d3_time.hour, 6 ], [ d3_time.hour, 12 ], [ d3_time.day, 1 ], [ d3_time.day, 2 ], [ d3_time.week, 1 ], [ d3_time.month, 1 ], [ d3_time.month, 3 ], [ d3_time.year, 1 ] ]; + var d3_time_scaleLocalFormat = d3_time_format.multi([ [ ".%L", function(d) { + return d.getMilliseconds(); + } ], [ ":%S", function(d) { + return d.getSeconds(); + } ], [ "%I:%M", function(d) { + return d.getMinutes(); + } ], [ "%I %p", function(d) { + return d.getHours(); + } ], [ "%a %d", function(d) { + return d.getDay() && d.getDate() != 1; + } ], [ "%b %d", function(d) { + return d.getDate() != 1; + } ], [ "%B", function(d) { + return d.getMonth(); + } ], [ "%Y", d3_true ] ]); + var d3_time_scaleMilliseconds = { + range: function(start, stop, step) { + return d3.range(Math.ceil(start / step) * step, +stop, step).map(d3_time_scaleDate); + }, + floor: d3_identity, + ceil: d3_identity + }; + d3_time_scaleLocalMethods.year = d3_time.year; + d3_time.scale = function() { + return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat); + }; + var d3_time_scaleUtcMethods = d3_time_scaleLocalMethods.map(function(m) { + return [ m[0].utc, m[1] ]; + }); + var d3_time_scaleUtcFormat = d3_time_formatUtc.multi([ [ ".%L", function(d) { + return d.getUTCMilliseconds(); + } ], [ ":%S", function(d) { + return d.getUTCSeconds(); + } ], [ "%I:%M", function(d) { + return d.getUTCMinutes(); + } ], [ "%I %p", function(d) { + return d.getUTCHours(); + } ], [ "%a %d", function(d) { + return d.getUTCDay() && d.getUTCDate() != 1; + } ], [ "%b %d", function(d) { + return d.getUTCDate() != 1; + } ], [ "%B", function(d) { + return d.getUTCMonth(); + } ], [ "%Y", d3_true ] ]); + d3_time_scaleUtcMethods.year = d3_time.year.utc; + d3_time.scale.utc = function() { + return d3_time_scale(d3.scale.linear(), d3_time_scaleUtcMethods, d3_time_scaleUtcFormat); + }; + d3.text = d3_xhrType(function(request) { + return request.responseText; + }); + d3.json = function(url, callback) { + return d3_xhr(url, "application/json", d3_json, callback); + }; + function d3_json(request) { + return JSON.parse(request.responseText); + } + d3.html = function(url, callback) { + return d3_xhr(url, "text/html", d3_html, callback); + }; + function d3_html(request) { + var range = d3_document.createRange(); + range.selectNode(d3_document.body); + return range.createContextualFragment(request.responseText); + } + d3.xml = d3_xhrType(function(request) { + return request.responseXML; + }); + if (typeof define === "function" && define.amd) this.d3 = d3, define(d3); else if (typeof module === "object" && module.exports) module.exports = d3; else this.d3 = d3; +}(); \ No newline at end of file diff --git a/report/js/functions.js b/report/js/functions.js new file mode 100644 index 00000000..74d290e6 --- /dev/null +++ b/report/js/functions.js @@ -0,0 +1,40 @@ +function loadJSON(filename, callback) { + + var xobj = new XMLHttpRequest(); + xobj.overrideMimeType("application/json"); + xobj.open('GET', filename, true); + xobj.onreadystatechange = function () { + if (xobj.readyState == 4 && xobj.status == "200") { + // Required use of an anonymous callback as .open will NOT return a value but simply returns undefined in asynchronous mode + callback(xobj.responseText); + } + }; + xobj.send(null); +} + + +function equalsHeightOf(node1, node2) { + var w1 = node1.style.height; + node2.style.height = w1 + 'px'; +} + +function saveSvgAsImage(svg, name, width, height) { + width = width || 600; + height = height || 600; + var img = new Image(), + serializer = new XMLSerializer(), + svgStr = serializer.serializeToString(svg); + + img.src = 'data:image/svg+xml;base64,' + window.btoa(svgStr); + var canvas = document.createElement("canvas"); + document.body.appendChild(canvas); + canvas.width = width; + canvas.height = height; + img.onload = function () { + canvas.getContext("2d").drawImage(img,0,0, width, height); + canvas.toBlob(function (blob) { + saveAs(blob, name + ".png"); + }); + }; + canvas.parentNode.removeChild(canvas); +} diff --git a/report/js/graph-licenses.js b/report/js/graph-licenses.js new file mode 100644 index 00000000..f9066ca5 --- /dev/null +++ b/report/js/graph-licenses.js @@ -0,0 +1,52 @@ +function chartLicenses(json) { + + var diameter = document.getElementById('svg-licenses').offsetWidth; + + var svg = d3.select('#svg-licenses').append('svg'), + width = 300,//document.getElementById('svg-licenses').offsetWidth, + height = 300,//document.getElementById('svg-licenses').offsetWidth, + radius = Math.min(width, height) / 2; + + //var r = 300; // outer radius + + var color = d3.scale.ordinal() + .range(["#BBDEFB", "#90CAF9", "#64B5F6", "#42A5F5", "#2196F3", "#1E88E5", "#1976D2", "#1565C0", "#0D47A1"]); + + svg + .attr("width", width) + .attr("height", height); + + var group = svg.append("g") + .attr("transform", "translate(" + Math.ceil(width / 2) + ", " + Math.ceil(height / 2) + ")"); // set center of pie + + var arc = d3.svg.arc() + .innerRadius(radius - 10) + .outerRadius(0); + + var pie = d3.layout.pie() + .value(function (d) { + return d.value; + }); + + var arcs = group.selectAll(".arc") + .data(pie(json)) + .enter() + .append("g") + .attr("class", "arc"); + + arcs.append("path") + .attr("d", arc) // here the arc function works on every record d of data + .attr("fill", function (d) { + return color(d.data.value); + }); + + arcs.append("text") + .attr("transform", function (d) { + return "translate(" + arc.centroid(d) + ")"; + }) + .attr("text-anchor", "middle") + .attr('color', '#FFF') + .text(function (d) { + return d.data.name; + }); +} \ No newline at end of file diff --git a/report/js/graph-maintainability.js b/report/js/graph-maintainability.js new file mode 100644 index 00000000..223a9fba --- /dev/null +++ b/report/js/graph-maintainability.js @@ -0,0 +1,124 @@ +function chartMaintainability(withoutComment) { + var chartId = 'svg-maintainability'; + withoutComment = typeof (withoutComment) !== 'undefined' ? withoutComment : false; + var diameter = document.getElementById(chartId).offsetWidth; + + var json = { + name: 'chart', + children: classes + }; + + // if already loaded, removed previous node + var previous = d3.select('#' + chartId).select('svg'); + if (previous) { + previous.remove(); + } + previous = d3.select('#' + chartId).select('button'); + if (previous) { + previous.remove(); + } + + var svg = d3.select('#' + chartId).append('svg') + .attr('width', diameter) + .attr('height', diameter); + + var bubble = d3.layout.pack() + .size([diameter, diameter]) + .padding(3) + .value(function (d) { + return d.ccn; + }); + + var nodes = bubble.nodes(json) + .filter(function (d) { + return !d.children; + }); // filter out the outer bubble* + + var vis = svg.selectAll('circle') + .data(nodes, function (d) { + return d.name; + }); + + vis.enter().append('circle') + .attr('transform', function (d) { + return 'translate(' + d.x + ',' + d.y + ')'; + }) + .attr('r', function (d) { + return d.r; + }) + .style("fill", function (d) { + if (true === withoutComment) { + if (d.mIwoC > 65) { + return '#8BC34A'; + } else if (d.mIwoC > 53) { + return '#FFC107'; + } else { + return '#F44336'; + } + } else { + if (d.mi > 85) { + return '#8BC34A'; + } else if (d.mi > 69) { + return '#FFC107'; + } else { + return '#F44336'; + } + } + }) + .on('mouseover', function (d) { + var text = ''; + if (true === withoutComment) { + text = '' + d.name + '' + + "
Cyclomatic Complexity : " + d.ccn + + "
Maintainability Index (w/o comments): " + d.mIwoC; + } else { + text = '' + d.name + '' + + "
Cyclomatic Complexity : " + d.ccn + + "
Maintainability Index: " + d.mi; + } + d3.select('.tooltip').html(text); + d3.select(".tooltip") + .style("opacity", 1) + .style("z-index", 1); + }) + .on('mousemove', function () { + d3.select(".tooltip") + .style("left", (d3.event.pageX + 5) + "px") + .style("top", (d3.event.pageY + 5) + "px"); + }) + .on('mouseout', function () { + d3.select(".tooltip") + .style("opacity", 0) + .style("z-index", -1); + }); + + d3.select("body") + .append("div") + .attr("class", "tooltip") + .style("opacity", 0); + + // button for saving image + var button = d3.select('#' + chartId).append('button'); + button + .classed('btn-save-image', true) + .text('download') + .on('click', function () { + var svg = d3.select('#' + chartId + ' svg')[0][0]; + var nameImage = (withoutComment) + ? 'PhpMetrics maintainability without comments / complexity' + : 'PhpMetrics maintainability / complexity'; + saveSvgAsImage(svg, nameImage, 1900, 1900); + }); +} + +function toggleChartMaintainability(item) { + if (item.getAttribute('data-current') === 'with-comments') { + item.setAttribute('data-current', 'without-comments'); + item.innerHTML = '(without comments)'; + } else { + item.setAttribute('data-current', 'with-comments'); + item.innerHTML = '(with comments)'; + } + + chartMaintainability(item.getAttribute('data-current') !== 'with-comments') +} diff --git a/report/js/history-1.json b/report/js/history-1.json new file mode 100644 index 00000000..80066541 --- /dev/null +++ b/report/js/history-1.json @@ -0,0 +1,42 @@ +{ + "avg": { + "wmc": 6.35, + "ccn": 4.27, + "bugs": 0.08, + "kanDefect": 0.3, + "relativeSystemComplexity": 112.57, + "relativeDataComplexity": 0.71, + "relativeStructuralComplexity": 111.87, + "volume": 244.85, + "commentWeight": 24.75, + "intelligentContent": 49.79, + "lcom": 2.04, + "instability": 0.77, + "afferentCoupling": 1.23, + "efferentCoupling": 3.32, + "difficulty": 4.76, + "mi": 87.79, + "distance": 0.13, + "incomingCDep": 2.18, + "incomingPDep": 1.22, + "outgoingCDep": 5.58, + "outgoingPDep": 3.62, + "classesPerPackage": 2.86 + }, + "sum": { + "loc": 4961, + "cloc": 789, + "lloc": 4172, + "nbMethods": 438, + "nbClasses": 142, + "nbInterfaces": 1, + "nbPackages": 50, + "violations": { + "total": 26, + "information": 0, + "warning": 18, + "error": 8, + "critical": 0 + } + } +} \ No newline at end of file diff --git a/report/js/latest.json b/report/js/latest.json new file mode 100644 index 00000000..80066541 --- /dev/null +++ b/report/js/latest.json @@ -0,0 +1,42 @@ +{ + "avg": { + "wmc": 6.35, + "ccn": 4.27, + "bugs": 0.08, + "kanDefect": 0.3, + "relativeSystemComplexity": 112.57, + "relativeDataComplexity": 0.71, + "relativeStructuralComplexity": 111.87, + "volume": 244.85, + "commentWeight": 24.75, + "intelligentContent": 49.79, + "lcom": 2.04, + "instability": 0.77, + "afferentCoupling": 1.23, + "efferentCoupling": 3.32, + "difficulty": 4.76, + "mi": 87.79, + "distance": 0.13, + "incomingCDep": 2.18, + "incomingPDep": 1.22, + "outgoingCDep": 5.58, + "outgoingPDep": 3.62, + "classesPerPackage": 2.86 + }, + "sum": { + "loc": 4961, + "cloc": 789, + "lloc": 4172, + "nbMethods": 438, + "nbClasses": 142, + "nbInterfaces": 1, + "nbPackages": 50, + "violations": { + "total": 26, + "information": 0, + "warning": 18, + "error": 8, + "critical": 0 + } + } +} \ No newline at end of file diff --git a/report/js/sort-table.min.js b/report/js/sort-table.min.js new file mode 100644 index 00000000..ab6a8838 --- /dev/null +++ b/report/js/sort-table.min.js @@ -0,0 +1,8 @@ +/* Copyright (c) 2006-2013 Tyler Uebele * Released under the MIT license. * latest at https://github.com/tyleruebele/sort-table * minified by Google Closure Compiler */ +function sortTable(a,b,d){var c;sortTable.sortCol=-1;c=a.className.match(/js-sort-\d+/);null!=c&&(sortTable.sortCol=c[0].replace(/js-sort-/,""),a.className=a.className.replace(RegExp(" ?"+c[0]+"\\b"),""));"undefined"===typeof b&&(b=sortTable.sortCol);"undefined"!==typeof d?sortTable.sortDir=-1==d||"desc"==d?-1:1:(c=a.className.match(/js-sort-(a|de)sc/),sortTable.sortDir=null!=c&&sortTable.sortCol==b?"js-sort-asc"==c[0]?-1:1:1);a.className=a.className.replace(/ ?js-sort-(a|de)sc/g,"");a.className+= +" js-sort-"+b;sortTable.sortCol=b;a.className+=" js-sort-"+(-1==sortTable.sortDir?"desc":"asc");bc?1:-1)}; +sortTable.stripTags=function(a){return a.replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi,"")};sortTable.date=function(a){return new Date(sortTable.stripTags(a.innerHTML))};sortTable.number=function(a){return Number(sortTable.stripTags(a.innerHTML).replace(/[^-\d.]/g,""))};sortTable.string=function(a){return sortTable.stripTags(a.innerHTML).toLowerCase()};sortTable.last=function(a){return sortTable.stripTags(a.innerHTML).split(" ").pop().toLowerCase()}; +sortTable.input=function(a){for(var b=0;b + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + +
Please use the --junit option to enable this report
\ No newline at end of file diff --git a/report/loc.html b/report/loc.html new file mode 100644 index 00000000..d5072a95 --- /dev/null +++ b/report/loc.html @@ -0,0 +1,3656 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + + + + +
+
+
+

Percentile distribution of logical lines of code by class

+
+
Percentile
+
+
+
+ +
+
+
+

Explore

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassLLOCCLOCVolumeIntelligent contentComment Weight
App\Auth\CustomerUserProvider + + 93 + + + 16 + + + 797.01 + + + 40.1 + + + 27.97 +
App\Providers\AppServiceProvider + + 40 + + + 13 + + + 104 + + + 60.67 + + + 34.71 +
App\Providers\FortifyServiceProvider + + 38 + + + 16 + + + 116.76 + + + 50.76 + + + 37.34 +
App\Models\OrderLine + + 27 + + + 16 + + + 88.81 + + + 112.18 + + + 40.52 +
App\Models\WebhookSubscription + + 15 + + + 7 + + + 44.97 + + + 81.77 + + + 38.34 +
App\Models\ThemeFile + + 15 + + + 7 + + + 30 + + + 52.5 + + + 38.34 +
App\Models\ProductOption + + 15 + + + 7 + + + 16.25 + + + 26.01 + + + 38.34 +
App\Models\NavigationItem + + 24 + + + 7 + + + 120 + + + 45.22 + + + 33.57 +
App\Models\Refund + + 19 + + + 10 + + + 36 + + + 56 + + + 39.47 +
App\Models\InventoryItem + + 19 + + + 7 + + + 50.72 + + + 29.59 + + + 36 +
App\Models\NavigationMenu + + 10 + + + 4 + + + 19.65 + + + 39.3 + + + 36.83 +
App\Models\App + + 15 + + + 7 + + + 34.87 + + + 61.99 + + + 38.34 +
App\Models\CartLine + + 15 + + + 7 + + + 38.04 + + + 60.86 + + + 38.34 +
App\Models\AppInstallation + + 21 + + + 10 + + + 55.35 + + + 85.16 + + + 38.53 +
App\Models\Cart + + 22 + + + 10 + + + 64.53 + + + 29.78 + + + 38.09 +
App\Models\Discount + + 27 + + + 4 + + + 222.97 + + + 38.36 + + + 26.41 +
App\Models\Product + + 26 + + + 16 + + + 97.67 + + + 136.74 + + + 40.85 +
App\Models\Order + + 50 + + + 19 + + + 322.02 + + + 55.91 + + + 36.32 +
App\Models\Store + + 26 + + + 16 + + + 65.73 + + + 93.9 + + + 40.85 +
App\Models\StoreDomain + + 15 + + + 7 + + + 33 + + + 51.33 + + + 38.34 +
App\Models\Theme + + 18 + + + 10 + + + 39 + + + 54.6 + + + 39.96 +
App\Models\ProductMedia + + 16 + + + 7 + + + 59.21 + + + 101.5 + + + 37.72 +
App\Models\User + + 32 + + + 24 + + + 267.19 + + + 79.52 + + + 42.45 +
App\Models\WebhookDelivery + + 16 + + + 7 + + + 57.36 + + + 90.14 + + + 37.72 +
App\Models\Fulfillment + + 19 + + + 10 + + + 56.47 + + + 72.61 + + + 39.47 +
App\Models\ThemeSettings + + 19 + + + 7 + + + 28.07 + + + 42.11 + + + 36 +
App\Models\Checkout + + 14 + + + 7 + + + 110.36 + + + 141.26 + + + 38.99 +
App\Models\Payment + + 19 + + + 10 + + + 58.81 + + + 84.01 + + + 39.47 +
App\Models\AnalyticsDaily + + 23 + + + 15 + + + 107.31 + + + 99.06 + + + 41.34 +
App\Models\Customer + + 35 + + + 16 + + + 89.62 + + + 109.54 + + + 38.14 +
App\Models\ProductVariant + + 22 + + + 13 + + + 110.36 + + + 153.55 + + + 40.5 +
App\Models\ProductOptionValue + + 11 + + + 4 + + + 13.93 + + + 22.29 + + + 35.87 +
App\Models\TaxSettings + + 19 + + + 7 + + + 55.35 + + + 79.07 + + + 36 +
App\Models\ShippingZone + + 14 + + + 7 + + + 36 + + + 50.4 + + + 38.99 +
App\Models\Collection + + 14 + + + 7 + + + 43.19 + + + 70.67 + + + 38.99 +
App\Models\Page + + 10 + + + 4 + + + 30 + + + 46.67 + + + 36.83 +
App\Models\ShippingRate + + 14 + + + 7 + + + 44.38 + + + 59.17 + + + 38.99 +
App\Models\StoreSettings + + 18 + + + 7 + + + 23.26 + + + 33.24 + + + 36.55 +
App\Models\CustomerAddress + + 15 + + + 7 + + + 33 + + + 51.33 + + + 38.34 +
App\Models\StoreUser + + 20 + + + 3 + + + 25.85 + + + 12.92 + + + 26.54 +
App\Models\FulfillmentLine + + 15 + + + 7 + + + 16.25 + + + 26.01 + + + 38.34 +
App\Models\AnalyticsEvent + + 12 + + + 4 + + + 44.97 + + + 74.95 + + + 34.97 +
App\Models\Scopes\StoreScope + + 12 + + + 1 + + + 41.21 + + + 11.45 + + + 20.83 +
App\Models\Organization + + 10 + + + 4 + + + 8 + + + 16 + + + 36.83 +
App\Models\Concerns\BelongsToStore + + 18 + + + 4 + + + 44.38 + + + 8.88 + + + 30.68 +
App\Exceptions\InsufficientInventoryException + + 4 + + + 0 + + + 0 + + + 0 + + + 0 +
App\Exceptions\FulfillmentGuardException + + 4 + + + 0 + + + 0 + + + 0 + + + 0 +
App\Exceptions\InvalidDiscountException + + 32 + + + 0 + + + 106.27 + + + 83.9 + + + 0 +
App\Exceptions\PaymentFailedException + + 4 + + + 0 + + + 0 + + + 0 + + + 0 +
App\Policies\StorePolicy + + 18 + + + 0 + + + 56.15 + + + 6.02 + + + 0 +
App\Policies\Concerns\ChecksStoreRole + + 17 + + + 1 + + + 76.11 + + + 13.05 + + + 17.85 +
App\Livewire\Settings\TwoFactor + + 84 + + + 32 + + + 470.65 + + + 67.98 + + + 36.34 +
App\Livewire\Settings\DeleteUserForm + + 12 + + + 3 + + + 18.58 + + + 23.22 + + + 31.94 +
App\Livewire\Settings\TwoFactor\RecoveryCodes + + 26 + + + 10 + + + 60.23 + + + 11.58 + + + 36.44 +
App\Livewire\Settings\Password + + 20 + + + 3 + + + 86.37 + + + 28.79 + + + 26.54 +
App\Livewire\Settings\Profile + + 41 + + + 11 + + + 148.49 + + + 21.38 + + + 32.69 +
App\Livewire\Settings\Appearance + + 4 + + + 1 + + + 0 + + + 0 + + + 31.94 +
App\Livewire\Storefront\Products\Show + + 52 + + + 1 + + + 416.15 + + + 38.59 + + + 10.56 +
App\Livewire\Storefront\Home + + 31 + + + 7 + + + 95.18 + + + 33.99 + + + 30.85 +
App\Livewire\Storefront\Checkout\Show + + 121 + + + 14 + + + 1932.88 + + + 105.05 + + + 23.92 +
App\Livewire\Storefront\Checkout\Confirmation + + 15 + + + 1 + + + 46.51 + + + 19.73 + + + 18.88 +
App\Livewire\Storefront\Search\Index + + 20 + + + 2 + + + 78.87 + + + 26.29 + + + 22.51 +
App\Livewire\Storefront\CartDrawer + + 27 + + + 2 + + + 132.83 + + + 20.44 + + + 19.79 +
App\Livewire\Storefront\Cart\Show + + 51 + + + 1 + + + 284.98 + + + 27.72 + + + 10.66 +
App\Livewire\Storefront\Account\Dashboard + + 15 + + + 2 + + + 56.47 + + + 32.27 + + + 25.34 +
App\Livewire\Storefront\Account\Auth\Login + + 32 + + + 1 + + + 169.92 + + + 29.74 + + + 13.32 +
App\Livewire\Storefront\Account\Auth\Register + + 30 + + + 2 + + + 197.65 + + + 57.85 + + + 18.88 +
App\Livewire\Storefront\Account\Addresses\Index + + 47 + + + 5 + + + 518.06 + + + 120.13 + + + 23.11 +
App\Livewire\Storefront\Account\Orders\Index + + 15 + + + 2 + + + 49.83 + + + 33.22 + + + 25.34 +
App\Livewire\Storefront\Account\Orders\Show + + 16 + + + 2 + + + 70.31 + + + 31.25 + + + 24.69 +
App\Livewire\Storefront\Collections\Index + + 14 + + + 1 + + + 34.87 + + + 27.12 + + + 19.47 +
App\Livewire\Storefront\Collections\Show + + 30 + + + 2 + + + 177.2 + + + 42.96 + + + 18.88 +
App\Livewire\Storefront\Pages\Show + + 16 + + + 1 + + + 50.72 + + + 27.31 + + + 18.35 +
App\Livewire\Storefront\Concerns\EnsuresStore + + 13 + + + 2 + + + 33.69 + + + 7.22 + + + 26.8 +
App\Livewire\Admin\Customers\Index + + 20 + + + 2 + + + 152.93 + + + 39.55 + + + 22.51 +
App\Livewire\Admin\Customers\Show + + 15 + + + 1 + + + 148.68 + + + 38.45 + + + 18.88 +
App\Livewire\Admin\Settings\Taxes + + 29 + + + 6 + + + 293.25 + + + 101.82 + + + 29.92 +
App\Livewire\Admin\Settings\Index + + 27 + + + 7 + + + 200.67 + + + 105.62 + + + 32.32 +
App\Livewire\Admin\Settings\Shipping + + 61 + + + 7 + + + 562.32 + + + 129.27 + + + 23.84 +
App\Livewire\Admin\Dashboard + + 30 + + + 9 + + + 240.37 + + + 73.96 + + + 33.87 +
App\Livewire\Admin\Products\Index + + 38 + + + 4 + + + 242.03 + + + 26.89 + + + 23 +
App\Livewire\Admin\Products\Form + + 62 + + + 12 + + + 1005.38 + + + 84.49 + + + 29.21 +
App\Livewire\Admin\Auth\Login + + 37 + + + 3 + + + 294.41 + + + 52.34 + + + 20.58 +
App\Livewire\Admin\Navigation\Index + + 71 + + + 7 + + + 860.77 + + + 89.25 + + + 22.38 +
App\Livewire\Admin\Discounts\Index + + 21 + + + 3 + + + 96.79 + + + 25.25 + + + 26.04 +
App\Livewire\Admin\Discounts\Form + + 56 + + + 11 + + + 729.11 + + + 71.41 + + + 29.36 +
App\Livewire\Admin\Orders\Index + + 35 + + + 5 + + + 272.32 + + + 44.51 + + + 26.04 +
App\Livewire\Admin\Orders\Show + + 99 + + + 2 + + + 846.19 + + + 60.9 + + + 10.81 +
App\Livewire\Admin\Collections\Index + + 16 + + + 2 + + + 97.67 + + + 26.86 + + + 24.69 +
App\Livewire\Admin\Collections\Form + + 62 + + + 8 + + + 972.06 + + + 65.16 + + + 25.01 +
App\Livewire\Admin\Pages\Index + + 22 + + + 2 + + + 135.93 + + + 39.21 + + + 21.62 +
App\Livewire\Admin\Pages\Form + + 45 + + + 7 + + + 641.02 + + + 52.05 + + + 26.91 +
App\Livewire\Admin\Apps\Index + + 29 + + + 4 + + + 325.53 + + + 55.17 + + + 25.68 +
App\Livewire\Admin\Themes\Index + + 35 + + + 3 + + + 279.68 + + + 41.68 + + + 21.08 +
App\Livewire\Admin\Analytics\Index + + 19 + + + 2 + + + 301.19 + + + 55.61 + + + 23 +
App\Livewire\Admin\Developers\Index + + 44 + + + 8 + + + 400.08 + + + 197.06 + + + 28.55 +
App\Livewire\Actions\Logout + + 11 + + + 3 + + + 4.75 + + + 9.51 + + + 32.86 +
App\Support\HandleGenerator + + 26 + + + 0 + + + 248.8 + + + 28.43 + + + 0 +
App\Support\CartSession + + 34 + + + 0 + + + 181.32 + + + 11.96 + + + 0 +
App\Http\Middleware\ResolveStore + + 52 + + + 0 + + + 390.14 + + + 63.02 + + + 0 +
App\Http\Controllers\Controller + + 4 + + + 1 + + + 0 + + + 0 + + + 31.94 +
App\Actions\Fortify\ResetUserPassword + + 10 + + + 5 + + + 18 + + + 16 + + + 38.99 +
App\Actions\Fortify\CreateNewUser + + 10 + + + 5 + + + 38.77 + + + 27.7 + + + 38.99 +
App\Jobs\ExpireAbandonedCheckouts + + 11 + + + 0 + + + 20.9 + + + 23.22 + + + 0 +
App\Jobs\CleanupAbandonedCarts + + 9 + + + 0 + + + 10 + + + 16 + + + 0 +
App\Jobs\AggregateAnalytics + + 27 + + + 0 + + + 532.19 + + + 114.04 + + + 0 +
App\Jobs\CancelUnpaidBankTransferOrders + + 11 + + + 0 + + + 33 + + + 48 + + + 0 +
App\Jobs\ProcessMediaUpload + + 14 + + + 0 + + + 23.26 + + + 29.08 + + + 0 +
App\Jobs\DeliverWebhook + + 44 + + + 7 + + + 618.62 + + + 54.3 + + + 27.15 +
App\Events\OrderRefunded + + 8 + + + 0 + + + 2 + + + 4 + + + 0 +
App\Events\OrderCancelled + + 8 + + + 0 + + + 0 + + + 0 + + + 0 +
App\Events\OrderCreated + + 8 + + + 0 + + + 0 + + + 0 + + + 0 +
App\Events\OrderPaid + + 8 + + + 0 + + + 0 + + + 0 + + + 0 +
App\Events\FulfillmentDelivered + + 8 + + + 0 + + + 0 + + + 0 + + + 0 +
App\Events\OrderFulfilled + + 8 + + + 0 + + + 0 + + + 0 + + + 0 +
App\Observers\ProductObserver + + 19 + + + 0 + + + 15.85 + + + 9.51 + + + 0 +
App\Listeners\DispatchOrderWebhooks + + 23 + + + 0 + + + 93.21 + + + 86.04 + + + 0 +
App\Services\WebhookService + + 19 + + + 3 + + + 130.8 + + + 43.6 + + + 27.07 +
App\Services\OrderService + + 79 + + + 2 + + + 1476.23 + + + 103.18 + + + 12.05 +
App\Services\Payments\MockPaymentProvider + + 35 + + + 6 + + + 335.2 + + + 76.18 + + + 27.93 +
App\Services\CheckoutService + + 160 + + + 10 + + + 1835.38 + + + 120.13 + + + 18.35 +
App\Services\FulfillmentService + + 48 + + + 8 + + + 609.51 + + + 48.56 + + + 27.63 +
App\Services\TaxCalculator + + 30 + + + 4 + + + 431.81 + + + 30.3 + + + 25.34 +
App\Services\ThemeSettingsService + + 23 + + + 9 + + + 153.73 + + + 37.84 + + + 36.61 +
App\Services\InventoryService + + 45 + + + 0 + + + 312 + + + 7.96 + + + 0 +
App\Services\NavigationService + + 17 + + + 5 + + + 99.91 + + + 95.92 + + + 33.66 +
App\Services\RefundService + + 39 + + + 1 + + + 479.27 + + + 32.84 + + + 12.13 +
App\Services\ProductService + + 119 + + + 10 + + + 1796.53 + + + 106.94 + + + 20.9 +
App\Services\ShippingCalculator + + 74 + + + 12 + + + 1220.05 + + + 53.25 + + + 27.35 +
App\Services\AnalyticsService + + 12 + + + 6 + + + 124 + + + 62 + + + 38.99 +
App\Services\CartService + + 130 + + + 0 + + + 1620.1 + + + 43.89 + + + 0 +
App\Services\PricingEngine + + 59 + + + 0 + + + 1072.41 + + + 64.84 + + + 0 +
App\Services\DiscountService + + 77 + + + 3 + + + 889.73 + + + 27.28 + + + 14.78 +
App\Services\SearchService + + 52 + + + 7 + + + 763.37 + + + 61.07 + + + 25.43 +
App\Services\VariantMatrixService + + 63 + + + 7 + + + 644.82 + + + 64.08 + + + 23.53 +
App\Concerns\ProfileValidationRules + + 16 + + + 15 + + + 63.4 + + + 27.74 + + + 44.04 +
App\Concerns\PasswordValidationRules + + 12 + + + 10 + + + 18.58 + + + 24.77 + + + 43.23 +
App\ValueObjects\PaymentResult + + 19 + + + 0 + + + 42 + + + 31.5 + + + 0 +
App\ValueObjects\DiscountResult + + 7 + + + 3 + + + 4.75 + + + 9.51 + + + 37.52 +
App\ValueObjects\PricingResult + + 11 + + + 15 + + + 102.8 + + + 102.8 + + + 46.17 +
App\ValueObjects\TaxLine + + 11 + + + 3 + + + 23.22 + + + 20.64 + + + 32.86 +
App\ValueObjects\RefundResult + + 11 + + + 0 + + + 19.65 + + + 19.65 + + + 0 +
+
+
+
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/report/oop.html b/report/oop.html new file mode 100644 index 00000000..c7327a83 --- /dev/null +++ b/report/oop.html @@ -0,0 +1,4125 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + + + +
+
+
+
classes
+
+ 142 (100 %) +
+
+
+
+
+
interfaces
+
1 (1 %) +
+
+
+
+
+
average LCOM
+
2.04
+
+
+
+
+
logical lines of code by class
+
29
+
+
+
+
+
logical lines of code by method
+
10
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassLCOMVolumeClass cycl.Max method cycl.BugsDifficulty
App\Auth\CustomerUserProvider + + 3 + + + 797.01 + + + 20 + + + 8 + + + 0.27 + + + 19.88 +
App\Providers\AppServiceProvider + + 2 + + + 104 + + + 2 + + + 2 + + + 0.03 + + + 1.71 +
App\Providers\FortifyServiceProvider + + 2 + + + 116.76 + + + 1 + + + 1 + + + 0.04 + + + 2.3 +
App\Models\OrderLine + + 3 + + + 88.81 + + + 1 + + + 1 + + + 0.03 + + + 0.79 +
App\Models\WebhookSubscription + + 2 + + + 44.97 + + + 1 + + + 1 + + + 0.01 + + + 0.55 +
App\Models\ThemeFile + + 2 + + + 30 + + + 1 + + + 1 + + + 0.01 + + + 0.57 +
App\Models\ProductOption + + 2 + + + 16.25 + + + 1 + + + 1 + + + 0.01 + + + 0.63 +
App\Models\NavigationItem + + 3 + + + 120 + + + 2 + + + 2 + + + 0.04 + + + 2.65 +
App\Models\Refund + + 2 + + + 36 + + + 1 + + + 1 + + + 0.01 + + + 0.64 +
App\Models\InventoryItem + + 3 + + + 50.72 + + + 1 + + + 1 + + + 0.02 + + + 1.71 +
App\Models\NavigationMenu + + 1 + + + 19.65 + + + 1 + + + 1 + + + 0.01 + + + 0.5 +
App\Models\App + + 2 + + + 34.87 + + + 1 + + + 1 + + + 0.01 + + + 0.56 +
App\Models\CartLine + + 1 + + + 38.04 + + + 1 + + + 1 + + + 0.01 + + + 0.63 +
App\Models\AppInstallation + + 3 + + + 55.35 + + + 1 + + + 1 + + + 0.02 + + + 0.65 +
App\Models\Cart + + 3 + + + 64.53 + + + 1 + + + 1 + + + 0.02 + + + 2.17 +
App\Models\Discount + + 2 + + + 222.97 + + + 8 + + + 8 + + + 0.07 + + + 5.81 +
App\Models\Product + + 3 + + + 97.67 + + + 1 + + + 1 + + + 0.03 + + + 0.71 +
App\Models\Order + + 3 + + + 322.02 + + + 5 + + + 5 + + + 0.11 + + + 5.76 +
App\Models\Store + + 5 + + + 65.73 + + + 1 + + + 1 + + + 0.02 + + + 0.7 +
App\Models\StoreDomain + + 2 + + + 33 + + + 1 + + + 1 + + + 0.01 + + + 0.64 +
App\Models\Theme + + 3 + + + 39 + + + 1 + + + 1 + + + 0.01 + + + 0.71 +
App\Models\ProductMedia + + 2 + + + 59.21 + + + 1 + + + 1 + + + 0.02 + + + 0.58 +
App\Models\User + + 3 + + + 267.19 + + + 4 + + + 4 + + + 0.09 + + + 3.36 +
App\Models\WebhookDelivery + + 2 + + + 57.36 + + + 1 + + + 1 + + + 0.02 + + + 0.64 +
App\Models\Fulfillment + + 3 + + + 56.47 + + + 1 + + + 1 + + + 0.02 + + + 0.78 +
App\Models\ThemeSettings + + 2 + + + 28.07 + + + 1 + + + 1 + + + 0.01 + + + 0.67 +
App\Models\Checkout + + 2 + + + 110.36 + + + 1 + + + 1 + + + 0.04 + + + 0.78 +
App\Models\Payment + + 3 + + + 58.81 + + + 1 + + + 1 + + + 0.02 + + + 0.7 +
App\Models\AnalyticsDaily + + 2 + + + 107.31 + + + 1 + + + 1 + + + 0.04 + + + 1.08 +
App\Models\Customer + + 4 + + + 89.62 + + + 1 + + + 1 + + + 0.03 + + + 0.82 +
App\Models\ProductVariant + + 4 + + + 110.36 + + + 1 + + + 1 + + + 0.04 + + + 0.72 +
App\Models\ProductOptionValue + + 1 + + + 13.93 + + + 1 + + + 1 + + + 0 + + + 0.63 +
App\Models\TaxSettings + + 2 + + + 55.35 + + + 1 + + + 1 + + + 0.02 + + + 0.7 +
App\Models\ShippingZone + + 2 + + + 36 + + + 1 + + + 1 + + + 0.01 + + + 0.71 +
App\Models\Collection + + 2 + + + 43.19 + + + 1 + + + 1 + + + 0.01 + + + 0.61 +
App\Models\Page + + 1 + + + 30 + + + 1 + + + 1 + + + 0.01 + + + 0.64 +
App\Models\ShippingRate + + 2 + + + 44.38 + + + 1 + + + 1 + + + 0.01 + + + 0.75 +
App\Models\StoreSettings + + 2 + + + 23.26 + + + 1 + + + 1 + + + 0.01 + + + 0.7 +
App\Models\CustomerAddress + + 2 + + + 33 + + + 1 + + + 1 + + + 0.01 + + + 0.64 +
App\Models\StoreUser + + 2 + + + 25.85 + + + 2 + + + 2 + + + 0.01 + + + 2 +
App\Models\FulfillmentLine + + 1 + + + 16.25 + + + 1 + + + 1 + + + 0.01 + + + 0.63 +
App\Models\AnalyticsEvent + + 1 + + + 44.97 + + + 1 + + + 1 + + + 0.01 + + + 0.6 +
App\Models\Scopes\StoreScope + + 1 + + + 41.21 + + + 2 + + + 2 + + + 0.01 + + + 3.6 +
App\Models\Organization + + 1 + + + 8 + + + 1 + + + 1 + + + 0 + + + 0.5 +
App\Models\Concerns\BelongsToStore + + 2 + + + 44.38 + + + 3 + + + 3 + + + 0.01 + + + 5 +
App\Exceptions\InsufficientInventoryException + + 0 + + + 0 + + + 1 + + + 0 + + + 0 + + + 0 +
App\Exceptions\FulfillmentGuardException + + 0 + + + 0 + + + 1 + + + 0 + + + 0 + + + 0 +
App\Exceptions\InvalidDiscountException + + 7 + + + 106.27 + + + 2 + + + 2 + + + 0.04 + + + 1.27 +
App\Exceptions\PaymentFailedException + + 0 + + + 0 + + + 1 + + + 0 + + + 0 + + + 0 +
App\Policies\StorePolicy + + 3 + + + 56.15 + + + 1 + + + 1 + + + 0.02 + + + 9.33 +
App\Policies\Concerns\ChecksStoreRole + + 1 + + + 76.11 + + + 3 + + + 2 + + + 0.03 + + + 5.83 +
App\Livewire\Settings\TwoFactor + + 1 + + + 470.65 + + + 9 + + + 3 + + + 0.16 + + + 6.92 +
App\Livewire\Settings\DeleteUserForm + + 1 + + + 18.58 + + + 1 + + + 1 + + + 0.01 + + + 0 +
App\Livewire\Settings\TwoFactor\RecoveryCodes + + 1 + + + 60.23 + + + 4 + + + 4 + + + 0.02 + + + 5.2 +
App\Livewire\Settings\Password + + 1 + + + 86.37 + + + 2 + + + 2 + + + 0.03 + + + 3 +
App\Livewire\Settings\Profile + + 5 + + + 148.49 + + + 6 + + + 3 + + + 0.05 + + + 6.94 +
App\Livewire\Settings\Appearance + + 0 + + + 0 + + + 1 + + + 0 + + + 0 + + + 0 +
App\Livewire\Storefront\Products\Show + + 1 + + + 416.15 + + + 5 + + + 3 + + + 0.14 + + + 10.78 +
App\Livewire\Storefront\Home + + 2 + + + 95.18 + + + 5 + + + 3 + + + 0.03 + + + 2.8 +
App\Livewire\Storefront\Checkout\Show + + 1 + + + 1932.88 + + + 25 + + + 9 + + + 0.64 + + + 18.4 +
App\Livewire\Storefront\Checkout\Confirmation + + 1 + + + 46.51 + + + 1 + + + 1 + + + 0.02 + + + 2.36 +
App\Livewire\Storefront\Search\Index + + 2 + + + 78.87 + + + 2 + + + 2 + + + 0.03 + + + 3 +
App\Livewire\Storefront\CartDrawer + + 2 + + + 132.83 + + + 3 + + + 3 + + + 0.04 + + + 6.5 +
App\Livewire\Storefront\Cart\Show + + 2 + + + 284.98 + + + 6 + + + 3 + + + 0.09 + + + 10.28 +
App\Livewire\Storefront\Account\Dashboard + + 2 + + + 56.47 + + + 1 + + + 1 + + + 0.02 + + + 1.75 +
App\Livewire\Storefront\Account\Auth\Login + + 2 + + + 169.92 + + + 4 + + + 3 + + + 0.06 + + + 5.71 +
App\Livewire\Storefront\Account\Auth\Register + + 2 + + + 197.65 + + + 2 + + + 2 + + + 0.07 + + + 3.42 +
App\Livewire\Storefront\Account\Addresses\Index + + 5 + + + 518.06 + + + 2 + + + 2 + + + 0.17 + + + 4.31 +
App\Livewire\Storefront\Account\Orders\Index + + 2 + + + 49.83 + + + 1 + + + 1 + + + 0.02 + + + 1.5 +
App\Livewire\Storefront\Account\Orders\Show + + 1 + + + 70.31 + + + 1 + + + 1 + + + 0.02 + + + 2.25 +
App\Livewire\Storefront\Collections\Index + + 2 + + + 34.87 + + + 1 + + + 1 + + + 0.01 + + + 1.29 +
App\Livewire\Storefront\Collections\Show + + 2 + + + 177.2 + + + 3 + + + 3 + + + 0.06 + + + 4.13 +
App\Livewire\Storefront\Pages\Show + + 1 + + + 50.72 + + + 1 + + + 1 + + + 0.02 + + + 1.86 +
App\Livewire\Storefront\Concerns\EnsuresStore + + 1 + + + 33.69 + + + 3 + + + 3 + + + 0.01 + + + 4.67 +
App\Livewire\Admin\Customers\Index + + 2 + + + 152.93 + + + 1 + + + 1 + + + 0.05 + + + 3.87 +
App\Livewire\Admin\Customers\Show + + 1 + + + 148.68 + + + 2 + + + 2 + + + 0.05 + + + 3.87 +
App\Livewire\Admin\Settings\Taxes + + 2 + + + 293.25 + + + 5 + + + 5 + + + 0.1 + + + 2.88 +
App\Livewire\Admin\Settings\Index + + 2 + + + 200.67 + + + 1 + + + 1 + + + 0.07 + + + 1.9 +
App\Livewire\Admin\Settings\Shipping + + 4 + + + 562.32 + + + 2 + + + 2 + + + 0.19 + + + 4.35 +
App\Livewire\Admin\Dashboard + + 3 + + + 240.37 + + + 2 + + + 2 + + + 0.08 + + + 3.25 +
App\Livewire\Admin\Products\Index + + 2 + + + 242.03 + + + 3 + + + 2 + + + 0.08 + + + 9 +
App\Livewire\Admin\Products\Form + + 2 + + + 1005.38 + + + 20 + + + 11 + + + 0.34 + + + 11.9 +
App\Livewire\Admin\Auth\Login + + 2 + + + 294.41 + + + 4 + + + 4 + + + 0.1 + + + 5.63 +
App\Livewire\Admin\Navigation\Index + + 5 + + + 860.77 + + + 7 + + + 6 + + + 0.29 + + + 9.64 +
App\Livewire\Admin\Discounts\Index + + 2 + + + 96.79 + + + 1 + + + 1 + + + 0.03 + + + 3.83 +
App\Livewire\Admin\Discounts\Form + + 2 + + + 729.11 + + + 12 + + + 8 + + + 0.24 + + + 10.21 +
App\Livewire\Admin\Orders\Index + + 2 + + + 272.32 + + + 1 + + + 1 + + + 0.09 + + + 6.12 +
App\Livewire\Admin\Orders\Show + + 2 + + + 846.19 + + + 13 + + + 6 + + + 0.28 + + + 13.89 +
App\Livewire\Admin\Collections\Index + + 2 + + + 97.67 + + + 1 + + + 1 + + + 0.03 + + + 3.64 +
App\Livewire\Admin\Collections\Form + + 1 + + + 972.06 + + + 14 + + + 8 + + + 0.32 + + + 14.92 +
App\Livewire\Admin\Pages\Index + + 3 + + + 135.93 + + + 1 + + + 1 + + + 0.05 + + + 3.47 +
App\Livewire\Admin\Pages\Form + + 2 + + + 641.02 + + + 13 + + + 10 + + + 0.21 + + + 12.31 +
App\Livewire\Admin\Apps\Index + + 3 + + + 325.53 + + + 1 + + + 1 + + + 0.11 + + + 5.9 +
App\Livewire\Admin\Themes\Index + + 4 + + + 279.68 + + + 2 + + + 2 + + + 0.09 + + + 6.71 +
App\Livewire\Admin\Analytics\Index + + 1 + + + 301.19 + + + 3 + + + 3 + + + 0.1 + + + 5.42 +
App\Livewire\Admin\Developers\Index + + 4 + + + 400.08 + + + 1 + + + 1 + + + 0.13 + + + 2.03 +
App\Livewire\Actions\Logout + + 1 + + + 4.75 + + + 1 + + + 1 + + + 0 + + + 0.5 +
App\Support\HandleGenerator + + 2 + + + 248.8 + + + 4 + + + 3 + + + 0.08 + + + 8.75 +
App\Support\CartSession + + 3 + + + 181.32 + + + 7 + + + 4 + + + 0.06 + + + 15.17 +
App\Http\Middleware\ResolveStore + + 1 + + + 390.14 + + + 8 + + + 5 + + + 0.13 + + + 6.19 +
App\Http\Controllers\Controller + + 0 + + + 0 + + + 1 + + + 0 + + + 0 + + + 0 +
App\Actions\Fortify\ResetUserPassword + + 1 + + + 18 + + + 1 + + + 1 + + + 0.01 + + + 0 +
App\Actions\Fortify\CreateNewUser + + 1 + + + 38.77 + + + 1 + + + 1 + + + 0.01 + + + 1.4 +
App\Jobs\ExpireAbandonedCheckouts + + 1 + + + 20.9 + + + 1 + + + 1 + + + 0.01 + + + 0 +
App\Jobs\CleanupAbandonedCarts + + 1 + + + 10 + + + 1 + + + 1 + + + 0 + + + 0 +
App\Jobs\AggregateAnalytics + + 2 + + + 532.19 + + + 3 + + + 3 + + + 0.18 + + + 4.67 +
App\Jobs\CancelUnpaidBankTransferOrders + + 1 + + + 33 + + + 1 + + + 1 + + + 0.01 + + + 0 +
App\Jobs\ProcessMediaUpload + + 2 + + + 23.26 + + + 1 + + + 1 + + + 0.01 + + + 0.8 +
App\Jobs\DeliverWebhook + + 2 + + + 618.62 + + + 8 + + + 6 + + + 0.21 + + + 11.39 +
App\Events\OrderRefunded + + 1 + + + 2 + + + 1 + + + 1 + + + 0 + + + 0 +
App\Events\OrderCancelled + + 1 + + + 0 + + + 1 + + + 1 + + + 0 + + + 0 +
App\Events\OrderCreated + + 1 + + + 0 + + + 1 + + + 1 + + + 0 + + + 0 +
App\Events\OrderPaid + + 1 + + + 0 + + + 1 + + + 1 + + + 0 + + + 0 +
App\Events\FulfillmentDelivered + + 1 + + + 0 + + + 1 + + + 1 + + + 0 + + + 0 +
App\Events\OrderFulfilled + + 1 + + + 0 + + + 1 + + + 1 + + + 0 + + + 0 +
App\Observers\ProductObserver + + 2 + + + 15.85 + + + 1 + + + 1 + + + 0.01 + + + 0 +
App\Listeners\DispatchOrderWebhooks + + 2 + + + 93.21 + + + 1 + + + 1 + + + 0.03 + + + 0 +
App\Services\WebhookService + + 2 + + + 130.8 + + + 2 + + + 2 + + + 0.04 + + + 3 +
App\Services\OrderService + + 3 + + + 1476.23 + + + 25 + + + 12 + + + 0.49 + + + 14.31 +
App\Services\Payments\MockPaymentProvider + + 2 + + + 335.2 + + + 5 + + + 4 + + + 0.11 + + + 4.4 +
App\Services\CheckoutService + + 3 + + + 1835.38 + + + 30 + + + 8 + + + 0.61 + + + 15.28 +
App\Services\FulfillmentService + + 3 + + + 609.51 + + + 14 + + + 7 + + + 0.2 + + + 12.55 +
App\Services\TaxCalculator + + 2 + + + 431.81 + + + 10 + + + 6 + + + 0.14 + + + 14.25 +
App\Services\ThemeSettingsService + + 2 + + + 153.73 + + + 3 + + + 3 + + + 0.05 + + + 4.06 +
App\Services\InventoryService + + 5 + + + 312 + + + 4 + + + 4 + + + 0.1 + + + 39.21 +
App\Services\NavigationService + + 2 + + + 99.91 + + + 1 + + + 1 + + + 0.03 + + + 1.04 +
App\Services\RefundService + + 2 + + + 479.27 + + + 10 + + + 10 + + + 0.16 + + + 14.6 +
App\Services\ProductService + + 4 + + + 1796.53 + + + 33 + + + 16 + + + 0.6 + + + 16.8 +
App\Services\ShippingCalculator + + 2 + + + 1220.05 + + + 36 + + + 12 + + + 0.41 + + + 22.91 +
App\Services\AnalyticsService + + 2 + + + 124 + + + 2 + + + 2 + + + 0.04 + + + 2 +
App\Services\CartService + + 3 + + + 1620.1 + + + 20 + + + 10 + + + 0.54 + + + 36.91 +
App\Services\PricingEngine + + 2 + + + 1072.41 + + + 16 + + + 16 + + + 0.36 + + + 16.54 +
App\Services\DiscountService + + 3 + + + 889.73 + + + 21 + + + 15 + + + 0.3 + + + 32.62 +
App\Services\SearchService + + 2 + + + 763.37 + + + 15 + + + 7 + + + 0.25 + + + 12.5 +
App\Services\VariantMatrixService + + 1 + + + 644.82 + + + 13 + + + 10 + + + 0.21 + + + 10.06 +
App\Concerns\ProfileValidationRules + + 1 + + + 63.4 + + + 2 + + + 2 + + + 0.02 + + + 2.29 +
App\Concerns\PasswordValidationRules + + 2 + + + 18.58 + + + 1 + + + 1 + + + 0.01 + + + 0.75 +
App\ValueObjects\PaymentResult + + 2 + + + 42 + + + 1 + + + 1 + + + 0.01 + + + 1.33 +
App\ValueObjects\DiscountResult + + 1 + + + 4.75 + + + 1 + + + 1 + + + 0 + + + 0 +
App\ValueObjects\PricingResult + + 2 + + + 102.8 + + + 1 + + + 1 + + + 0.03 + + + 1 +
App\ValueObjects\TaxLine + + 2 + + + 23.22 + + + 1 + + + 1 + + + 0.01 + + + 1.13 +
App\ValueObjects\RefundResult + + 2 + + + 19.65 + + + 1 + + + 1 + + + 0.01 + + + 1 +
+
+
+
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + diff --git a/report/package_relations.html b/report/package_relations.html new file mode 100644 index 00000000..5f983ac9 --- /dev/null +++ b/report/package_relations.html @@ -0,0 +1,1016 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + + + +
+
+
+

Package relations

+
+
+
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/report/packages.html b/report/packages.html new file mode 100644 index 00000000..a3d4299c --- /dev/null +++ b/report/packages.html @@ -0,0 +1,1301 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + +
+
+
+

Packages

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameClassesAbstractionInstabilityDistanceOutgoing class dep.Outgoing package dep.Incoming class dep.Incoming package dep.
App\Auth100.8750.1257511
App\Contracts110.6250.6255332
App\Providers201012700
App\Models4000.120.881067336
App\Models\Scopes100.750.253111
App\Models\Concerns11113300
App\Exceptions400.20.81141
App\Policies10102100
App\Policies\Concerns11112200
App\Livewire\Settings50109500
App\Livewire\Settings\TwoFactor10102200
App\Livewire\Storefront\Products10104400
App\Livewire\Storefront20107500
App\Livewire\Storefront\Checkout20106500
App\Livewire\Storefront\Search10103300
App\Livewire\Storefront\Cart10103300
App\Livewire\Storefront\Account10104400
App\Livewire\Storefront\Account\Auth20106500
App\Livewire\Storefront\Account\Addresses10104400
App\Livewire\Storefront\Account\Orders20104400
App\Livewire\Storefront\Collections20103300
App\Livewire\Storefront\Pages10103300
App\Livewire\Storefront\Concerns11111100
App\Livewire\Admin\Customers20103300
App\Livewire\Admin\Settings30105300
App\Livewire\Admin10105500
App\Livewire\Admin\Products20105500
App\Livewire\Admin\Auth10106400
App\Livewire\Admin\Navigation10105400
App\Livewire\Admin\Discounts20103300
App\Livewire\Admin\Orders20106400
App\Livewire\Admin\Collections20105400
App\Livewire\Admin\Pages20104400
App\Livewire\Admin\Apps10104300
App\Livewire\Admin\Themes10104400
App\Livewire\Admin\Analytics10103300
App\Livewire\Admin\Developers10105500
App\Livewire\Actions100.6670.3332111
App\Support200.4170.5835377
App\Http\Middleware10109600
App\Http\Controllers110000
App\Actions\Fortify20104300
App\Jobs600.9470.05318711
App\Events600.3750.6253152
App\Observers10102200
App\Listeners10105300
App\Services1700.8690.131531386
App\Services\Payments10107500
App\Concerns21112200
App\ValueObjects500.2860.7142153
+
+
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/report/panel.html b/report/panel.html new file mode 100644 index 00000000..f97f7380 --- /dev/null +++ b/report/panel.html @@ -0,0 +1,243 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + +
+
+
+
4961
+
lines of code
+
+
+
+
+
+ 142 (100 %) + +
+
classes
+
+
+
+
+
1 (1 %) + +
+
interfaces
+
+
+ + +
+ +
+ +
+
+
3
+
methods by class
+
+
+
+
+
29
+
logical lines of code by class
+
+
+ + +
+
+
10
+
logical lines of code by method
+
+
+ +
+
+
2.04
+
average LCOM
+
+
+
+ +
+
+
+
Top 10 ClassRank
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassClassRank
App\Models\Order 81.02 + 44.71 + 0.1
App\Models\Store 97.12 + 56.27 + 0.06
App\Models\Cart 96 + 57.91 + 0.04
App\Models\Product 95.92 + 55.07 + 0.04
App\Models\WebhookSubscription 100.98 + 62.64 + 0.02
App\Models\User 92.09 + 49.64 + 0.02
App\Models\Fulfillment 99.17 + 59.7 + 0.02
App\Models\Checkout 99.55 + 60.56 + 0.02
App\Support\CartSession 49.84 + 49.84 + 0.02
App\Models\NavigationItem 88.64 + 55.06 + 0.01
+ +
+ +
+
+
+
+ 4.27
+
Average cyclomatic complexity by class
+
+
+
+
+
+

Maintainability / complexity

+
+
+
+
+
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + diff --git a/report/relations.html b/report/relations.html new file mode 100644 index 00000000..17f23899 --- /dev/null +++ b/report/relations.html @@ -0,0 +1,1882 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + + + +
+
+
+

Object relations

+
+
+
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/report/violations.html b/report/violations.html new file mode 100644 index 00000000..a77a293b --- /dev/null +++ b/report/violations.html @@ -0,0 +1,999 @@ + + + + + PhpMetrics report + + + + + + + + + + +
+ +
+ + + + +
+
+
+ Created at 2026-04-13 05:14:05 , with PHPMetrics v2.9.1 (Jean-François Lépine). +
+ + + +
+
+
+
Violations
+
26
+
+
+
+
+
Information
+
0
+
+
+
+
+
Warnings
+
18
+
+
+
+
+
Errors
+
8
+
+
+
+
+
Criticals
+
0
+
+
+
+ +
+
+
+

Class Violations

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassViolations
+ + App\Livewire\Storefront\Checkout\Show + +
+
+
+ Probably bugged warning + +
+
This component contains in theory 0.64 bugs.
+
+ * Calculation is based on number of operators, operands, cyclomatic complexity
+ * See more details at https://en.wikipedia.org/wiki/Halstead_complexity_measures
+ * testsuites has dependency to this class.
+
+ Maybe you should check your unit tests for this class.
+ +
+
+
+ Probably bugged +
+ + App\Livewire\Admin\Products\Form + +
+
+
+ Too complex method code error + +
+
This class looks really complex.
+
+ * Algorithms are complex (Max cyclomatic complexity of class methods is 11)
+
+ Maybe you should delegate some code to other objects or split complex method.
+ +
+
+
+ Too complex method code +
+ + App\Livewire\Admin\Navigation\Index + +
+
+
+ Blob / God object error + +
+
A blob object (or "god class") does not follow the Single responsibility principle.
+
+ * object has lot of public methods (8, excluding getters and setters)
+ * object has a high Lack of cohesion of methods (LCOM=5)
+ * object knows everything (and use lot of external classes)
+
+ Maybe you should reducing the number of methods splitting this object in many sub objects.
+ +
+
+
+ Blob / God object +
+ + App\Services\OrderService + +
+
+
+ Too complex method code error + +
+
This class looks really complex.
+
+ * Algorithms are complex (Max cyclomatic complexity of class methods is 12)
+
+ Maybe you should delegate some code to other objects or split complex method.
+ +
+
+
+ Probably bugged warning + +
+
This component contains in theory 0.49 bugs.
+
+ * Calculation is based on number of operators, operands, cyclomatic complexity
+ * See more details at https://en.wikipedia.org/wiki/Halstead_complexity_measures
+ * testsuites has dependency to this class.
+
+ Maybe you should check your unit tests for this class.
+ +
+
+
+ Too complex method code + Probably bugged +
+ + App\Services\CheckoutService + +
+
+
+ Blob / God object error + +
+
A blob object (or "god class") does not follow the Single responsibility principle.
+
+ * object has lot of public methods (9, excluding getters and setters)
+ * object has a high Lack of cohesion of methods (LCOM=3)
+ * object knows everything (and use lot of external classes)
+
+ Maybe you should reducing the number of methods splitting this object in many sub objects.
+ +
+
+
+ Probably bugged warning + +
+
This component contains in theory 0.61 bugs.
+
+ * Calculation is based on number of operators, operands, cyclomatic complexity
+ * See more details at https://en.wikipedia.org/wiki/Halstead_complexity_measures
+ * testsuites has dependency to this class.
+
+ Maybe you should check your unit tests for this class.
+ +
+
+
+ Blob / God object + Probably bugged +
+ + App\Services\ProductService + +
+
+
+ Too complex method code error + +
+
This class looks really complex.
+
+ * Algorithms are complex (Max cyclomatic complexity of class methods is 16)
+
+ Maybe you should delegate some code to other objects or split complex method.
+ +
+
+
+ Probably bugged warning + +
+
This component contains in theory 0.6 bugs.
+
+ * Calculation is based on number of operators, operands, cyclomatic complexity
+ * See more details at https://en.wikipedia.org/wiki/Halstead_complexity_measures
+ * testsuites has dependency to this class.
+
+ Maybe you should check your unit tests for this class.
+ +
+
+
+ Too complex method code + Probably bugged +
+ + App\Services\ShippingCalculator + +
+
+
+ Too complex method code error + +
+
This class looks really complex.
+
+ * Algorithms are complex (Max cyclomatic complexity of class methods is 12)
+
+ Maybe you should delegate some code to other objects or split complex method.
+ +
+
+
+ Probably bugged warning + +
+
This component contains in theory 0.41 bugs.
+
+ * Calculation is based on number of operators, operands, cyclomatic complexity
+ * See more details at https://en.wikipedia.org/wiki/Halstead_complexity_measures
+ * testsuites has dependency to this class.
+
+ Maybe you should check your unit tests for this class.
+ +
+
+
+ Too complex method code + Probably bugged +
+ + App\Services\CartService + +
+
+
+ Probably bugged warning + +
+
This component contains in theory 0.54 bugs.
+
+ * Calculation is based on number of operators, operands, cyclomatic complexity
+ * See more details at https://en.wikipedia.org/wiki/Halstead_complexity_measures
+ * testsuites has dependency to this class.
+
+ Maybe you should check your unit tests for this class.
+ +
+
+
+ Probably bugged +
+ + App\Services\PricingEngine + +
+
+
+ Too complex method code error + +
+
This class looks really complex.
+
+ * Algorithms are complex (Max cyclomatic complexity of class methods is 16)
+
+ Maybe you should delegate some code to other objects or split complex method.
+ +
+
+
+ Probably bugged warning + +
+
This component contains in theory 0.36 bugs.
+
+ * Calculation is based on number of operators, operands, cyclomatic complexity
+ * See more details at https://en.wikipedia.org/wiki/Halstead_complexity_measures
+ * testsuites has dependency to this class.
+
+ Maybe you should check your unit tests for this class.
+ +
+
+
+ Too complex method code + Probably bugged +
+ + App\Services\DiscountService + +
+
+
+ Too complex method code error + +
+
This class looks really complex.
+
+ * Algorithms are complex (Max cyclomatic complexity of class methods is 15)
+
+ Maybe you should delegate some code to other objects or split complex method.
+ +
+
+
+ Too complex method code +
+
+
+
+
+
+
+

Package Violations

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PackageViolations
+ + App\Contracts + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\Models + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\Models\Concerns + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\Exceptions + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\Policies\Concerns + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\Livewire\Storefront\Concerns + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\Support + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\Events + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\Services + +
+
+
+ Stable Dependencies Principle warning + +
+
Packages should depend in the direction of stability.
+
+ This package is more stable (0.869) than 1 package(s) that it depends on.
+ The packages that are more stable are
+
+ * App\Jobs (0.947)
+ +
+
+
+ Stable Dependencies Principle +
+ + App\Concerns + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+ + App\ValueObjects + +
+
+
+ Stable Abstractions Principle warning + +
+
Packages should be either abstract and stable or concrete and instable.
+
+ This package is instable and abstract.
+ +
+
+
+ Stable Abstractions Principle +
+
+
+
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/layouts/admin-auth.blade.php b/resources/views/components/layouts/admin-auth.blade.php new file mode 100644 index 00000000..1b1860f2 --- /dev/null +++ b/resources/views/components/layouts/admin-auth.blade.php @@ -0,0 +1,25 @@ + + + + + + + {{ $title ?? __('Admin Login') }} + @vite(['resources/css/app.css', 'resources/js/app.js']) + @livewireStyles + @fluxAppearance + + +
+
+
+
+ {{ $slot }} +
+
+
+
+ @livewireScripts + @fluxScripts + + diff --git a/resources/views/components/layouts/admin.blade.php b/resources/views/components/layouts/admin.blade.php new file mode 100644 index 00000000..3dceff1d --- /dev/null +++ b/resources/views/components/layouts/admin.blade.php @@ -0,0 +1,142 @@ +@php + /** @var \App\Models\Store|null $currentStore */ + $currentStore = app()->bound('current_store') ? app('current_store') : null; + $user = auth()->user(); +@endphp + + + + + + + {{ $title ?? 'Shop Admin' }} + @vite(['resources/css/app.css', 'resources/js/app.js']) + @livewireStyles + @fluxAppearance + + + + + +
+ +
+
+ Shop Admin + @if ($currentStore !== null) + {{ $currentStore->name }} + @endif +
+
+ +
+ + + + + Dashboard + + + Products + + + Collections + + + Customers + + + Discounts + + + + + + Orders + + + + + + Pages + + + Navigation + + + Themes + + + + + + Analytics + + + + + + Settings + + + Apps + + + Developers + + + + + + + @if ($user !== null) + + + +
+
{{ $user->name }}
+
{{ $user->email }}
+
+ +
+ @csrf + + Log out + +
+
+
+ @endif +
+ + + + + @if ($user !== null) + + + +
+
{{ $user->name }}
+
{{ $user->email }}
+
+ +
+ @csrf + + Log out + +
+
+
+ @endif +
+ + + {{ $slot }} + + + @livewireScripts + @fluxScripts + + diff --git a/resources/views/components/layouts/storefront.blade.php b/resources/views/components/layouts/storefront.blade.php new file mode 100644 index 00000000..3096c807 --- /dev/null +++ b/resources/views/components/layouts/storefront.blade.php @@ -0,0 +1,43 @@ +@php + /** @var \App\Services\ThemeSettingsService $themeSettingsService */ + $themeSettingsService = app(\App\Services\ThemeSettingsService::class); + $currentStore = app()->bound('current_store') ? app('current_store') : \App\Models\Store::first(); + $themeSettings = $currentStore ? $themeSettingsService->forStore($currentStore) : $themeSettingsService->defaultSettings(); + $announcement = $themeSettings['announcement'] ?? null; + $footerText = $themeSettings['footer_text'] ?? '(c) Shop'; + $primaryColor = $themeSettings['colors']['primary'] ?? '#111'; +@endphp + + + + + + + {{ $title ?? ($currentStore->name ?? config('app.name')) }} + + + @vite(['resources/css/app.css']) + @livewireStyles + @fluxAppearance + + + + @include('storefront.partials.announcement', ['announcement' => $announcement]) + @include('storefront.partials.header', ['store' => $currentStore]) + +
+ {{ $slot }} +
+ + @include('storefront.partials.footer', ['footerText' => $footerText]) + +
+ +
+ + @livewireScripts + @fluxScripts + + diff --git a/resources/views/components/storefront/price.blade.php b/resources/views/components/storefront/price.blade.php new file mode 100644 index 00000000..120a5281 --- /dev/null +++ b/resources/views/components/storefront/price.blade.php @@ -0,0 +1,20 @@ +@props([ + 'amount' => 0, + 'currency' => 'USD', +]) + +@php + $amountCents = is_numeric($amount) ? (int) $amount : 0; + $amountFormatted = number_format($amountCents / 100, 2); + $symbols = [ + 'USD' => '$', + 'EUR' => 'EUR ', + 'GBP' => 'GBP ', + 'JPY' => 'JPY ', + 'CHF' => 'CHF ', + ]; + $currencyUpper = strtoupper((string) $currency); + $symbol = $symbols[$currencyUpper] ?? ($currencyUpper.' '); +@endphp + +merge(['class' => 'tabular-nums']) }}>{{ $symbol }}{{ $amountFormatted }} diff --git a/resources/views/components/storefront/product-card.blade.php b/resources/views/components/storefront/product-card.blade.php new file mode 100644 index 00000000..d026c2f7 --- /dev/null +++ b/resources/views/components/storefront/product-card.blade.php @@ -0,0 +1,21 @@ +@props(['product']) + +@php + $firstVariant = $product->variants?->first(); + $price = $firstVariant?->price_amount ?? null; + $currency = $firstVariant?->currency ?? 'USD'; +@endphp + + +
+
+

{{ $product->title }}

+ @if ($price !== null) +

+ +

+ @else +

View details

+ @endif +
+
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php new file mode 100644 index 00000000..24e8c57d --- /dev/null +++ b/resources/views/errors/404.blade.php @@ -0,0 +1,10 @@ + +
+

404

+

Page not found

+

Sorry, we could not find the page you were looking for.

+ + Back to home + +
+
diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php new file mode 100644 index 00000000..0da5a944 --- /dev/null +++ b/resources/views/errors/503.blade.php @@ -0,0 +1,7 @@ + +
+

503

+

We will be right back

+

Our store is undergoing scheduled maintenance. Please check back in a few minutes.

+
+
diff --git a/resources/views/livewire/admin/analytics/index.blade.php b/resources/views/livewire/admin/analytics/index.blade.php new file mode 100644 index 00000000..67a04913 --- /dev/null +++ b/resources/views/livewire/admin/analytics/index.blade.php @@ -0,0 +1,66 @@ +
+
+ Analytics +
+ + Start date + + + + End date + + +
+
+ +
+
+

Revenue

+

{{ number_format($totals['revenue'] / 100, 2) }} {{ $currency }}

+
+
+

Orders

+

{{ number_format($totals['orders']) }}

+
+
+

AOV

+

{{ number_format($totals['aov'] / 100, 2) }} {{ $currency }}

+
+
+

Visits

+

{{ number_format($totals['visits']) }}

+
+
+ +
+
+ Daily breakdown +
+ @if ($metrics->isEmpty()) +
No data for this range.
+ @else + + + Date + Orders + Revenue + AOV + Visits + Add to cart + + + @foreach ($metrics as $row) + + {{ $row->date?->toDateString() }} + {{ number_format($row->orders_count) }} + {{ number_format($row->revenue_amount / 100, 2) }} {{ $currency }} + {{ number_format($row->aov_amount / 100, 2) }} {{ $currency }} + {{ number_format($row->visits_count) }} + {{ number_format($row->add_to_cart_count) }} + + @endforeach + + + @endif +
+
diff --git a/resources/views/livewire/admin/apps/index.blade.php b/resources/views/livewire/admin/apps/index.blade.php new file mode 100644 index 00000000..1e18aa0f --- /dev/null +++ b/resources/views/livewire/admin/apps/index.blade.php @@ -0,0 +1,53 @@ +
+ Apps + + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ Installed ({{ $installedApps->count() }}) + @if ($installedApps->isEmpty()) +

No apps installed.

+ @else +
+ @foreach ($installedApps as $app) +
+
+ {{ $app->name }} + installed +
+

{{ $app->description }}

+
+ Uninstall +
+
+ @endforeach +
+ @endif +
+ +
+ Marketplace ({{ $marketplaceApps->count() }}) + @if ($marketplaceApps->isEmpty()) +

No apps available in the marketplace.

+ @else +
+ @foreach ($marketplaceApps as $app) +
+
+ {{ $app->name }} + {{ $app->type }} +
+

{{ $app->description }}

+
+ Install +
+
+ @endforeach +
+ @endif +
+
diff --git a/resources/views/livewire/admin/auth/login.blade.php b/resources/views/livewire/admin/auth/login.blade.php new file mode 100644 index 00000000..009d4d6c --- /dev/null +++ b/resources/views/livewire/admin/auth/login.blade.php @@ -0,0 +1,33 @@ +
+
+

{{ __('Admin sign in') }}

+

{{ __('Enter your email and password to access the admin panel') }}

+
+ +
+ + + + + + + + {{ __('Log in') }} + + +
diff --git a/resources/views/livewire/admin/collections/form.blade.php b/resources/views/livewire/admin/collections/form.blade.php new file mode 100644 index 00000000..4d80431d --- /dev/null +++ b/resources/views/livewire/admin/collections/form.blade.php @@ -0,0 +1,89 @@ +
+
+ {{ $mode === 'create' ? 'New collection' : 'Edit collection' }} + Back +
+ +
+
+
+ + Title + + + +
+ + Handle + + + +
+
+ + Description + + +
+
+ +
+ Products +
+ + @if ($searchResults->isNotEmpty()) +
+ @foreach ($searchResults as $product) +
+ {{ $product->title }} + Add +
+ @endforeach +
+ @endif +
+
+ @if ($assignedProducts->isEmpty()) +

No products assigned.

+ @else +
    + @foreach ($assignedProducts as $product) +
  • + {{ $product->title }} + Remove +
  • + @endforeach +
+ @endif +
+
+
+ +
+
+ Settings +
+ + Type + + Manual + Smart + + + + Status + + Draft + Active + + +
+
+
+ +
+ Cancel + Save collection +
+
+
diff --git a/resources/views/livewire/admin/collections/index.blade.php b/resources/views/livewire/admin/collections/index.blade.php new file mode 100644 index 00000000..c49f64a5 --- /dev/null +++ b/resources/views/livewire/admin/collections/index.blade.php @@ -0,0 +1,46 @@ +
+
+ Collections + New collection +
+ + + +
+ @if ($collections->isEmpty()) +
No collections yet.
+ @else + + + Title + Type + Status + Products + + + + @foreach ($collections as $collection) + + + + {{ $collection->title }} + + + {{ $collection->type->value }} + + + {{ $collection->status->value }} + + + {{ $collection->products_count }} + + Edit + + + @endforeach + + +
{{ $collections->links() }}
+ @endif +
+
diff --git a/resources/views/livewire/admin/customers/index.blade.php b/resources/views/livewire/admin/customers/index.blade.php new file mode 100644 index 00000000..e530784c --- /dev/null +++ b/resources/views/livewire/admin/customers/index.blade.php @@ -0,0 +1,37 @@ +
+ Customers + + + +
+ @if ($customers->isEmpty()) +
No customers found.
+ @else + + + Name + Email + Orders + Total spent + Joined + + + @foreach ($customers as $customer) + + + + {{ $customer->name ?? 'Guest' }} + + + {{ $customer->email }} + {{ $customer->orders_count }} + {{ number_format((int) ($customer->total_spent ?? 0) / 100, 2) }} + {{ $customer->created_at?->format('M d, Y') }} + + @endforeach + + +
{{ $customers->links() }}
+ @endif +
+
diff --git a/resources/views/livewire/admin/customers/show.blade.php b/resources/views/livewire/admin/customers/show.blade.php new file mode 100644 index 00000000..65144e7e --- /dev/null +++ b/resources/views/livewire/admin/customers/show.blade.php @@ -0,0 +1,82 @@ +
+
+ {{ $customer->name ?? $customer->email }} + Back +
+ +
+
+
+ Contact +
+
{{ $customer->email }}
+
Member since {{ $customer->created_at?->format('M d, Y') }}
+
+
+ +
+ Addresses + @if ($customer->addresses->isEmpty()) +

No addresses on file.

+ @else +
+ @foreach ($customer->addresses as $address) +
+
{{ $address->first_name }} {{ $address->last_name }}
+
{{ $address->address1 }}, {{ $address->city }}, {{ $address->country_code }}
+
+ @endforeach +
+ @endif +
+ +
+ Recent orders + @if ($customer->orders->isEmpty()) +

No orders yet.

+ @else + + + Order + Total + Status + Date + + + @foreach ($customer->orders as $order) + + + {{ $order->order_number }} + + {{ number_format($order->total_amount / 100, 2) }} + {{ $order->financial_status->value }} + {{ $order->placed_at?->format('M d, Y') }} + + @endforeach + + + @endif +
+
+ +
+
+ Lifetime stats +
+
+
Orders
+
{{ $stats['orders_count'] }}
+
+
+
Total spent
+
{{ number_format($stats['total_spent'] / 100, 2) }}
+
+
+
Average order
+
{{ number_format($stats['average'] / 100, 2) }}
+
+
+
+
+
+
diff --git a/resources/views/livewire/admin/dashboard.blade.php b/resources/views/livewire/admin/dashboard.blade.php new file mode 100644 index 00000000..4669c134 --- /dev/null +++ b/resources/views/livewire/admin/dashboard.blade.php @@ -0,0 +1,80 @@ +
+
+ Dashboard + + Last 7 days + Last 30 days + Last 90 days + +
+ + @php($kpis = $this->kpis) + +
+
+
Total sales
+
+ {{ number_format($kpis['total_sales'] / 100, 2) }} +
+
+
+
Orders
+
+ {{ $kpis['orders_count'] }} +
+
+
+
Average order value
+
+ {{ number_format($kpis['aov'] / 100, 2) }} +
+
+
+
Conversion rate
+
N/A
+
+
+ +
+ Sales over time +
+ Charts coming soon +
+
+ +
+ Recent orders +
+ @if ($this->recentOrders->isEmpty()) +

No orders yet.

+ @else + + + Order + Customer + Total + Status + + + @foreach ($this->recentOrders as $order) + + + + {{ $order->order_number }} + + + {{ $order->email }} + {{ number_format($order->total_amount / 100, 2) }} {{ $order->currency }} + + + {{ $order->financial_status->value }} + + + + @endforeach + + + @endif +
+
+
diff --git a/resources/views/livewire/admin/developers/index.blade.php b/resources/views/livewire/admin/developers/index.blade.php new file mode 100644 index 00000000..e37b44e5 --- /dev/null +++ b/resources/views/livewire/admin/developers/index.blade.php @@ -0,0 +1,107 @@ +
+ Developers + + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ API tokens +

Personal access tokens for the Admin API.

+ +
+ + Token name + + + + Create token +
+ + @if ($plaintextToken !== null) +
+

Copy this token now. It will not be shown again.

+ {{ $plaintextToken }} +
+ @endif + +
+ @if ($tokens->isEmpty()) +

No tokens yet.

+ @else + + + Name + Created + Last used + + + + @foreach ($tokens as $token) + + {{ $token->name }} + {{ $token->created_at?->diffForHumans() }} + {{ $token->last_used_at?->diffForHumans() ?? 'never' }} + + Revoke + + + @endforeach + + + @endif +
+
+ +
+ Webhook subscriptions +

HTTP endpoints notified when events occur.

+ +
+ + Event type + + + + + URL + + + +
+ Add webhook +
+
+ +
+ @if ($webhooks->isEmpty()) +

No webhooks yet.

+ @else + + + Event + URL + Status + + + + @foreach ($webhooks as $webhook) + + {{ $webhook->event_type }} + {{ $webhook->url }} + + {{ $webhook->status }} + + + Delete + + + @endforeach + + + @endif +
+
+
diff --git a/resources/views/livewire/admin/discounts/form.blade.php b/resources/views/livewire/admin/discounts/form.blade.php new file mode 100644 index 00000000..32394f56 --- /dev/null +++ b/resources/views/livewire/admin/discounts/form.blade.php @@ -0,0 +1,93 @@ +
+
+ {{ $mode === 'create' ? 'New discount' : 'Edit discount' }} + Back +
+ +
+
+
+ Details +
+ + Type + + Code + Automatic + + + + @if ($type === 'code') + + Code + + + + @endif + + + Value type + + Percentage + Fixed amount + Free shipping + + + + @if ($valueType !== 'free_shipping') + + Value {{ $valueType === 'percent' ? '(%)' : '(cents)' }} + + + + @endif + + + Minimum purchase (cents) + + +
+
+ +
+ Active dates +
+ + Starts at + + + + Ends at + + +
+
+
+ +
+
+ Status +
+ + Status + + Draft + Active + Disabled + Expired + + + + Usage limit + + +
+
+
+ +
+ Cancel + Save discount +
+
+
diff --git a/resources/views/livewire/admin/discounts/index.blade.php b/resources/views/livewire/admin/discounts/index.blade.php new file mode 100644 index 00000000..79929ce1 --- /dev/null +++ b/resources/views/livewire/admin/discounts/index.blade.php @@ -0,0 +1,69 @@ +
+
+ Discounts + New discount +
+ +
+ + All statuses + Draft + Active + Disabled + Expired + + + All types + Code + Automatic + +
+ +
+ @if ($discounts->isEmpty()) +
No discounts yet.
+ @else + + + Code + Type + Value + Status + Usage + + + + @foreach ($discounts as $discount) + + + + {{ $discount->code ?? 'Automatic' }} + + + {{ $discount->type->value }} + + @if ($discount->value_type->value === 'percent') + {{ $discount->value_amount }}% + @elseif ($discount->value_type->value === 'free_shipping') + Free shipping + @else + {{ number_format($discount->value_amount / 100, 2) }} + @endif + + + + {{ $discount->status->value }} + + + {{ $discount->usage_count }}{{ $discount->usage_limit !== null ? '/'.$discount->usage_limit : '' }} + + Edit + + + @endforeach + + +
{{ $discounts->links() }}
+ @endif +
+
diff --git a/resources/views/livewire/admin/navigation/index.blade.php b/resources/views/livewire/admin/navigation/index.blade.php new file mode 100644 index 00000000..615631de --- /dev/null +++ b/resources/views/livewire/admin/navigation/index.blade.php @@ -0,0 +1,101 @@ +
+
+ Navigation +
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ Create menu +
+ + Title + + + + Add menu +
+
+ +
+ @forelse ($menus as $menu) +
+
+
+ {{ $menu->title }} +

{{ $menu->handle }}

+
+
+ Add item + Delete +
+
+ +
+ @if ($menu->items->isEmpty()) +

No items yet.

+ @else +
    + @foreach ($menu->items as $item) +
  • +
    + {{ $item->label }} + ({{ $item->type->value }}) +
    +
    + + + Remove +
    +
  • + @endforeach +
+ @endif +
+
+ @empty +
+ No menus yet. Create one above. +
+ @endforelse +
+ + +
+ Add menu item + + Type + + Link + Page + Collection + Product + + + + Label + + + + @if ($newItemType === 'link') + + URL + + + @else + + Resource ID + + + @endif +
+ Cancel + Add +
+
+
+
diff --git a/resources/views/livewire/admin/orders/index.blade.php b/resources/views/livewire/admin/orders/index.blade.php new file mode 100644 index 00000000..9c0eb79c --- /dev/null +++ b/resources/views/livewire/admin/orders/index.blade.php @@ -0,0 +1,62 @@ +
+ Orders + +
+ + + All payments + Pending + Paid + Refunded + Partially refunded + + + All fulfillment + Unfulfilled + Partial + Fulfilled + +
+ +
+ @if ($orders->isEmpty()) +
No orders found.
+ @else + + + Order + Customer + Total + Payment + Fulfillment + Date + + + @foreach ($orders as $order) + + + + {{ $order->order_number }} + + + {{ $order->email }} + {{ number_format($order->total_amount / 100, 2) }} {{ $order->currency }} + + + {{ $order->financial_status->value }} + + + + + {{ $order->fulfillment_status->value }} + + + {{ $order->placed_at?->format('M d, Y') }} + + @endforeach + + +
{{ $orders->links() }}
+ @endif +
+
diff --git a/resources/views/livewire/admin/orders/show.blade.php b/resources/views/livewire/admin/orders/show.blade.php new file mode 100644 index 00000000..79ad90bf --- /dev/null +++ b/resources/views/livewire/admin/orders/show.blade.php @@ -0,0 +1,170 @@ +
+
+
+ Order {{ $order->order_number }} +
+ + {{ $order->financial_status->value }} + + + {{ $order->fulfillment_status->value }} + + {{ $order->placed_at?->format('M d, Y H:i') }} +
+
+
+ @if ($order->payment_method->value === 'bank_transfer' && $order->financial_status->value === 'pending') + + Confirm payment + + @endif + @if (in_array($order->financial_status->value, ['paid', 'partially_refunded'], true) && $order->fulfillment_status->value !== 'fulfilled') + + Fulfill items + + @endif + @if (in_array($order->financial_status->value, ['paid', 'partially_refunded'], true) && $order->refundableAmount() > 0) + + Refund + + @endif +
+
+ + @if (session()->has('status')) +
+ {{ session('status') }} +
+ @endif + + @error('order') +
{{ $message }}
+ @enderror + +
+
+
+ Items + + + Product + SKU + Qty + Total + + + @foreach ($order->lines as $line) + + {{ $line->title_snapshot }} + {{ $line->sku_snapshot ?? '-' }} + {{ $line->quantity }} + {{ number_format($line->total_amount / 100, 2) }} {{ $order->currency }} + + @endforeach + + +
+
Subtotal{{ number_format($order->subtotal_amount / 100, 2) }}
+
Shipping{{ number_format($order->shipping_amount / 100, 2) }}
+
Tax{{ number_format($order->tax_amount / 100, 2) }}
+
Total{{ number_format($order->total_amount / 100, 2) }}
+
+
+ +
+ Fulfillments + @if ($order->fulfillments->isEmpty()) +

No fulfillments yet.

+ @else +
+ @foreach ($order->fulfillments as $fulfillment) +
+
+
Fulfillment #{{ $fulfillment->id }} {{ $fulfillment->status }}
+
+ @if ($fulfillment->status === 'pending') + Mark shipped + @endif + @if ($fulfillment->status === 'shipped') + Mark delivered + @endif +
+
+
+ @endforeach +
+ @endif +
+
+ +
+
+ Customer +
+
{{ $order->customer?->name ?? $order->email }}
+
{{ $order->email }}
+
+
+ +
+ Payment +
+
Method: {{ $order->payment_method->value }}
+
Status: {{ $order->financial_status->value }}
+ @if ($order->refundedTotal() > 0) +
Refunded: {{ number_format($order->refundedTotal() / 100, 2) }} {{ $order->currency }}
+ @endif +
+
+
+
+ + +
+ Create fulfillment + @error('fulfill') +
{{ $message }}
+ @enderror +
+ @foreach ($order->lines as $line) + + {{ $line->title_snapshot }} (max {{ $line->quantity }}) + + + @endforeach +
+ + Tracking number + + +
+ Cancel + Create +
+
+
+ + +
+ Refund order + @error('refund') +
{{ $message }}
+ @enderror + + Amount (cents) + + + + Reason + + + + + +
+ Cancel + Refund +
+
+
+
diff --git a/resources/views/livewire/admin/pages/form.blade.php b/resources/views/livewire/admin/pages/form.blade.php new file mode 100644 index 00000000..6ab15ce3 --- /dev/null +++ b/resources/views/livewire/admin/pages/form.blade.php @@ -0,0 +1,57 @@ +
+
+ {{ $mode === 'create' ? 'New page' : 'Edit page' }} + Back +
+ +
+
+
+ + Title + + + +
+ + Handle + + + +
+
+ + Body + + + +
+
+
+ +
+
+ Settings +
+ + Status + + Draft + Published + Archived + + + + Publish date + + +
+
+
+ +
+ Cancel + Save page +
+
+
diff --git a/resources/views/livewire/admin/pages/index.blade.php b/resources/views/livewire/admin/pages/index.blade.php new file mode 100644 index 00000000..14ff3a5c --- /dev/null +++ b/resources/views/livewire/admin/pages/index.blade.php @@ -0,0 +1,55 @@ +
+
+ Pages + New page +
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + + + +
+ @if ($pages->isEmpty()) +
No pages yet.
+ @else + + + Title + Handle + Status + Updated + + + + @foreach ($pages as $page) + + + + {{ $page->title }} + + + {{ $page->handle }} + + + {{ $page->status->value }} + + + {{ $page->updated_at?->diffForHumans() }} + +
+ Edit + Delete +
+
+
+ @endforeach +
+
+
{{ $pages->links() }}
+ @endif +
+
diff --git a/resources/views/livewire/admin/products/form.blade.php b/resources/views/livewire/admin/products/form.blade.php new file mode 100644 index 00000000..a2fdb0ff --- /dev/null +++ b/resources/views/livewire/admin/products/form.blade.php @@ -0,0 +1,98 @@ +
+
+ {{ $mode === 'create' ? 'New product' : 'Edit product' }} + Back +
+ +
+
+
+ + Title + + + + +
+ + Handle + + + +
+ +
+ + Description + + + +
+
+ +
+ Pricing & Inventory +
+ + Price (cents) + + + + + SKU + + + + + Inventory + + + +
+
+
+ +
+
+ Status +
+ + Status + + @foreach ($statuses as $statusOption) + {{ ucfirst($statusOption->value) }} + @endforeach + + + +
+
+ +
+ Organization +
+ + Vendor + + + + + Product type + + + + + Tags (comma separated) + + + +
+
+
+ +
+ Cancel + Save product +
+
+
diff --git a/resources/views/livewire/admin/products/index.blade.php b/resources/views/livewire/admin/products/index.blade.php new file mode 100644 index 00000000..201c9626 --- /dev/null +++ b/resources/views/livewire/admin/products/index.blade.php @@ -0,0 +1,67 @@ +
+
+ Products + New product +
+ +
+ + + All statuses + Draft + Active + Archived + +
+ + @if (count($selectedIds) > 0) +
+ {{ count($selectedIds) }} selected + Archive + Delete drafts +
+ @endif + +
+ @if ($products->isEmpty()) +
+ No products match your filters. +
+ @else + + + Title + Status + Vendor + Variants + + + + @foreach ($products as $product) + + + + {{ $product->title }} + + + + + {{ $product->status->value }} + + + {{ $product->vendor ?? '-' }} + {{ $product->variants_count }} + + Edit + + + @endforeach + + + +
+ {{ $products->links() }} +
+ @endif +
+
diff --git a/resources/views/livewire/admin/settings/index.blade.php b/resources/views/livewire/admin/settings/index.blade.php new file mode 100644 index 00000000..612b6771 --- /dev/null +++ b/resources/views/livewire/admin/settings/index.blade.php @@ -0,0 +1,48 @@ +
+ Settings + + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ General + Shipping + Taxes +
+ +
+ + Store name + + + +
+ + Currency + + + + + Locale + + + + + Timezone + + + +
+
+ Save settings +
+
+ +
+ Notifications +

Notification channel configuration is coming soon.

+
+
diff --git a/resources/views/livewire/admin/settings/shipping.blade.php b/resources/views/livewire/admin/settings/shipping.blade.php new file mode 100644 index 00000000..f3839cf7 --- /dev/null +++ b/resources/views/livewire/admin/settings/shipping.blade.php @@ -0,0 +1,113 @@ +
+
+ Shipping +
+ Back + New zone +
+
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ @forelse ($zones as $zone) +
+
+
+ {{ $zone->name }} +
+ @foreach (($zone->countries_json ?? []) as $country) + {{ $country }} + @endforeach +
+
+
+ Add rate + Delete +
+
+ +
+ @if ($zone->rates->isEmpty()) +

No rates yet.

+ @else + + + Name + Type + Amount + + + + @foreach ($zone->rates as $rate) + + {{ $rate->name }} + {{ $rate->type->value }} + {{ number_format(($rate->config_json['amount'] ?? 0) / 100, 2) }} + + Remove + + + @endforeach + + + @endif +
+
+ @empty +
+ No shipping zones configured. +
+ @endforelse +
+ + +
+ New shipping zone + + Name + + + + + Countries (comma separated ISO codes) + + +
+ Cancel + Create +
+
+
+ + +
+ New shipping rate + + Name + + + + + Type + + Flat + Weight + Price + + + + Amount (in cents) + + +
+ Cancel + Create +
+
+
+
diff --git a/resources/views/livewire/admin/settings/taxes.blade.php b/resources/views/livewire/admin/settings/taxes.blade.php new file mode 100644 index 00000000..ab7aa0a1 --- /dev/null +++ b/resources/views/livewire/admin/settings/taxes.blade.php @@ -0,0 +1,44 @@ +
+
+ Taxes + Back +
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ + Mode + + Manual + Provider + + + + + + Tax name + + + + + Rate (basis points) + + 1900 = 19%. Basis points are 1/100 of a percent. + + + + + + Prices include tax + + +
+ Save taxes +
+
+
diff --git a/resources/views/livewire/admin/themes/index.blade.php b/resources/views/livewire/admin/themes/index.blade.php new file mode 100644 index 00000000..d818038c --- /dev/null +++ b/resources/views/livewire/admin/themes/index.blade.php @@ -0,0 +1,48 @@ +
+
+ Themes +
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + + @if ($themes->isEmpty()) +
+ No themes installed. +
+ @else +
+ @foreach ($themes as $theme) +
$theme->status->value === 'published', + 'border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900' => $theme->status->value !== 'published', + ])> +
+ {{ $theme->name }} + + {{ $theme->status->value }} + +
+

Version {{ $theme->version ?? '1.0' }}

+ @if ($theme->published_at !== null) +

Published {{ $theme->published_at->diffForHumans() }}

+ @endif + +
+ @if ($theme->status->value !== 'published') + Publish + @endif + Duplicate + @if ($theme->status->value !== 'published') + Delete + @endif +
+
+ @endforeach +
+ @endif +
diff --git a/resources/views/livewire/storefront/account/addresses/index.blade.php b/resources/views/livewire/storefront/account/addresses/index.blade.php new file mode 100644 index 00000000..24c1377d --- /dev/null +++ b/resources/views/livewire/storefront/account/addresses/index.blade.php @@ -0,0 +1,106 @@ +
+
+ Back to account +

Addresses

+
+ +
+

Saved addresses

+ + @if ($addresses->isEmpty()) +
+

No addresses saved yet.

+
+ @else +
+ @foreach ($addresses as $address) + @php($data = $address->address_json ?? []) +
+
+

{{ $address->label }}

+ @if ($address->is_default) + Default + @endif +
+
+ {{ trim(($data['first_name'] ?? '').' '.($data['last_name'] ?? '')) }}
+ @if (! empty($data['address1'])) {{ $data['address1'] }}
@endif + @if (! empty($data['city'])) {{ $data['city'] }} {{ $data['postal_code'] ?? '' }}
@endif + {{ $data['country'] ?? '' }} +
+
+ @unless ($address->is_default) + + @endunless + +
+
+ @endforeach +
+ @endif +
+ +
+

Add a new address

+ +
+
+ + + + + + + + +
+ + + + +
+
+
diff --git a/resources/views/livewire/storefront/account/auth/login.blade.php b/resources/views/livewire/storefront/account/auth/login.blade.php new file mode 100644 index 00000000..6323026d --- /dev/null +++ b/resources/views/livewire/storefront/account/auth/login.blade.php @@ -0,0 +1,34 @@ +
+
+

Sign in

+

Access your orders and saved addresses.

+
+ +
+ + + + + + + +
+ +

+ New customer? + Create an account +

+
diff --git a/resources/views/livewire/storefront/account/auth/register.blade.php b/resources/views/livewire/storefront/account/auth/register.blade.php new file mode 100644 index 00000000..8c40442f --- /dev/null +++ b/resources/views/livewire/storefront/account/auth/register.blade.php @@ -0,0 +1,45 @@ +
+
+

Create account

+

Sign up to track orders and check out faster.

+
+ +
+ + + + + + + + + + + +
+ +

+ Already have an account? + Sign in +

+
diff --git a/resources/views/livewire/storefront/account/dashboard.blade.php b/resources/views/livewire/storefront/account/dashboard.blade.php new file mode 100644 index 00000000..41e8d46b --- /dev/null +++ b/resources/views/livewire/storefront/account/dashboard.blade.php @@ -0,0 +1,59 @@ +
+
+
+

Account

+

Hi, {{ $customer->name }}

+

{{ $customer->email }}

+
+
+ @csrf + +
+
+ + + +
+

Recent orders

+ + @if ($recentOrders->isEmpty()) +
+

You have not placed any orders yet.

+
+ @else +
    + @foreach ($recentOrders as $order) +
  • +
    +

    {{ $order->order_number }}

    +

    {{ $order->placed_at?->format('M j, Y') }}

    +
    +
    + + + + + View + +
    +
  • + @endforeach +
+ @endif +
+
diff --git a/resources/views/livewire/storefront/account/orders/index.blade.php b/resources/views/livewire/storefront/account/orders/index.blade.php new file mode 100644 index 00000000..f3484291 --- /dev/null +++ b/resources/views/livewire/storefront/account/orders/index.blade.php @@ -0,0 +1,43 @@ +
+
+ Back to account +

Orders

+
+ + @if ($orders->isEmpty()) +
+

You have not placed any orders yet.

+
+ @else +
+ + + + + + + + + + + + @foreach ($orders as $order) + + + + + + + + @endforeach + +
OrderDateTotalStatus
{{ $order->order_number }}{{ $order->placed_at?->format('M j, Y') }}{{ $order->status?->value ?? 'pending' }} + View +
+
+ +
+ {{ $orders->links() }} +
+ @endif +
diff --git a/resources/views/livewire/storefront/account/orders/show.blade.php b/resources/views/livewire/storefront/account/orders/show.blade.php new file mode 100644 index 00000000..5751f2bb --- /dev/null +++ b/resources/views/livewire/storefront/account/orders/show.blade.php @@ -0,0 +1,63 @@ +
+
+ Back to orders +

Order {{ $order->order_number }}

+

Placed on {{ $order->placed_at?->format('F j, Y') }}

+
+ +
+
+

Items

+
    + @foreach ($order->lines as $line) +
  • +
    +
    +
    +

    {{ $line->title_snapshot }}

    +

    Qty {{ $line->quantity }}

    +
    +
    +

    + +

    +
  • + @endforeach +
+ +
+
+ Subtotal + +
+
+ Shipping + +
+
+ Total + +
+
+
+ + +
+
diff --git a/resources/views/livewire/storefront/cart-drawer.blade.php b/resources/views/livewire/storefront/cart-drawer.blade.php new file mode 100644 index 00000000..1ebc8c18 --- /dev/null +++ b/resources/views/livewire/storefront/cart-drawer.blade.php @@ -0,0 +1,97 @@ +
+ + +
+ + +
diff --git a/resources/views/livewire/storefront/cart/show.blade.php b/resources/views/livewire/storefront/cart/show.blade.php new file mode 100644 index 00000000..bd923368 --- /dev/null +++ b/resources/views/livewire/storefront/cart/show.blade.php @@ -0,0 +1,106 @@ +
+
+

Your cart

+

Review the items you are about to purchase.

+
+ + @if ($cart === null || $cart->lines->isEmpty()) +
+

Your cart is empty.

+ + Browse collections + +
+ @else +
+
+
    + @foreach ($cart->lines as $line) +
  • +
    +
    +
    +
    +

    + {{ $line->variant?->product?->title ?? 'Product' }} +

    +

    {{ $line->variant?->sku }}

    +
    +

    + +

    +
    + +
    +
    + + {{ $line->quantity }} + +
    + + +
    +
    +
  • + @endforeach +
+ + @error('cart') +
+ {{ $message }} +
+ @enderror +
+ + +
+ @endif +
diff --git a/resources/views/livewire/storefront/checkout/confirmation.blade.php b/resources/views/livewire/storefront/checkout/confirmation.blade.php new file mode 100644 index 00000000..f90561be --- /dev/null +++ b/resources/views/livewire/storefront/checkout/confirmation.blade.php @@ -0,0 +1,83 @@ +
+
+
+ +
+

Thank you for your order

+

Order {{ $order->order_number }} is confirmed. A receipt has been sent to {{ $order->email }}.

+
+ +
+
+

Order details

+
    + @foreach ($order->lines as $line) +
  • +
    +
    +
    +

    {{ $line->title_snapshot }}

    +

    Qty {{ $line->quantity }}

    +
    +
    +

    + +

    +
  • + @endforeach +
+ +
+
+ Subtotal + +
+
+ Shipping + +
+ @if ($order->tax_amount > 0) +
+ Tax + +
+ @endif +
+ Total + +
+
+
+ + +
+
diff --git a/resources/views/livewire/storefront/checkout/show.blade.php b/resources/views/livewire/storefront/checkout/show.blade.php new file mode 100644 index 00000000..0d910310 --- /dev/null +++ b/resources/views/livewire/storefront/checkout/show.blade.php @@ -0,0 +1,222 @@ +
+
+

Checkout

+
+ +
    + @foreach ([1 => 'Address', 2 => 'Shipping', 3 => 'Payment'] as $stepNumber => $label) +
  1. + + {{ $stepNumber }} + + {{ $label }} + @if ($stepNumber < 3) + + @endif +
  2. + @endforeach +
+ +
+
+ @if ($step === 1) +
+

Contact and shipping address

+ +
+ + +
+ + +
+ + + + + +
+ + +
+ + + + +
+ + +
+ @elseif ($step === 2) +
+

Shipping method

+ + @if ($shippingRates->isEmpty()) +

+ No shipping options available for the selected country. +

+ @else +
+ @foreach ($shippingRates as $rate) + + @endforeach +
+ @error('shippingMethodId') {{ $message }} @enderror + @endif + +
+ + +
+
+ @else +
+

Payment

+ +
+ + + +
+ + @if ($paymentMethod === 'credit_card') +
+ Magic test card: 4242 4242 4242 4242, expiry 12/30, CVC 123. +
+ + +
+ + +
+ @endif + + @error('payment') +
+ {{ $message }} +
+ @enderror + +
+ + +
+
+ @endif +
+ + +
+
diff --git a/resources/views/livewire/storefront/collections/index.blade.php b/resources/views/livewire/storefront/collections/index.blade.php new file mode 100644 index 00000000..2d6d7576 --- /dev/null +++ b/resources/views/livewire/storefront/collections/index.blade.php @@ -0,0 +1,35 @@ +
+
+

Shop

+

All collections

+

Browse every curated edit across the store.

+
+ + @if ($collections->isEmpty()) +
+

No collections yet.

+
+ @else + + @endif +
diff --git a/resources/views/livewire/storefront/collections/show.blade.php b/resources/views/livewire/storefront/collections/show.blade.php new file mode 100644 index 00000000..48691a11 --- /dev/null +++ b/resources/views/livewire/storefront/collections/show.blade.php @@ -0,0 +1,48 @@ +
+
+ +

{{ $collection->title }}

+ @if ($collection->description_html) +
+ {!! $collection->description_html !!} +
+ @endif +
+ +
+

+ {{ $products->total() }} {{ \Illuminate\Support\Str::plural('product', $products->total()) }} +

+ +
+ + @if ($products->isEmpty()) +
+

No products in this collection yet.

+
+ @else +
+ @foreach ($products as $product) + + @endforeach +
+ +
+ {{ $products->links() }} +
+ @endif +
diff --git a/resources/views/livewire/storefront/home.blade.php b/resources/views/livewire/storefront/home.blade.php new file mode 100644 index 00000000..2071764c --- /dev/null +++ b/resources/views/livewire/storefront/home.blade.php @@ -0,0 +1,62 @@ +
+
+

New season

+

+ Thoughtfully made, honestly priced. +

+

+ A curated collection of timeless goods designed to last. Explore our latest arrivals and find something you will love. +

+ +
+ + @if ($featuredCollections->isNotEmpty()) + + @endif + +
+
+
+

New arrivals

+

Fresh goods, just in.

+
+
+ + @if ($recentProducts->isNotEmpty()) +
+ @foreach ($recentProducts as $product) + + @endforeach +
+ @else +
+

No products yet. Check back soon.

+
+ @endif +
+
diff --git a/resources/views/livewire/storefront/pages/show.blade.php b/resources/views/livewire/storefront/pages/show.blade.php new file mode 100644 index 00000000..772543b8 --- /dev/null +++ b/resources/views/livewire/storefront/pages/show.blade.php @@ -0,0 +1,9 @@ +
+
+

{{ $page->title }}

+
+ +
+ {!! $page->body_html !!} +
+
diff --git a/resources/views/livewire/storefront/products/show.blade.php b/resources/views/livewire/storefront/products/show.blade.php new file mode 100644 index 00000000..4ea2b77b --- /dev/null +++ b/resources/views/livewire/storefront/products/show.blade.php @@ -0,0 +1,82 @@ +
+
+
+
+ @if ($product->media && $product->media->count() > 1) +
+ @foreach ($product->media->take(4) as $media) +
+ @endforeach +
+ @endif +
+ +
+
+ @if ($product->vendor) +

{{ $product->vendor }}

+ @endif +

{{ $product->title }}

+ @if ($selectedVariant) +

+ +

+ @endif +
+ + @if ($activeVariants->count() > 1) +
+ Variant +
+ @foreach ($activeVariants as $variant) + + @endforeach +
+
+ @endif + +
+ +
+ + {{ $quantity }} + +
+
+ + @if (session('cart-success')) +
+ {{ session('cart-success') }} +
+ @endif + + @error('cart') +
+ {{ $message }} +
+ @enderror + + + + @if ($product->description_html) +
+ {!! $product->description_html !!} +
+ @endif +
+
+
diff --git a/resources/views/livewire/storefront/search/index.blade.php b/resources/views/livewire/storefront/search/index.blade.php new file mode 100644 index 00000000..991a2cf6 --- /dev/null +++ b/resources/views/livewire/storefront/search/index.blade.php @@ -0,0 +1,29 @@ +
+
+

Search

+
+ +
+
+ + @if (trim($q) === '') +

Start typing to search the catalogue.

+ @elseif ($products->isEmpty()) +

No products found for "{{ $q }}".

+ @else +
+ @foreach ($products as $product) + + @endforeach +
+ +
+ {{ $products->links() }} +
+ @endif +
diff --git a/resources/views/storefront/partials/announcement.blade.php b/resources/views/storefront/partials/announcement.blade.php new file mode 100644 index 00000000..d258c3d4 --- /dev/null +++ b/resources/views/storefront/partials/announcement.blade.php @@ -0,0 +1,7 @@ +@if (! empty($announcement)) +
+
+ {{ $announcement }} +
+
+@endif diff --git a/resources/views/storefront/partials/footer.blade.php b/resources/views/storefront/partials/footer.blade.php new file mode 100644 index 00000000..1beb2e34 --- /dev/null +++ b/resources/views/storefront/partials/footer.blade.php @@ -0,0 +1,28 @@ +@php + $footerMenu = null; + if (app()->bound('current_store')) { + $store = app('current_store'); + $footerMenu = \App\Models\NavigationMenu::withoutGlobalScopes() + ->where('store_id', $store->id) + ->where('handle', 'footer-menu') + ->with(['items' => fn ($q) => $q->orderBy('position')]) + ->first(); + } + $footerItems = $footerMenu + ? app(\App\Services\NavigationService::class)->buildTree($footerMenu) + : []; +@endphp + diff --git a/resources/views/storefront/partials/header.blade.php b/resources/views/storefront/partials/header.blade.php new file mode 100644 index 00000000..e5568be7 --- /dev/null +++ b/resources/views/storefront/partials/header.blade.php @@ -0,0 +1,46 @@ +@php + $mainMenu = null; + if (isset($store) && $store) { + $mainMenu = \App\Models\NavigationMenu::withoutGlobalScopes() + ->where('store_id', $store->id) + ->where('handle', 'main-menu') + ->with(['items' => fn ($q) => $q->orderBy('position')]) + ->first(); + } + $navItems = $mainMenu + ? app(\App\Services\NavigationService::class)->buildTree($mainMenu) + : []; +@endphp + +
+
+ + {{ $store->name ?? config('app.name') }} + + + + + +
+
diff --git a/review-01-storefront-home.png b/review-01-storefront-home.png new file mode 100644 index 00000000..56f40ab4 Binary files /dev/null and b/review-01-storefront-home.png differ diff --git a/review-02-product-detail.png b/review-02-product-detail.png new file mode 100644 index 00000000..da63df0d Binary files /dev/null and b/review-02-product-detail.png differ diff --git a/review-03-admin-dashboard.png b/review-03-admin-dashboard.png new file mode 100644 index 00000000..0b132d9f Binary files /dev/null and b/review-03-admin-dashboard.png differ diff --git a/review-04-admin-products.png b/review-04-admin-products.png new file mode 100644 index 00000000..7040090c Binary files /dev/null and b/review-04-admin-products.png differ diff --git a/review-05-admin-order-detail.png b/review-05-admin-order-detail.png new file mode 100644 index 00000000..a06bda0f Binary files /dev/null and b/review-05-admin-order-detail.png differ diff --git a/review-06-admin-shipping.png b/review-06-admin-shipping.png new file mode 100644 index 00000000..c6f50872 Binary files /dev/null and b/review-06-admin-shipping.png differ diff --git a/review-07-order-confirmation.png b/review-07-order-confirmation.png new file mode 100644 index 00000000..d0aab3f2 Binary files /dev/null and b/review-07-order-confirmation.png differ diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 00000000..86f193a5 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,14 @@ +prefix('admin')->group(function (): void { + Route::get('/user', fn (Request $request) => $request->user()); + + Route::get('/products', fn () => Product::query()->paginate()); + + Route::get('/orders', fn () => Order::query()->paginate()); +}); diff --git a/routes/console.php b/routes/console.php index 3c9adf1a..bb9603fc 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,8 +1,18 @@ comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +Schedule::job(new ExpireAbandonedCheckouts)->everyFifteenMinutes(); +Schedule::job(new CleanupAbandonedCarts)->dailyAt('03:00'); +Schedule::job(new CancelUnpaidBankTransferOrders)->dailyAt('04:00'); +Schedule::job(new AggregateAnalytics)->dailyAt('02:00'); diff --git a/routes/web.php b/routes/web.php index f755f111..f53f4fee 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,13 +1,111 @@ name('home'); +Route::get('/', StorefrontHome::class)->name('home'); +Route::get('/storefront', StorefrontHome::class)->name('storefront.home'); -Route::view('dashboard', 'dashboard') - ->middleware(['auth', 'verified']) - ->name('dashboard'); +Route::get('/collections', CollectionsIndex::class)->name('storefront.collections.index'); +Route::get('/collections/{handle}', CollectionsShow::class)->name('storefront.collections.show'); +Route::get('/products/{handle}', ProductsShow::class)->name('storefront.products.show'); +Route::get('/cart', CartShow::class)->name('storefront.cart.show'); +Route::get('/checkout', CheckoutShow::class)->name('storefront.checkout.show'); +Route::get('/checkout/confirmation/{order_number}', CheckoutConfirmation::class)->name('storefront.checkout.confirmation'); +Route::get('/pages/{handle}', PagesShow::class)->name('storefront.pages.show'); +Route::get('/search', SearchIndex::class)->name('storefront.search'); + +Route::get('/account/login', AccountLogin::class)->name('storefront.account.login'); +Route::get('/account/register', AccountRegister::class)->name('storefront.account.register'); + +Route::post('/account/logout', function (Request $request) { + Auth::guard('customer')->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('storefront.account.login'); +})->name('storefront.account.logout'); + +Route::middleware('auth:customer') + ->prefix('account') + ->name('storefront.account.') + ->group(function (): void { + Route::get('/', AccountDashboard::class)->name('dashboard'); + Route::get('/orders', AccountOrdersIndex::class)->name('orders.index'); + Route::get('/orders/{orderNumber}', AccountOrdersShow::class)->name('orders.show'); + Route::get('/addresses', AccountAddressesIndex::class)->name('addresses.index'); + }); + +Route::redirect('dashboard', '/admin')->name('dashboard'); + +Route::get('/admin/login', AdminLogin::class) + ->middleware('guest') + ->name('admin.login'); + +Route::post('/admin/logout', function (Request $request) { + Auth::guard('web')->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('admin.login'); +})->middleware('auth')->name('admin.logout'); + +Route::prefix('admin') + ->middleware(['auth', 'store.resolve:admin']) + ->name('admin.') + ->group(function (): void { + Route::get('/', \App\Livewire\Admin\Dashboard::class)->name('dashboard'); + + Route::get('/products', \App\Livewire\Admin\Products\Index::class)->name('products.index'); + Route::get('/products/create', \App\Livewire\Admin\Products\Form::class)->name('products.create'); + Route::get('/products/{product}/edit', \App\Livewire\Admin\Products\Form::class)->name('products.edit'); + + Route::get('/orders', \App\Livewire\Admin\Orders\Index::class)->name('orders.index'); + Route::get('/orders/{order}', \App\Livewire\Admin\Orders\Show::class)->name('orders.show'); + + Route::get('/customers', \App\Livewire\Admin\Customers\Index::class)->name('customers.index'); + Route::get('/customers/{customer}', \App\Livewire\Admin\Customers\Show::class)->name('customers.show'); + + Route::get('/collections', \App\Livewire\Admin\Collections\Index::class)->name('collections.index'); + Route::get('/collections/create', \App\Livewire\Admin\Collections\Form::class)->name('collections.create'); + Route::get('/collections/{collection}/edit', \App\Livewire\Admin\Collections\Form::class)->name('collections.edit'); + + Route::get('/discounts', \App\Livewire\Admin\Discounts\Index::class)->name('discounts.index'); + Route::get('/discounts/create', \App\Livewire\Admin\Discounts\Form::class)->name('discounts.create'); + Route::get('/discounts/{discount}/edit', \App\Livewire\Admin\Discounts\Form::class)->name('discounts.edit'); + + Route::get('/pages', \App\Livewire\Admin\Pages\Index::class)->name('pages.index'); + Route::get('/pages/create', \App\Livewire\Admin\Pages\Form::class)->name('pages.create'); + Route::get('/pages/{page}/edit', \App\Livewire\Admin\Pages\Form::class)->name('pages.edit'); + + Route::get('/navigation', \App\Livewire\Admin\Navigation\Index::class)->name('navigation.index'); + Route::get('/themes', \App\Livewire\Admin\Themes\Index::class)->name('themes.index'); + + Route::get('/analytics', \App\Livewire\Admin\Analytics\Index::class)->name('analytics.index'); + + Route::get('/settings', \App\Livewire\Admin\Settings\Index::class)->name('settings.index'); + Route::get('/settings/shipping', \App\Livewire\Admin\Settings\Shipping::class)->name('settings.shipping'); + Route::get('/settings/taxes', \App\Livewire\Admin\Settings\Taxes::class)->name('settings.taxes'); + + Route::get('/apps', \App\Livewire\Admin\Apps\Index::class)->name('apps.index'); + Route::get('/developers', \App\Livewire\Admin\Developers\Index::class)->name('developers.index'); + }); require __DIR__.'/settings.php'; diff --git a/specs/progress.md b/specs/progress.md new file mode 100644 index 00000000..f2d833d7 --- /dev/null +++ b/specs/progress.md @@ -0,0 +1,23 @@ +# Shop Build Progress + +Tracking progress for the full shop system implementation (team: shop-v2). + +## Phase Checklist + +- [x] Phase 1: Foundation (migrations, models, middleware, auth, authorization) - 42 tests passing +- [x] Phase 2: Catalog (products, variants, inventory, collections, media) - 63 tests passing +- [x] Phase 3: Themes, pages, navigation, storefront layout - 79 tests passing +- [x] Phase 4: Cart, checkout, discounts, shipping, taxes - 138 tests passing +- [x] Phase 5: Payments, orders, fulfillment - 167 tests passing +- [x] Phase 6: Customer accounts + storefront UI - 192 tests passing +- [x] Phase 7a: Admin panel core - 212 tests passing +- [x] Phase 7b: Admin panel (settings, themes, pages, navigation, analytics, apps, developers) - 247 tests passing +- [~] Phase 8: Search FTS5 backend + storefront wired - 234 tests; admin search settings UI in 7b +- [~] Phase 9: Analytics events + daily aggregator backend; admin analytics UI in 7b +- [~] Phase 10: Apps and webhooks backend (Sanctum, WebhookService, DeliverWebhook job); admin apps/developers UI in 7b +- [~] Phase 11: Polish - seeders done; dark mode classes applied throughout; deeper accessibility audit pending +- [x] Phase 12: Full test suite + browser review - 247 tests passing, all key flows verified end-to-end via Playwright + +## Log + +- Starting Phase 1: Foundation diff --git a/tests/Feature/Admin/AnalyticsAdminTest.php b/tests/Feature/Admin/AnalyticsAdminTest.php new file mode 100644 index 00000000..658a7a38 --- /dev/null +++ b/tests/Feature/Admin/AnalyticsAdminTest.php @@ -0,0 +1,33 @@ +forgetInstance('current_store'); +}); + +it('renders analytics for a date range', function (): void { + [$user, $store] = loginAsAdmin(); + + AnalyticsDaily::create([ + 'store_id' => $store->id, + 'date' => now()->subDays(1)->toDateString(), + 'orders_count' => 5, + 'revenue_amount' => 25000, + 'aov_amount' => 5000, + 'visits_count' => 200, + 'add_to_cart_count' => 30, + 'checkout_started_count' => 10, + 'checkout_completed_count' => 5, + ]); + + Livewire::test(AnalyticsIndex::class) + ->assertSet('endDate', now()->toDateString()) + ->assertSee('Analytics') + ->assertSee('Daily breakdown'); +}); diff --git a/tests/Feature/Admin/AppsTest.php b/tests/Feature/Admin/AppsTest.php new file mode 100644 index 00000000..8841854b --- /dev/null +++ b/tests/Feature/Admin/AppsTest.php @@ -0,0 +1,60 @@ +forgetInstance('current_store'); +}); + +it('installs an app', function (): void { + [$user, $store] = loginAsAdmin(); + + $app = App::create([ + 'name' => 'Analytics Pro', + 'slug' => 'analytics-pro', + 'description' => 'Advanced analytics', + 'type' => 'first_party', + ]); + + Livewire::test(AppsIndex::class) + ->call('install', $app->id); + + $installation = AppInstallation::where('store_id', $store->id) + ->where('app_id', $app->id) + ->first(); + + expect($installation)->not->toBeNull() + ->and($installation->status)->toBe('active'); +}); + +it('uninstalls an app', function (): void { + [$user, $store] = loginAsAdmin(); + + $app = App::create([ + 'name' => 'Demo App', + 'slug' => 'demo-app', + 'type' => 'first_party', + ]); + + AppInstallation::create([ + 'store_id' => $store->id, + 'app_id' => $app->id, + 'status' => 'active', + 'installed_at' => now(), + ]); + + Livewire::test(AppsIndex::class) + ->call('uninstall', $app->id); + + $installation = AppInstallation::where('store_id', $store->id) + ->where('app_id', $app->id) + ->first(); + + expect($installation->status)->toBe('uninstalled'); +}); diff --git a/tests/Feature/Admin/CollectionManagementTest.php b/tests/Feature/Admin/CollectionManagementTest.php new file mode 100644 index 00000000..e2cff740 --- /dev/null +++ b/tests/Feature/Admin/CollectionManagementTest.php @@ -0,0 +1,42 @@ +forgetInstance('current_store'); +}); + +it('creates a collection', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(CollectionForm::class) + ->set('title', 'Summer 2026') + ->set('status', 'active') + ->call('save') + ->assertRedirect(route('admin.collections.index')); + + $collection = Collection::where('title', 'Summer 2026')->first(); + expect($collection)->not->toBeNull() + ->and($collection->store_id)->toBe($store->id); +}); + +it('adds products to a collection', function (): void { + [$user, $store] = loginAsAdmin(); + + $product = Product::factory()->create(['store_id' => $store->id]); + + Livewire::test(CollectionForm::class) + ->set('title', 'Featured') + ->set('productIds', [$product->id]) + ->call('save') + ->assertRedirect(route('admin.collections.index')); + + $collection = Collection::where('title', 'Featured')->first(); + expect($collection->products()->count())->toBe(1); +}); diff --git a/tests/Feature/Admin/CustomerManagementTest.php b/tests/Feature/Admin/CustomerManagementTest.php new file mode 100644 index 00000000..7953525b --- /dev/null +++ b/tests/Feature/Admin/CustomerManagementTest.php @@ -0,0 +1,39 @@ +forgetInstance('current_store'); +}); + +it('lists customers for the current store', function (): void { + [$user, $store] = loginAsAdmin(); + + Customer::factory()->create([ + 'store_id' => $store->id, + 'email' => 'one@example.com', + ]); + + Livewire::test(CustomersIndex::class) + ->assertSee('one@example.com'); +}); + +it('shows customer detail', function (): void { + [$user, $store] = loginAsAdmin(); + + $customer = Customer::factory()->create([ + 'store_id' => $store->id, + 'name' => 'Jane Doe', + 'email' => 'jane@example.com', + ]); + + Livewire::test(CustomersShow::class, ['customer' => $customer]) + ->assertSee('Jane Doe') + ->assertSee('jane@example.com'); +}); diff --git a/tests/Feature/Admin/DashboardTest.php b/tests/Feature/Admin/DashboardTest.php new file mode 100644 index 00000000..f55a072a --- /dev/null +++ b/tests/Feature/Admin/DashboardTest.php @@ -0,0 +1,44 @@ +forgetInstance('current_store'); +}); + +it('redirects guests away from the admin dashboard', function (): void { + $this->get('/admin')->assertRedirect(); +}); + +it('renders the admin dashboard for an authenticated admin', function (): void { + loginAsAdmin(); + + $this->get('/admin') + ->assertOk() + ->assertSeeLivewire(Dashboard::class) + ->assertSee('Dashboard'); +}); + +it('computes KPIs from recent orders', function (): void { + [$user, $store] = loginAsAdmin(); + + Order::factory()->count(3)->create([ + 'store_id' => $store->id, + 'total_amount' => 5000, + 'placed_at' => now()->subDays(2), + ]); + + $component = Livewire::test(Dashboard::class) + ->assertSet('period', '30d'); + + $kpis = $component->instance()->kpis(); + + expect($kpis['total_sales'])->toBe(15000) + ->and($kpis['orders_count'])->toBe(3) + ->and($kpis['aov'])->toBe(5000); +}); diff --git a/tests/Feature/Admin/DevelopersTest.php b/tests/Feature/Admin/DevelopersTest.php new file mode 100644 index 00000000..9c3855a9 --- /dev/null +++ b/tests/Feature/Admin/DevelopersTest.php @@ -0,0 +1,38 @@ +forgetInstance('current_store'); +}); + +it('creates an API token', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(DevelopersIndex::class) + ->set('newTokenName', 'CI integration') + ->call('createToken') + ->assertSet('newTokenName', ''); + + expect($user->fresh()->tokens()->count())->toBe(1) + ->and($user->fresh()->tokens()->first()->name)->toBe('CI integration'); +}); + +it('creates a webhook subscription', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(DevelopersIndex::class) + ->set('webhookEventType', 'order.placed') + ->set('webhookUrl', 'https://example.com/webhook') + ->call('createWebhook'); + + $webhook = WebhookSubscription::where('event_type', 'order.placed')->first(); + expect($webhook)->not->toBeNull() + ->and($webhook->store_id)->toBe($store->id) + ->and($webhook->url)->toBe('https://example.com/webhook'); +}); diff --git a/tests/Feature/Admin/DiscountManagementTest.php b/tests/Feature/Admin/DiscountManagementTest.php new file mode 100644 index 00000000..b8a37295 --- /dev/null +++ b/tests/Feature/Admin/DiscountManagementTest.php @@ -0,0 +1,63 @@ +forgetInstance('current_store'); +}); + +it('creates a percent discount', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(DiscountForm::class) + ->set('type', 'code') + ->set('code', 'SAVE10') + ->set('valueType', 'percent') + ->set('valueAmount', 10) + ->set('status', 'active') + ->call('save') + ->assertRedirect(route('admin.discounts.index')); + + $discount = Discount::where('code', 'SAVE10')->first(); + expect($discount)->not->toBeNull() + ->and($discount->value_type->value)->toBe('percent') + ->and($discount->value_amount)->toBe(10); +}); + +it('creates a free shipping discount', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(DiscountForm::class) + ->set('type', 'code') + ->set('code', 'FREESHIP') + ->set('valueType', 'free_shipping') + ->set('valueAmount', 0) + ->set('status', 'active') + ->call('save') + ->assertRedirect(route('admin.discounts.index')); + + $discount = Discount::where('code', 'FREESHIP')->first(); + expect($discount)->not->toBeNull() + ->and($discount->value_type->value)->toBe('free_shipping'); +}); + +it('disables a discount', function (): void { + [$user, $store] = loginAsAdmin(); + + $discount = Discount::factory()->create([ + 'store_id' => $store->id, + 'status' => 'active', + ]); + + Livewire::test(DiscountForm::class, ['discount' => $discount]) + ->set('status', 'disabled') + ->call('save') + ->assertRedirect(route('admin.discounts.index')); + + expect($discount->fresh()->status->value)->toBe('disabled'); +}); diff --git a/tests/Feature/Admin/NavigationTest.php b/tests/Feature/Admin/NavigationTest.php new file mode 100644 index 00000000..b0acfa2d --- /dev/null +++ b/tests/Feature/Admin/NavigationTest.php @@ -0,0 +1,48 @@ +forgetInstance('current_store'); +}); + +it('creates a navigation menu', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(NavigationIndex::class) + ->set('newMenuTitle', 'Main menu') + ->call('createMenu'); + + $menu = NavigationMenu::where('title', 'Main menu')->first(); + expect($menu)->not->toBeNull() + ->and($menu->store_id)->toBe($store->id) + ->and($menu->handle)->toBe('main-menu'); +}); + +it('adds an item to a menu', function (): void { + [$user, $store] = loginAsAdmin(); + + $menu = NavigationMenu::create([ + 'store_id' => $store->id, + 'title' => 'Main', + 'handle' => 'main', + ]); + + Livewire::test(NavigationIndex::class) + ->call('openItemModal', $menu->id) + ->set('newItemType', 'link') + ->set('newItemLabel', 'Home') + ->set('newItemUrl', '/') + ->call('addItem'); + + $item = NavigationItem::where('menu_id', $menu->id)->first(); + expect($item)->not->toBeNull() + ->and($item->label)->toBe('Home') + ->and($item->url)->toBe('/'); +}); diff --git a/tests/Feature/Admin/OrderManagementTest.php b/tests/Feature/Admin/OrderManagementTest.php new file mode 100644 index 00000000..a238145a --- /dev/null +++ b/tests/Feature/Admin/OrderManagementTest.php @@ -0,0 +1,163 @@ +forgetInstance('current_store'); +}); + +function setupPaidOrder(int $storeId): Order +{ + $product = Product::factory()->create(['store_id' => $storeId]); + $variant = ProductVariant::factory()->create([ + 'product_id' => $product->id, + 'price_amount' => 2500, + 'currency' => 'EUR', + 'is_default' => true, + ]); + InventoryItem::create([ + 'store_id' => $storeId, + 'variant_id' => $variant->id, + 'quantity_on_hand' => 10, + 'quantity_reserved' => 0, + 'policy' => 'deny', + ]); + + $order = Order::factory()->paid()->create([ + 'store_id' => $storeId, + 'total_amount' => 5000, + 'subtotal_amount' => 5000, + ]); + + OrderLine::create([ + 'order_id' => $order->id, + 'product_id' => $product->id, + 'variant_id' => $variant->id, + 'title_snapshot' => $product->title, + 'sku_snapshot' => $variant->sku, + 'quantity' => 2, + 'unit_price_amount' => 2500, + 'total_amount' => 5000, + ]); + + Payment::factory()->captured()->create([ + 'order_id' => $order->id, + 'amount' => 5000, + ]); + + return $order->fresh(['lines', 'payments']); +} + +it('lists orders in the admin index', function (): void { + [$user, $store] = loginAsAdmin(); + + Order::factory()->create([ + 'store_id' => $store->id, + 'order_number' => '#9001', + 'email' => 'buyer@example.com', + ]); + + Livewire::test(OrdersIndex::class) + ->assertSee('#9001') + ->assertSee('buyer@example.com'); +}); + +it('shows an order detail page', function (): void { + [$user, $store] = loginAsAdmin(); + + $order = setupPaidOrder($store->id); + + Livewire::test(OrdersShow::class, ['order' => $order]) + ->assertSee($order->order_number); +}); + +it('creates a fulfillment for a paid order', function (): void { + [$user, $store] = loginAsAdmin(); + + $order = setupPaidOrder($store->id); + $line = $order->lines->first(); + + Livewire::test(OrdersShow::class, ['order' => $order]) + ->set('fulfillLines.'.$line->id, (int) $line->quantity) + ->call('createFulfillment'); + + expect($order->fresh()->fulfillments()->count())->toBe(1); +}); + +it('refunds an order', function (): void { + [$user, $store] = loginAsAdmin(); + + $order = setupPaidOrder($store->id); + + Livewire::test(OrdersShow::class, ['order' => $order]) + ->set('refundAmount', 1000) + ->set('refundReason', 'Customer request') + ->call('createRefund'); + + expect($order->fresh()->refunds()->count())->toBe(1) + ->and($order->fresh()->refundedTotal())->toBe(1000); +}); + +it('confirms a bank transfer payment', function (): void { + [$user, $store] = loginAsAdmin(); + + $product = Product::factory()->create(['store_id' => $store->id]); + $variant = ProductVariant::factory()->create([ + 'product_id' => $product->id, + 'is_default' => true, + ]); + InventoryItem::create([ + 'store_id' => $store->id, + 'variant_id' => $variant->id, + 'quantity_on_hand' => 5, + 'quantity_reserved' => 1, + 'policy' => 'deny', + ]); + + $order = Order::factory()->create([ + 'store_id' => $store->id, + 'payment_method' => PaymentMethod::BankTransfer->value, + 'status' => OrderStatus::Pending->value, + 'financial_status' => FinancialStatus::Pending->value, + 'fulfillment_status' => FulfillmentStatus::Unfulfilled->value, + ]); + + OrderLine::create([ + 'order_id' => $order->id, + 'product_id' => $product->id, + 'variant_id' => $variant->id, + 'title_snapshot' => $product->title, + 'sku_snapshot' => $variant->sku, + 'quantity' => 1, + 'unit_price_amount' => 2500, + 'total_amount' => 2500, + ]); + + Payment::factory()->create([ + 'order_id' => $order->id, + 'method' => PaymentMethod::BankTransfer->value, + 'status' => PaymentStatus::Pending->value, + 'amount' => 2500, + ]); + + Livewire::test(OrdersShow::class, ['order' => $order->fresh(['lines', 'payments'])]) + ->call('confirmBankTransfer'); + + expect($order->fresh()->financial_status->value)->toBe('paid'); +}); diff --git a/tests/Feature/Admin/PagesTest.php b/tests/Feature/Admin/PagesTest.php new file mode 100644 index 00000000..16fed236 --- /dev/null +++ b/tests/Feature/Admin/PagesTest.php @@ -0,0 +1,58 @@ +forgetInstance('current_store'); +}); + +it('creates a page', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(PageForm::class) + ->set('title', 'About Us') + ->set('bodyHtml', '

Welcome

') + ->set('status', 'published') + ->call('save') + ->assertRedirect(route('admin.pages.index')); + + $page = Page::where('title', 'About Us')->first(); + expect($page)->not->toBeNull() + ->and($page->store_id)->toBe($store->id) + ->and($page->handle)->toBe('about-us') + ->and($page->status->value)->toBe('published'); +}); + +it('edits a page', function (): void { + [$user, $store] = loginAsAdmin(); + + $page = Page::factory()->create([ + 'store_id' => $store->id, + 'title' => 'Draft Title', + 'status' => 'draft', + ]); + + Livewire::test(PageForm::class, ['page' => $page]) + ->set('title', 'New Title') + ->call('save') + ->assertRedirect(route('admin.pages.index')); + + expect($page->fresh()->title)->toBe('New Title'); +}); + +it('deletes a page', function (): void { + [$user, $store] = loginAsAdmin(); + + $page = Page::factory()->create(['store_id' => $store->id]); + + Livewire::test(PageIndex::class) + ->call('delete', $page->id); + + expect(Page::find($page->id))->toBeNull(); +}); diff --git a/tests/Feature/Admin/ProductManagementTest.php b/tests/Feature/Admin/ProductManagementTest.php new file mode 100644 index 00000000..802e39ef --- /dev/null +++ b/tests/Feature/Admin/ProductManagementTest.php @@ -0,0 +1,95 @@ +forgetInstance('current_store'); +}); + +it('lists products for the current store only', function (): void { + [$user, $store] = loginAsAdmin(); + + Product::factory()->create(['store_id' => $store->id, 'title' => 'Alpha Shirt']); + + $otherStore = \App\Models\Store::factory()->create(); + Product::factory()->create(['store_id' => $otherStore->id, 'title' => 'Omega Shoes']); + + Livewire::test(ProductIndex::class) + ->assertSee('Alpha Shirt') + ->assertDontSee('Omega Shoes'); +}); + +it('filters products by search term', function (): void { + [$user, $store] = loginAsAdmin(); + + Product::factory()->create(['store_id' => $store->id, 'title' => 'Red Hat']); + Product::factory()->create(['store_id' => $store->id, 'title' => 'Blue Jacket']); + + Livewire::test(ProductIndex::class) + ->set('search', 'Red') + ->assertSee('Red Hat') + ->assertDontSee('Blue Jacket'); +}); + +it('creates a product via the form', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(ProductForm::class) + ->set('title', 'Test Sneaker') + ->set('status', 'active') + ->set('priceAmount', 1999) + ->set('sku', 'TS-001') + ->call('save') + ->assertRedirect(route('admin.products.index')); + + $product = Product::where('title', 'Test Sneaker')->first(); + expect($product)->not->toBeNull() + ->and($product->store_id)->toBe($store->id) + ->and($product->variants()->first()->price_amount)->toBe(1999); +}); + +it('edits an existing product', function (): void { + [$user, $store] = loginAsAdmin(); + + $product = Product::factory()->create([ + 'store_id' => $store->id, + 'title' => 'Old Name', + ]); + $product->variants()->create([ + 'price_amount' => 1000, + 'currency' => 'EUR', + 'is_default' => true, + 'position' => 0, + 'status' => 'active', + ]); + + Livewire::test(ProductForm::class, ['product' => $product]) + ->set('title', 'New Name') + ->set('priceAmount', 2500) + ->call('save') + ->assertRedirect(route('admin.products.index')); + + expect($product->fresh()->title)->toBe('New Name'); +}); + +it('archives selected products in bulk', function (): void { + [$user, $store] = loginAsAdmin(); + + $product = Product::factory()->create([ + 'store_id' => $store->id, + 'status' => ProductStatus::Active->value, + ]); + + Livewire::test(ProductIndex::class) + ->set('selectedIds', [$product->id]) + ->call('bulkArchive'); + + expect($product->fresh()->status->value)->toBe('archived'); +}); diff --git a/tests/Feature/Admin/SettingsTest.php b/tests/Feature/Admin/SettingsTest.php new file mode 100644 index 00000000..7ac4c7a2 --- /dev/null +++ b/tests/Feature/Admin/SettingsTest.php @@ -0,0 +1,73 @@ +forgetInstance('current_store'); +}); + +it('saves general settings', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(SettingsIndex::class) + ->set('name', 'Renamed Store') + ->set('defaultCurrency', 'USD') + ->set('defaultLocale', 'en') + ->set('timezone', 'UTC') + ->call('save'); + + expect($store->fresh()->name)->toBe('Renamed Store') + ->and($store->fresh()->default_currency)->toBe('USD'); +}); + +it('creates a shipping zone with a rate', function (): void { + [$user, $store] = loginAsAdmin(); + + $component = Livewire::test(SettingsShipping::class) + ->call('openZoneModal') + ->set('zoneName', 'EU') + ->set('zoneCountries', 'DE, AT, CH') + ->call('createZone'); + + $zone = ShippingZone::where('name', 'EU')->first(); + expect($zone)->not->toBeNull() + ->and($zone->countries_json)->toBe(['DE', 'AT', 'CH']); + + $component + ->call('openRateModal', $zone->id) + ->set('rateName', 'Standard') + ->set('rateType', 'flat') + ->set('rateAmount', 599) + ->call('createRate'); + + $rate = ShippingRate::where('zone_id', $zone->id)->first(); + expect($rate)->not->toBeNull() + ->and($rate->config_json['amount'])->toBe(599); +}); + +it('configures tax settings', function (): void { + [$user, $store] = loginAsAdmin(); + + Livewire::test(SettingsTaxes::class) + ->set('mode', 'manual') + ->set('taxName', 'VAT') + ->set('rateBasisPoints', 1900) + ->set('pricesIncludeTax', true) + ->call('save'); + + $settings = TaxSettings::where('store_id', $store->id)->first(); + expect($settings)->not->toBeNull() + ->and($settings->mode->value)->toBe('manual') + ->and($settings->prices_include_tax)->toBeTrue() + ->and($settings->config_json['rate_basis_points'])->toBe(1900) + ->and($settings->config_json['name'])->toBe('VAT'); +}); diff --git a/tests/Feature/Analytics/AggregationTest.php b/tests/Feature/Analytics/AggregationTest.php new file mode 100644 index 00000000..84e425a7 --- /dev/null +++ b/tests/Feature/Analytics/AggregationTest.php @@ -0,0 +1,96 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->date = CarbonImmutable::parse('2026-04-10'); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('aggregates raw events into a daily row', function (): void { + $at = $this->date->setTime(12, 0, 0); + + Order::factory()->for($this->store)->create([ + 'placed_at' => $at, + 'total_amount' => 5000, + ]); + Order::factory()->for($this->store)->create([ + 'placed_at' => $at, + 'total_amount' => 3000, + ]); + + AnalyticsEvent::factory()->for($this->store)->create([ + 'type' => 'page_view', + 'session_id' => 'sess-1', + 'occurred_at' => $at, + ]); + AnalyticsEvent::factory()->for($this->store)->create([ + 'type' => 'page_view', + 'session_id' => 'sess-2', + 'occurred_at' => $at, + ]); + AnalyticsEvent::factory()->for($this->store)->create([ + 'type' => 'page_view', + 'session_id' => 'sess-1', + 'occurred_at' => $at, + ]); + AnalyticsEvent::factory()->for($this->store)->create([ + 'type' => 'add_to_cart', + 'session_id' => 'sess-1', + 'occurred_at' => $at, + ]); + AnalyticsEvent::factory()->for($this->store)->create([ + 'type' => 'checkout_started', + 'session_id' => 'sess-1', + 'occurred_at' => $at, + ]); + AnalyticsEvent::factory()->for($this->store)->create([ + 'type' => 'checkout_completed', + 'session_id' => 'sess-1', + 'occurred_at' => $at, + ]); + + (new AggregateAnalytics($this->date->toDateString()))->handle(); + + $daily = AnalyticsDaily::query() + ->where('store_id', $this->store->id) + ->where('date', $this->date->toDateString()) + ->first(); + + expect($daily)->not->toBeNull() + ->and((int) $daily->orders_count)->toBe(2) + ->and((int) $daily->revenue_amount)->toBe(8000) + ->and((int) $daily->aov_amount)->toBe(4000) + ->and((int) $daily->visits_count)->toBe(2) + ->and((int) $daily->add_to_cart_count)->toBe(1) + ->and((int) $daily->checkout_started_count)->toBe(1) + ->and((int) $daily->checkout_completed_count)->toBe(1); +}); + +it('handles zero events for a store gracefully', function (): void { + (new AggregateAnalytics($this->date->toDateString()))->handle(); + + $daily = AnalyticsDaily::query() + ->where('store_id', $this->store->id) + ->where('date', $this->date->toDateString()) + ->first(); + + expect($daily)->not->toBeNull() + ->and((int) $daily->orders_count)->toBe(0) + ->and((int) $daily->revenue_amount)->toBe(0) + ->and((int) $daily->aov_amount)->toBe(0) + ->and((int) $daily->visits_count)->toBe(0); +}); diff --git a/tests/Feature/Analytics/EventIngestionTest.php b/tests/Feature/Analytics/EventIngestionTest.php new file mode 100644 index 00000000..f7f65d6f --- /dev/null +++ b/tests/Feature/Analytics/EventIngestionTest.php @@ -0,0 +1,36 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('tracks a page view event', function (): void { + $event = app(AnalyticsService::class)->track($this->store, 'page_view', [], 'session-abc'); + + expect($event)->toBeInstanceOf(AnalyticsEvent::class) + ->and($event->type)->toBe('page_view') + ->and($event->session_id)->toBe('session-abc') + ->and($event->store_id)->toBe($this->store->id); +}); + +it('tracks an add_to_cart event with properties', function (): void { + $properties = ['variant_id' => 42, 'quantity' => 2, 'client_event_id' => 'evt-xyz']; + + $event = app(AnalyticsService::class)->track($this->store, 'add_to_cart', $properties, 'session-xyz'); + + expect($event->type)->toBe('add_to_cart') + ->and($event->properties_json)->toBe($properties) + ->and($event->client_event_id)->toBe('evt-xyz'); +}); diff --git a/tests/Feature/Auth/AdminAuthTest.php b/tests/Feature/Auth/AdminAuthTest.php new file mode 100644 index 00000000..3693291b --- /dev/null +++ b/tests/Feature/Auth/AdminAuthTest.php @@ -0,0 +1,70 @@ +create([ + 'email' => 'admin@example.test', + 'password' => Hash::make('secret-pass'), + ]); + + $store = Store::factory()->create(); + $store->users()->attach($user, ['role' => 'owner']); + + Livewire::test(Login::class) + ->set('email', 'admin@example.test') + ->set('password', 'secret-pass') + ->call('login') + ->assertRedirect('/admin'); + + expect(auth('web')->id())->toBe($user->id) + ->and(session('current_store_id'))->toBe($store->id); +}); + +it('rejects login with wrong password', function (): void { + User::factory()->create([ + 'email' => 'admin@example.test', + 'password' => Hash::make('secret-pass'), + ]); + + Livewire::test(Login::class) + ->set('email', 'admin@example.test') + ->set('password', 'nope') + ->call('login') + ->assertHasErrors('email'); + + expect(auth('web')->check())->toBeFalse(); +}); + +it('rate limits login after 5 failed attempts', function (): void { + User::factory()->create([ + 'email' => 'admin@example.test', + 'password' => Hash::make('secret-pass'), + ]); + + $component = Livewire::test(Login::class) + ->set('email', 'admin@example.test') + ->set('password', 'wrong'); + + for ($i = 0; $i < 5; $i++) { + $component->call('login'); + } + + $component->set('password', 'secret-pass') + ->call('login') + ->assertHasErrors('email'); + + expect(auth('web')->check())->toBeFalse(); +}); diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index fff11fd7..06b152aa 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -21,7 +21,7 @@ $response ->assertSessionHasNoErrors() - ->assertRedirect(route('dashboard', absolute: false)); + ->assertRedirect('/admin'); $this->assertAuthenticated(); }); @@ -66,4 +66,4 @@ $response->assertRedirect(route('home')); $this->assertGuest(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php index 66f58e36..3e8866d6 100644 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -31,7 +31,7 @@ Event::assertDispatched(Verified::class); expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); - $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + $response->assertRedirect('/admin?verified=1'); }); test('email is not verified with invalid hash', function () { @@ -62,8 +62,8 @@ ); $this->actingAs($user)->get($verificationUrl) - ->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + ->assertRedirect('/admin?verified=1'); expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); Event::assertNotDispatched(Verified::class); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index c22ea5e1..f429ae4b 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -17,7 +17,7 @@ ]); $response->assertSessionHasNoErrors() - ->assertRedirect(route('dashboard', absolute: false)); + ->assertRedirect('/admin'); $this->assertAuthenticated(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/SanctumTokenTest.php b/tests/Feature/Auth/SanctumTokenTest.php new file mode 100644 index 00000000..74d90cfe --- /dev/null +++ b/tests/Feature/Auth/SanctumTokenTest.php @@ -0,0 +1,32 @@ +create(); + + $token = $user->createToken('test-token'); + + expect($token->plainTextToken)->toBeString() + ->and($user->tokens()->count())->toBe(1); +}); + +it('authenticates an API request with a valid bearer token', function (): void { + $user = User::factory()->create(); + $token = $user->createToken('api')->plainTextToken; + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->getJson('/api/admin/user'); + + $response->assertOk() + ->assertJsonPath('id', $user->id); +}); + +it('rejects an API request without a token', function (): void { + $response = $this->getJson('/api/admin/user'); + + $response->assertStatus(401); +}); diff --git a/tests/Feature/Cart/CartServiceTest.php b/tests/Feature/Cart/CartServiceTest.php new file mode 100644 index 00000000..a1abfd6a --- /dev/null +++ b/tests/Feature/Cart/CartServiceTest.php @@ -0,0 +1,105 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->service = new CartService(new InventoryService); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function makeVariantWithStock(Store $store, int $price = 1000, int $onHand = 10): ProductVariant +{ + $product = Product::factory()->for($store)->create(); + $variant = ProductVariant::factory()->for($product)->create(['price_amount' => $price]); + InventoryItem::factory() + ->for($store) + ->for($variant, 'variant') + ->create(['quantity_on_hand' => $onHand]); + + return $variant; +} + +it('adds a line to a cart', function (): void { + $cart = $this->service->create($this->store); + $variant = makeVariantWithStock($this->store, price: 2000); + + $line = $this->service->addLine($cart, $variant->id, 2); + + expect($line->quantity)->toBe(2) + ->and($line->line_subtotal_amount)->toBe(4000) + ->and($line->line_total_amount)->toBe(4000); +}); + +it('increments quantity for existing variant', function (): void { + $cart = $this->service->create($this->store); + $variant = makeVariantWithStock($this->store, price: 1000); + + $this->service->addLine($cart, $variant->id, 1); + $line = $this->service->addLine($cart->fresh(), $variant->id, 2); + + expect($cart->lines()->count())->toBe(1) + ->and($line->quantity)->toBe(3) + ->and($line->line_subtotal_amount)->toBe(3000); +}); + +it('validates inventory before adding', function (): void { + $cart = $this->service->create($this->store); + $variant = makeVariantWithStock($this->store, price: 1000, onHand: 2); + + $this->service->addLine($cart, $variant->id, 5); +})->throws(RuntimeException::class, 'Insufficient inventory'); + +it('removes a line', function (): void { + $cart = $this->service->create($this->store); + $variant = makeVariantWithStock($this->store); + $line = $this->service->addLine($cart, $variant->id, 1); + + $this->service->removeLine($cart->fresh(), $line->id); + + expect($cart->lines()->count())->toBe(0); +}); + +it('updates quantity', function (): void { + $cart = $this->service->create($this->store); + $variant = makeVariantWithStock($this->store, price: 500); + $line = $this->service->addLine($cart, $variant->id, 1); + + $updated = $this->service->updateLineQuantity($cart->fresh(), $line->id, 4); + + expect($updated->quantity)->toBe(4) + ->and($updated->line_subtotal_amount)->toBe(2000); +}); + +it('merges guest cart into customer cart on login', function (): void { + $guestCart = $this->service->create($this->store); + $customerCart = $this->service->create($this->store); + $variantA = makeVariantWithStock($this->store, price: 500); + $variantB = makeVariantWithStock($this->store, price: 1500); + + $this->service->addLine($guestCart, $variantA->id, 2); + $this->service->addLine($guestCart->fresh(), $variantB->id, 1); + $this->service->addLine($customerCart, $variantA->id, 1); + + $merged = $this->service->mergeOnLogin($guestCart->fresh(), $customerCart->fresh()); + + $lines = $merged->fresh('lines')->lines; + $variantALine = $lines->firstWhere('variant_id', $variantA->id); + $variantBLine = $lines->firstWhere('variant_id', $variantB->id); + + expect($lines)->toHaveCount(2) + ->and($variantALine->quantity)->toBe(3) + ->and($variantBLine->quantity)->toBe(1); +}); diff --git a/tests/Feature/Checkout/CheckoutFlowTest.php b/tests/Feature/Checkout/CheckoutFlowTest.php new file mode 100644 index 00000000..f0074a58 --- /dev/null +++ b/tests/Feature/Checkout/CheckoutFlowTest.php @@ -0,0 +1,139 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->inventoryService = new InventoryService; + $this->cartService = new CartService($this->inventoryService); + $this->checkoutService = new CheckoutService( + new PricingEngine( + new DiscountService, + new ShippingCalculator, + new TaxCalculator, + ), + $this->inventoryService, + new OrderService, + new MockPaymentProvider, + ); + + $product = Product::factory()->for($this->store)->create(); + $this->variant = ProductVariant::factory()->for($product)->create([ + 'price_amount' => 2500, + 'weight_g' => 300, + 'requires_shipping' => true, + ]); + InventoryItem::factory() + ->for($this->store) + ->for($this->variant, 'variant') + ->create(['quantity_on_hand' => 10]); + + $this->cart = $this->cartService->create($this->store); + $this->cartService->addLine($this->cart, $this->variant->id, 2); + $this->cart->refresh(); + + $this->zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + 'regions_json' => [], + ]); + $this->rate = ShippingRate::factory()->for($this->zone, 'zone')->flat(599)->create(); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('starts checkout from cart', function (): void { + $checkout = $this->checkoutService->start($this->cart); + + expect($checkout->status)->toBe(CheckoutStatus::Started) + ->and((int) $checkout->cart_id)->toBe($this->cart->id); +}); + +it('transitions started -> addressed via setAddress', function (): void { + $checkout = $this->checkoutService->start($this->cart); + + $result = $this->checkoutService->setAddress($checkout, [ + 'email' => 'buyer@example.com', + 'shipping_address' => [ + 'first_name' => 'A', + 'last_name' => 'B', + 'address1' => 'Street 1', + 'city' => 'Berlin', + 'country' => 'DE', + 'postal_code' => '10115', + ], + ]); + + expect($result->status)->toBe(CheckoutStatus::Addressed) + ->and($result->email)->toBe('buyer@example.com'); +}); + +it('transitions addressed -> shipping_selected', function (): void { + $checkout = $this->checkoutService->start($this->cart); + $this->checkoutService->setAddress($checkout, [ + 'email' => 'a@b.c', + 'shipping_address' => ['country' => 'DE'], + ]); + + $result = $this->checkoutService->setShippingMethod($checkout->fresh(), $this->rate->id); + + expect($result->status)->toBe(CheckoutStatus::ShippingSelected) + ->and((int) $result->shipping_method_id)->toBe($this->rate->id); +}); + +it('transitions shipping_selected -> payment_selected', function (): void { + $checkout = $this->checkoutService->start($this->cart); + $this->checkoutService->setAddress($checkout, [ + 'email' => 'a@b.c', + 'shipping_address' => ['country' => 'DE'], + ]); + $this->checkoutService->setShippingMethod($checkout->fresh(), $this->rate->id); + + $result = $this->checkoutService->selectPaymentMethod($checkout->fresh(), 'credit_card'); + + expect($result->status)->toBe(CheckoutStatus::PaymentSelected) + ->and($result->payment_method)->toBe('credit_card') + ->and($result->expires_at)->not->toBeNull(); +}); + +it('rejects invalid transitions', function (): void { + $checkout = $this->checkoutService->start($this->cart); + + $this->checkoutService->selectPaymentMethod($checkout, 'credit_card'); +})->throws(DomainException::class); + +it('recalculates totals on each step', function (): void { + $checkout = $this->checkoutService->start($this->cart); + $result = $this->checkoutService->setAddress($checkout, [ + 'email' => 'a@b.c', + 'shipping_address' => ['country' => 'DE'], + ]); + + expect($result->totals_json)->not->toBeNull() + ->and($result->totals_json['subtotal'])->toBe(5000); + + $result = $this->checkoutService->setShippingMethod($result->fresh(), $this->rate->id); + + expect($result->totals_json['shipping'])->toBe(599); +}); diff --git a/tests/Feature/Checkout/CheckoutStateTest.php b/tests/Feature/Checkout/CheckoutStateTest.php new file mode 100644 index 00000000..6971b1e4 --- /dev/null +++ b/tests/Feature/Checkout/CheckoutStateTest.php @@ -0,0 +1,68 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->inventoryService = new InventoryService; + $this->cartService = new CartService($this->inventoryService); + $this->checkoutService = new CheckoutService( + new PricingEngine(new DiscountService, new ShippingCalculator, new TaxCalculator), + $this->inventoryService, + new OrderService, + new MockPaymentProvider, + ); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('expires checkouts via job and releases reserved inventory', function (): void { + $product = Product::factory()->for($this->store)->create(); + $variant = ProductVariant::factory()->for($product)->create([ + 'price_amount' => 1000, + 'requires_shipping' => false, + ]); + $inventory = InventoryItem::factory() + ->for($this->store) + ->for($variant, 'variant') + ->create(['quantity_on_hand' => 10, 'quantity_reserved' => 0]); + + $cart = $this->cartService->create($this->store); + $this->cartService->addLine($cart, $variant->id, 2); + $checkout = $this->checkoutService->start($cart); + $this->checkoutService->setAddress($checkout, [ + 'email' => 'a@b.c', + 'shipping_address' => ['country' => 'DE'], + ]); + $this->checkoutService->selectPaymentMethod($checkout->fresh(), 'credit_card'); + + expect($inventory->fresh()->quantity_reserved)->toBe(2); + + $checkout->fresh()->update(['expires_at' => now()->subHour()]); + + app(ExpireAbandonedCheckouts::class)->handle($this->checkoutService); + + expect($checkout->fresh()->status)->toBe(CheckoutStatus::Expired) + ->and($inventory->fresh()->quantity_reserved)->toBe(0); +}); diff --git a/tests/Feature/Customers/CustomerAccountTest.php b/tests/Feature/Customers/CustomerAccountTest.php new file mode 100644 index 00000000..26af400b --- /dev/null +++ b/tests/Feature/Customers/CustomerAccountTest.php @@ -0,0 +1,54 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('creates a customer with hashed password', function (): void { + $customer = Customer::factory()->for($this->store)->create([ + 'email' => 'jane@example.com', + 'password_hash' => 'secret-password', + ]); + + expect($customer->email)->toBe('jane@example.com') + ->and(Hash::check('secret-password', $customer->password_hash))->toBeTrue(); +}); + +it('enforces unique email per store but allows duplicates across stores', function (): void { + Customer::factory()->for($this->store)->create(['email' => 'same@example.com']); + + $otherStore = Store::factory()->create(); + Customer::withoutGlobalScopes()->create([ + 'store_id' => $otherStore->id, + 'email' => 'same@example.com', + 'name' => 'Dup', + ]); + + expect(Customer::withoutGlobalScopes()->where('email', 'same@example.com')->count())->toBe(2); + + expect(fn () => Customer::factory()->for($this->store)->create(['email' => 'same@example.com'])) + ->toThrow(QueryException::class); +}); + +it('can authenticate using the hashed password column', function (): void { + $customer = Customer::factory()->for($this->store)->create([ + 'email' => 'auth@example.com', + 'password_hash' => 'password', + ]); + + expect($customer->getAuthPassword())->toBe($customer->password_hash) + ->and(Hash::check('password', $customer->getAuthPassword()))->toBeTrue(); +}); diff --git a/tests/Feature/Customers/CustomerLoginTest.php b/tests/Feature/Customers/CustomerLoginTest.php new file mode 100644 index 00000000..435df0e9 --- /dev/null +++ b/tests/Feature/Customers/CustomerLoginTest.php @@ -0,0 +1,82 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('registers a new customer and logs them in', function (): void { + Livewire::test(Register::class) + ->set('name', 'Ada Lovelace') + ->set('email', 'ada@example.com') + ->set('password', 'secretpass') + ->set('password_confirmation', 'secretpass') + ->call('register') + ->assertHasNoErrors(); + + expect(Auth::guard('customer')->check())->toBeTrue(); + expect(Customer::query()->where('email', 'ada@example.com')->exists())->toBeTrue(); +}); + +it('logs in an existing customer', function (): void { + Customer::factory()->for($this->store)->create([ + 'email' => 'grace@example.com', + 'password_hash' => 'hoppers', + ]); + + Livewire::test(Login::class) + ->set('email', 'grace@example.com') + ->set('password', 'hoppers') + ->call('login') + ->assertHasNoErrors(); + + expect(Auth::guard('customer')->check())->toBeTrue(); +}); + +it('rejects invalid credentials', function (): void { + Customer::factory()->for($this->store)->create([ + 'email' => 'fail@example.com', + 'password_hash' => 'rightone', + ]); + + Livewire::test(Login::class) + ->set('email', 'fail@example.com') + ->set('password', 'wrongone') + ->call('login') + ->assertHasErrors('email'); + + expect(Auth::guard('customer')->check())->toBeFalse(); +}); + +it('blocks unauthenticated access to account dashboard via route', function (): void { + $this->get(route('storefront.account.dashboard')) + ->assertRedirect(route('storefront.account.login')); +}); + +it('logs out a customer via the logout route', function (): void { + $customer = Customer::factory()->for($this->store)->create([ + 'password_hash' => 'password', + ]); + + Auth::guard('customer')->login($customer); + expect(Auth::guard('customer')->check())->toBeTrue(); + + $this->post(route('storefront.account.logout')) + ->assertRedirect(route('storefront.account.login')); + + expect(Auth::guard('customer')->check())->toBeFalse(); +}); diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php index fcd0258d..d3a2bb1e 100644 --- a/tests/Feature/DashboardTest.php +++ b/tests/Feature/DashboardTest.php @@ -4,15 +4,15 @@ uses(\Illuminate\Foundation\Testing\RefreshDatabase::class); -test('guests are redirected to the login page', function () { +test('the legacy dashboard route redirects to the admin home', function () { $response = $this->get(route('dashboard')); - $response->assertRedirect(route('login')); + $response->assertRedirect('/admin'); }); -test('authenticated users can visit the dashboard', function () { +test('authenticated users land on the admin home from the legacy dashboard route', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('dashboard')); - $response->assertOk(); -}); \ No newline at end of file + $response->assertRedirect('/admin'); +}); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 8b5843f4..93efb40b 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -1,5 +1,9 @@ get('/'); diff --git a/tests/Feature/Navigation/NavigationTest.php b/tests/Feature/Navigation/NavigationTest.php new file mode 100644 index 00000000..fdb101fd --- /dev/null +++ b/tests/Feature/Navigation/NavigationTest.php @@ -0,0 +1,92 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->service = app(NavigationService::class); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); + Cache::flush(); +}); + +it('builds an ordered tree of navigation items', function (): void { + $menu = NavigationMenu::factory()->create(); + + NavigationItem::factory()->create([ + 'menu_id' => $menu->id, + 'label' => 'Third', + 'position' => 30, + ]); + NavigationItem::factory()->create([ + 'menu_id' => $menu->id, + 'label' => 'First', + 'position' => 10, + ]); + NavigationItem::factory()->create([ + 'menu_id' => $menu->id, + 'label' => 'Second', + 'position' => 20, + ]); + + $tree = $this->service->buildTree($menu); + + expect($tree)->toHaveCount(3) + ->and($tree[0]['label'])->toBe('First') + ->and($tree[1]['label'])->toBe('Second') + ->and($tree[2]['label'])->toBe('Third'); +}); + +it('resolves placeholder URLs for page, collection and product items', function (): void { + $menu = NavigationMenu::factory()->create(); + + $pageItem = NavigationItem::factory()->create([ + 'menu_id' => $menu->id, + 'type' => NavigationItemType::Page->value, + 'url' => null, + 'resource_id' => 42, + ]); + $collectionItem = NavigationItem::factory()->create([ + 'menu_id' => $menu->id, + 'type' => NavigationItemType::Collection->value, + 'url' => null, + 'resource_id' => 7, + ]); + $productItem = NavigationItem::factory()->create([ + 'menu_id' => $menu->id, + 'type' => NavigationItemType::Product->value, + 'url' => null, + 'resource_id' => 99, + ]); + $linkItem = NavigationItem::factory()->create([ + 'menu_id' => $menu->id, + 'type' => NavigationItemType::Link->value, + 'url' => '/external', + ]); + + expect($pageItem->resolveUrl())->toBe('/pages/42') + ->and($collectionItem->resolveUrl())->toBe('/collections/7') + ->and($productItem->resolveUrl())->toBe('/products/99') + ->and($linkItem->resolveUrl())->toBe('/external'); +}); + +it('caches the menu tree', function (): void { + $menu = NavigationMenu::factory()->create(); + NavigationItem::factory()->count(2)->create(['menu_id' => $menu->id]); + + $this->service->buildTree($menu); + + expect(Cache::has("nav:menu:{$menu->id}"))->toBeTrue(); +}); diff --git a/tests/Feature/Orders/FulfillmentTest.php b/tests/Feature/Orders/FulfillmentTest.php new file mode 100644 index 00000000..10012d52 --- /dev/null +++ b/tests/Feature/Orders/FulfillmentTest.php @@ -0,0 +1,117 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->fulfillmentService = new FulfillmentService; + + $this->order = Order::factory()->for($this->store)->paid()->create([ + 'subtotal_amount' => 6000, + 'total_amount' => 6000, + ]); + + $this->line1 = OrderLine::factory()->create([ + 'order_id' => $this->order->id, + 'title_snapshot' => 'Line A', + 'quantity' => 2, + 'unit_price_amount' => 2000, + 'total_amount' => 4000, + ]); + + $this->line2 = OrderLine::factory()->create([ + 'order_id' => $this->order->id, + 'title_snapshot' => 'Line B', + 'quantity' => 1, + 'unit_price_amount' => 2000, + 'total_amount' => 2000, + ]); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('creates a fulfillment for a paid order', function (): void { + $fulfillment = $this->fulfillmentService->create( + $this->order, + [$this->line1->id => 2, $this->line2->id => 1], + ['company' => 'DHL', 'number' => 'TRACK123', 'url' => 'https://example.com/track/TRACK123'], + ); + + expect($fulfillment->tracking_company)->toBe('DHL') + ->and($fulfillment->tracking_number)->toBe('TRACK123') + ->and($fulfillment->lines)->toHaveCount(2) + ->and($this->order->fresh()->fulfillment_status)->toBe(FulfillmentStatus::Fulfilled) + ->and($this->order->fresh()->status)->toBe(OrderStatus::Fulfilled); +}); + +it('marks partial fulfillment when only some lines are shipped', function (): void { + $this->fulfillmentService->create($this->order, [$this->line1->id => 1]); + + expect($this->order->fresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial); +}); + +it('marks a fulfillment as shipped with tracking', function (): void { + $fulfillment = $this->fulfillmentService->create($this->order, [$this->line1->id => 2]); + + $this->fulfillmentService->markAsShipped($fulfillment, [ + 'company' => 'UPS', + 'number' => 'UPS9999', + 'url' => 'https://ups.com/track', + ]); + + expect($fulfillment->fresh()->status)->toBe(FulfillmentShipmentStatus::Shipped) + ->and($fulfillment->fresh()->shipped_at)->not->toBeNull() + ->and($fulfillment->fresh()->tracking_company)->toBe('UPS'); +}); + +it('marks a fulfillment as delivered and dispatches event', function (): void { + \Illuminate\Support\Facades\Event::fake(\App\Events\FulfillmentDelivered::class); + + $fulfillment = $this->fulfillmentService->create($this->order, [$this->line1->id => 2]); + $this->fulfillmentService->markAsShipped($fulfillment, ['company' => 'DHL', 'number' => 'X']); + $this->fulfillmentService->markAsDelivered($fulfillment->fresh()); + + expect($fulfillment->fresh()->status)->toBe(FulfillmentShipmentStatus::Delivered) + ->and($fulfillment->fresh()->delivered_at)->not->toBeNull(); + + \Illuminate\Support\Facades\Event::assertDispatched(\App\Events\FulfillmentDelivered::class); +}); + +it('guard rejects fulfillment for an unpaid order', function (): void { + $unpaidOrder = Order::factory()->for($this->store)->create([ + 'subtotal_amount' => 1000, + 'total_amount' => 1000, + ]); + $line = OrderLine::factory()->create([ + 'order_id' => $unpaidOrder->id, + 'quantity' => 1, + 'unit_price_amount' => 1000, + 'total_amount' => 1000, + ]); + + expect(fn () => $this->fulfillmentService->create($unpaidOrder, [$line->id => 1])) + ->toThrow(FulfillmentGuardException::class); +}); + +it('updates order to fulfilled after two partial fulfillments complete it', function (): void { + $this->fulfillmentService->create($this->order, [$this->line1->id => 2]); + expect($this->order->fresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial); + + $this->fulfillmentService->create($this->order->fresh(), [$this->line2->id => 1]); + expect($this->order->fresh()->fulfillment_status)->toBe(FulfillmentStatus::Fulfilled) + ->and($this->order->fresh()->status)->toBe(OrderStatus::Fulfilled); +}); diff --git a/tests/Feature/Orders/OrderCreationTest.php b/tests/Feature/Orders/OrderCreationTest.php new file mode 100644 index 00000000..29e9177c --- /dev/null +++ b/tests/Feature/Orders/OrderCreationTest.php @@ -0,0 +1,145 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->inventoryService = new InventoryService; + $this->cartService = new CartService($this->inventoryService); + $this->orderService = new OrderService; + $this->checkoutService = new CheckoutService( + new PricingEngine(new DiscountService, new ShippingCalculator, new TaxCalculator), + $this->inventoryService, + $this->orderService, + new MockPaymentProvider, + ); + + $this->product = Product::factory()->for($this->store)->create(['title' => 'Herbal Tea']); + $this->variant = ProductVariant::factory()->for($this->product)->create([ + 'price_amount' => 2500, + 'requires_shipping' => true, + 'sku' => 'TEA-001', + ]); + InventoryItem::factory() + ->for($this->store) + ->for($this->variant, 'variant') + ->create(['quantity_on_hand' => 10]); + + $this->zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + 'regions_json' => [], + ]); + $this->rate = ShippingRate::factory()->for($this->zone, 'zone')->flat(599)->create(); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function runCheckoutToPayment(): \App\Models\Checkout +{ + $cart = test()->cartService->create(test()->store); + test()->cartService->addLine($cart, test()->variant->id, 2); + + $checkout = test()->checkoutService->start($cart->fresh()); + test()->checkoutService->setAddress($checkout, [ + 'email' => 'buyer@example.com', + 'shipping_address' => ['country' => 'DE'], + ]); + test()->checkoutService->setShippingMethod($checkout->fresh(), test()->rate->id); + test()->checkoutService->selectPaymentMethod($checkout->fresh(), 'credit_card'); + + return $checkout->fresh(); +} + +it('creates an order from a completed checkout', function (): void { + $checkout = runCheckoutToPayment(); + + $this->checkoutService->complete($checkout, ['card_number' => '4242424242424242']); + + $order = \App\Models\Order::withoutGlobalScopes()->first(); + + expect($order)->not->toBeNull() + ->and($order->store_id)->toBe($this->store->id) + ->and($order->email)->toBe('buyer@example.com') + ->and($order->status)->toBe(OrderStatus::Paid) + ->and($order->financial_status)->toBe(FinancialStatus::Paid) + ->and($order->payment_method)->toBe(PaymentMethod::CreditCard) + ->and($order->lines)->toHaveCount(1) + ->and($order->lines->first()->title_snapshot)->toBe('Herbal Tea') + ->and($order->lines->first()->sku_snapshot)->toBe('TEA-001') + ->and($order->lines->first()->quantity)->toBe(2); +}); + +it('generates sequential order numbers per store', function (): void { + $checkout = runCheckoutToPayment(); + $order1 = $this->orderService->createFromCheckout($checkout); + + // Second cart/checkout cycle in same store + $cart2 = $this->cartService->create($this->store); + $this->cartService->addLine($cart2, $this->variant->id, 1); + $checkout2 = $this->checkoutService->start($cart2->fresh()); + $this->checkoutService->setAddress($checkout2, [ + 'email' => 'b@c.d', + 'shipping_address' => ['country' => 'DE'], + ]); + $this->checkoutService->setShippingMethod($checkout2->fresh(), $this->rate->id); + $this->checkoutService->selectPaymentMethod($checkout2->fresh(), 'credit_card'); + + $order2 = $this->orderService->createFromCheckout($checkout2->fresh()); + + expect($order1->order_number)->toBe('#1001') + ->and($order2->order_number)->toBe('#1002'); +}); + +it('cancels an order and releases reserved inventory', function (): void { + $checkout = runCheckoutToPayment(); + $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $this->variant->id)->first(); + + expect($inventory->quantity_reserved)->toBe(2); + + $order = $this->orderService->createFromCheckout($checkout); + + $this->orderService->cancel($order->fresh(), 'Customer request'); + + expect($order->fresh()->status)->toBe(OrderStatus::Cancelled) + ->and($inventory->fresh()->quantity_reserved)->toBe(0); +}); + +it('rejects payment with declined magic card and releases inventory', function (): void { + $checkout = runCheckoutToPayment(); + $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $this->variant->id)->first(); + + expect($inventory->quantity_reserved)->toBe(2); + + try { + $this->checkoutService->complete($checkout, ['card_number' => '4000000000000002']); + } catch (\App\Exceptions\PaymentFailedException $e) { + // expected + } + + expect($inventory->fresh()->quantity_reserved)->toBe(0) + ->and(\App\Models\Order::withoutGlobalScopes()->count())->toBe(0); +}); diff --git a/tests/Feature/Orders/RefundTest.php b/tests/Feature/Orders/RefundTest.php new file mode 100644 index 00000000..1158de6b --- /dev/null +++ b/tests/Feature/Orders/RefundTest.php @@ -0,0 +1,99 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->refundService = new RefundService(new MockPaymentProvider, new InventoryService); + + $product = Product::factory()->for($this->store)->create(['title' => 'Candle']); + $this->variant = ProductVariant::factory()->for($product)->create([ + 'price_amount' => 1500, + 'requires_shipping' => true, + ]); + InventoryItem::factory() + ->for($this->store) + ->for($this->variant, 'variant') + ->create(['quantity_on_hand' => 8]); + + $this->order = Order::factory()->for($this->store)->paid()->create([ + 'subtotal_amount' => 3000, + 'total_amount' => 3000, + ]); + OrderLine::factory()->create([ + 'order_id' => $this->order->id, + 'variant_id' => $this->variant->id, + 'title_snapshot' => 'Candle', + 'quantity' => 2, + 'unit_price_amount' => 1500, + 'total_amount' => 3000, + ]); + $this->payment = Payment::factory()->captured()->create([ + 'order_id' => $this->order->id, + 'amount' => 3000, + ]); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('creates a partial refund and marks order as partially refunded', function (): void { + $refund = $this->refundService->create($this->order, $this->payment, 1000, 'customer_request', false); + + expect($refund->status)->toBe(RefundStatus::Processed) + ->and($refund->amount)->toBe(1000) + ->and($this->order->fresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded); +}); + +it('creates a full refund and marks order as refunded', function (): void { + $this->refundService->create($this->order, $this->payment, 3000, 'customer_request', false); + + expect($this->order->fresh()->financial_status)->toBe(FinancialStatus::Refunded); +}); + +it('restocks inventory when restock flag is true', function (): void { + $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $this->variant->id)->first(); + $startingStock = $inventory->quantity_on_hand; + + $this->refundService->create($this->order, $this->payment, 3000, 'customer_request', true); + + expect($inventory->fresh()->quantity_on_hand)->toBe($startingStock + 2); +}); + +it('does not restock inventory when restock flag is false', function (): void { + $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $this->variant->id)->first(); + $startingStock = $inventory->quantity_on_hand; + + $this->refundService->create($this->order, $this->payment, 1500, null, false); + + expect($inventory->fresh()->quantity_on_hand)->toBe($startingStock); +}); + +it('rejects a refund amount exceeding the remaining payment balance', function (): void { + $this->refundService->create($this->order, $this->payment, 3000, null, false); + + expect(fn () => $this->refundService->create($this->order->fresh(), $this->payment->fresh(), 1, null, false)) + ->toThrow(InvalidArgumentException::class); +}); + +it('rejects refunds with zero or negative amount', function (): void { + expect(fn () => $this->refundService->create($this->order, $this->payment, 0, null, false)) + ->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/Feature/Pages/PageTest.php b/tests/Feature/Pages/PageTest.php new file mode 100644 index 00000000..c2bad1fb --- /dev/null +++ b/tests/Feature/Pages/PageTest.php @@ -0,0 +1,70 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('creates a page via factory', function (): void { + $page = Page::factory()->create(['store_id' => $this->store->id]); + + expect($page->exists)->toBeTrue() + ->and($page->status)->toBe(PageStatus::Published) + ->and($page->store_id)->toBe($this->store->id); +}); + +it('scopes pages to the current store', function (): void { + $storeA = $this->store; + $storeB = Store::factory()->create(); + + Page::factory()->create(['store_id' => $storeA->id]); + + app()->instance('current_store', $storeB); + Page::factory()->create(['store_id' => $storeB->id]); + + expect(Page::count())->toBe(1); + + app()->instance('current_store', $storeA); + expect(Page::count())->toBe(1); +}); + +it('enforces unique handle per store', function (): void { + Page::factory()->create([ + 'store_id' => $this->store->id, + 'handle' => 'about-us', + ]); + + expect(fn () => Page::factory()->create([ + 'store_id' => $this->store->id, + 'handle' => 'about-us', + ]))->toThrow(QueryException::class); +}); + +it('allows the same handle across different stores', function (): void { + $storeB = Store::factory()->create(); + + Page::factory()->create([ + 'store_id' => $this->store->id, + 'handle' => 'about-us', + ]); + + app()->instance('current_store', $storeB); + $page = Page::factory()->create([ + 'store_id' => $storeB->id, + 'handle' => 'about-us', + ]); + + expect($page->exists)->toBeTrue(); +}); diff --git a/tests/Feature/Payments/BankTransferConfirmationTest.php b/tests/Feature/Payments/BankTransferConfirmationTest.php new file mode 100644 index 00000000..72d7cf93 --- /dev/null +++ b/tests/Feature/Payments/BankTransferConfirmationTest.php @@ -0,0 +1,126 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->inventoryService = new InventoryService; + $this->cartService = new CartService($this->inventoryService); + $this->orderService = new OrderService; + $this->checkoutService = new CheckoutService( + new PricingEngine(new DiscountService, new ShippingCalculator, new TaxCalculator), + $this->inventoryService, + $this->orderService, + new MockPaymentProvider, + ); + + $product = Product::factory()->for($this->store)->create(['title' => 'Wool Scarf']); + $this->variant = ProductVariant::factory()->for($product)->create([ + 'price_amount' => 3000, + 'requires_shipping' => true, + ]); + InventoryItem::factory() + ->for($this->store) + ->for($this->variant, 'variant') + ->create(['quantity_on_hand' => 5]); + + $this->zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + 'regions_json' => [], + ]); + $this->rate = ShippingRate::factory()->for($this->zone, 'zone')->flat(599)->create(); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function completeWithBankTransfer(): \App\Models\Order +{ + $cart = test()->cartService->create(test()->store); + test()->cartService->addLine($cart, test()->variant->id, 1); + + $checkout = test()->checkoutService->start($cart->fresh()); + test()->checkoutService->setAddress($checkout, [ + 'email' => 'b@c.d', + 'shipping_address' => ['country' => 'DE'], + ]); + test()->checkoutService->setShippingMethod($checkout->fresh(), test()->rate->id); + test()->checkoutService->selectPaymentMethod($checkout->fresh(), 'bank_transfer'); + test()->checkoutService->complete($checkout->fresh()); + + return \App\Models\Order::withoutGlobalScopes()->latest('id')->firstOrFail(); +} + +it('creates a pending order for bank transfer without committing inventory', function (): void { + $order = completeWithBankTransfer(); + $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $this->variant->id)->first(); + + expect($order->financial_status)->toBe(FinancialStatus::Pending) + ->and($order->status)->toBe(OrderStatus::Pending) + ->and($inventory->quantity_reserved)->toBe(1) + ->and($inventory->quantity_on_hand)->toBe(5); + + $payment = $order->payments()->first(); + expect($payment->status)->toBe(PaymentStatus::Pending); +}); + +it('confirms bank transfer payment and commits inventory', function (): void { + $order = completeWithBankTransfer(); + + $this->orderService->confirmBankTransferPayment($order->fresh()); + + $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $this->variant->id)->first(); + + expect($order->fresh()->financial_status)->toBe(FinancialStatus::Paid) + ->and($order->fresh()->status)->toBe(OrderStatus::Paid) + ->and($inventory->quantity_on_hand)->toBe(4) + ->and($inventory->quantity_reserved)->toBe(0); + + $payment = $order->payments()->first(); + expect($payment->status)->toBe(PaymentStatus::Captured); +}); + +it('cancels pending bank transfer orders older than 7 days via job', function (): void { + $order = completeWithBankTransfer(); + $order->update(['placed_at' => now()->subDays(8)]); + + app(CancelUnpaidBankTransferOrders::class)->handle($this->orderService); + + expect($order->fresh()->status)->toBe(OrderStatus::Cancelled); + + $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $this->variant->id)->first(); + expect($inventory->quantity_reserved)->toBe(0); +}); + +it('does not cancel bank transfer orders placed less than 7 days ago', function (): void { + $order = completeWithBankTransfer(); + $order->update(['placed_at' => now()->subDays(3)]); + + app(CancelUnpaidBankTransferOrders::class)->handle($this->orderService); + + expect($order->fresh()->status)->toBe(OrderStatus::Pending); +}); diff --git a/tests/Feature/Payments/MockPaymentProviderTest.php b/tests/Feature/Payments/MockPaymentProviderTest.php new file mode 100644 index 00000000..866624ae --- /dev/null +++ b/tests/Feature/Payments/MockPaymentProviderTest.php @@ -0,0 +1,85 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->provider = new MockPaymentProvider; +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function buildCheckout(Store $store, int $total = 5000, string $currency = 'EUR'): Checkout +{ + $cart = \App\Models\Cart::factory()->for($store)->create(['currency' => $currency]); + + return Checkout::factory()->for($store)->for($cart)->create([ + 'payment_method' => PaymentMethod::CreditCard->value, + 'totals_json' => ['total' => $total, 'currency' => $currency], + ]); +} + +it('captures a credit card payment with success magic number', function (): void { + $checkout = buildCheckout($this->store); + + $result = $this->provider->charge($checkout, PaymentMethod::CreditCard, [ + 'card_number' => '4242424242424242', + ]); + + expect($result->status)->toBe(PaymentStatus::Captured) + ->and($result->providerPaymentId)->toStartWith('mock_') + ->and($result->amount)->toBe(5000) + ->and($result->currency)->toBe('EUR'); +}); + +it('fails a credit card payment when magic number is declined', function (): void { + $checkout = buildCheckout($this->store); + + $result = $this->provider->charge($checkout, PaymentMethod::CreditCard, [ + 'card_number' => '4000000000000002', + ]); + + expect($result->status)->toBe(PaymentStatus::Failed) + ->and($result->errorMessage)->toBe('card_declined') + ->and($result->providerPaymentId)->toBeNull(); +}); + +it('fails a credit card payment when insufficient funds', function (): void { + $checkout = buildCheckout($this->store); + + $result = $this->provider->charge($checkout, PaymentMethod::CreditCard, [ + 'card_number' => '4000000000009995', + ]); + + expect($result->status)->toBe(PaymentStatus::Failed) + ->and($result->errorMessage)->toBe('insufficient_funds') + ->and($result->providerPaymentId)->toBeNull(); +}); + +it('captures a paypal payment', function (): void { + $checkout = buildCheckout($this->store); + + $result = $this->provider->charge($checkout, PaymentMethod::Paypal, []); + + expect($result->status)->toBe(PaymentStatus::Captured) + ->and($result->providerPaymentId)->toStartWith('mock_'); +}); + +it('returns pending for a bank transfer payment', function (): void { + $checkout = buildCheckout($this->store); + + $result = $this->provider->charge($checkout, PaymentMethod::BankTransfer, []); + + expect($result->status)->toBe(PaymentStatus::Pending) + ->and($result->providerPaymentId)->toStartWith('mock_'); +}); diff --git a/tests/Feature/Payments/PaymentServiceTest.php b/tests/Feature/Payments/PaymentServiceTest.php new file mode 100644 index 00000000..4541d67b --- /dev/null +++ b/tests/Feature/Payments/PaymentServiceTest.php @@ -0,0 +1,79 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->inventoryService = new InventoryService; + $this->cartService = new CartService($this->inventoryService); + $this->orderService = new OrderService; + $this->checkoutService = new CheckoutService( + new PricingEngine(new DiscountService, new ShippingCalculator, new TaxCalculator), + $this->inventoryService, + $this->orderService, + new MockPaymentProvider, + ); + + $product = Product::factory()->for($this->store)->create(); + $this->variant = ProductVariant::factory()->for($product)->create([ + 'price_amount' => 4500, + 'requires_shipping' => true, + ]); + InventoryItem::factory() + ->for($this->store) + ->for($this->variant, 'variant') + ->create(['quantity_on_hand' => 10]); + + $this->zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + 'regions_json' => [], + ]); + $this->rate = ShippingRate::factory()->for($this->zone, 'zone')->flat(599)->create(); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('records a payment with method and captured status on successful complete', function (): void { + $cart = $this->cartService->create($this->store); + $this->cartService->addLine($cart, $this->variant->id, 1); + $checkout = $this->checkoutService->start($cart->fresh()); + $this->checkoutService->setAddress($checkout, [ + 'email' => 'a@b.c', + 'shipping_address' => ['country' => 'DE'], + ]); + $this->checkoutService->setShippingMethod($checkout->fresh(), $this->rate->id); + $this->checkoutService->selectPaymentMethod($checkout->fresh(), 'credit_card'); + + $this->checkoutService->complete($checkout->fresh(), ['card_number' => '4242424242424242']); + + $payment = \App\Models\Payment::query()->first(); + expect($payment)->not->toBeNull() + ->and($payment->method)->toBe(PaymentMethod::CreditCard) + ->and($payment->status)->toBe(PaymentStatus::Captured) + ->and($payment->amount)->toBeGreaterThan(0) + ->and($payment->provider)->toBe('mock') + ->and($payment->provider_payment_id)->toStartWith('mock_'); +}); diff --git a/tests/Feature/Products/CollectionTest.php b/tests/Feature/Products/CollectionTest.php new file mode 100644 index 00000000..137e80b6 --- /dev/null +++ b/tests/Feature/Products/CollectionTest.php @@ -0,0 +1,64 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('creates a manual collection with products', function (): void { + $collection = Collection::factory()->for($this->store)->create(); + $productA = Product::factory()->for($this->store)->create(); + $productB = Product::factory()->for($this->store)->create(); + + $collection->products()->attach([ + $productA->id => ['position' => 0], + $productB->id => ['position' => 1], + ]); + + expect($collection->fresh()->products)->toHaveCount(2); +}); + +it('maintains product position in collection', function (): void { + $collection = Collection::factory()->for($this->store)->create(); + $productA = Product::factory()->for($this->store)->create(); + $productB = Product::factory()->for($this->store)->create(); + + $collection->products()->attach([ + $productA->id => ['position' => 1], + $productB->id => ['position' => 0], + ]); + + $ordered = $collection->products()->orderBy('collection_products.position')->get(); + + expect($ordered->first()->id)->toBe($productB->id) + ->and($ordered->last()->id)->toBe($productA->id); +}); + +it('scopes collections to current store', function (): void { + $storeA = $this->store; + $storeB = Store::factory()->create(); + + app()->instance('current_store', $storeA); + Collection::factory()->for($storeA)->create(['title' => 'Spring']); + + app()->instance('current_store', $storeB); + Collection::factory()->for($storeB)->create(['title' => 'Autumn']); + + expect(Collection::count())->toBe(1) + ->and(Collection::first()->title)->toBe('Autumn'); + + app()->instance('current_store', $storeA); + expect(Collection::count())->toBe(1) + ->and(Collection::first()->title)->toBe('Spring'); +}); diff --git a/tests/Feature/Products/InventoryTest.php b/tests/Feature/Products/InventoryTest.php new file mode 100644 index 00000000..a0df149e --- /dev/null +++ b/tests/Feature/Products/InventoryTest.php @@ -0,0 +1,89 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->inventory = new InventoryService; +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function makeInventoryItem(Store $store, int $onHand = 10, InventoryPolicy $policy = InventoryPolicy::Deny): InventoryItem +{ + $product = Product::factory()->for($store)->create(); + $variant = ProductVariant::factory()->for($product)->create(); + + return InventoryItem::factory() + ->for($store) + ->for($variant, 'variant') + ->create([ + 'quantity_on_hand' => $onHand, + 'policy' => $policy->value, + ]); +} + +it('reserves inventory when quantity is available', function (): void { + $item = makeInventoryItem($this->store, 10); + + $this->inventory->reserve($item, 3); + + expect($item->fresh()->quantity_reserved)->toBe(3) + ->and($item->fresh()->quantityAvailable())->toBe(7); +}); + +it('throws InsufficientInventoryException when policy is deny and insufficient', function (): void { + $item = makeInventoryItem($this->store, 2, InventoryPolicy::Deny); + + $this->inventory->reserve($item, 5); +})->throws(InsufficientInventoryException::class); + +it('allows over-selling when policy is continue', function (): void { + $item = makeInventoryItem($this->store, 2, InventoryPolicy::Continue); + + $this->inventory->reserve($item, 5); + + expect($item->fresh()->quantity_reserved)->toBe(5) + ->and($item->fresh()->quantityAvailable())->toBe(-3); +}); + +it('releases reserved inventory', function (): void { + $item = makeInventoryItem($this->store, 10); + $this->inventory->reserve($item, 4); + + $this->inventory->release($item->fresh(), 3); + + expect($item->fresh()->quantity_reserved)->toBe(1); +}); + +it('commits inventory on order completion', function (): void { + $item = makeInventoryItem($this->store, 10); + $this->inventory->reserve($item, 4); + + $this->inventory->commit($item->fresh(), 4); + + $fresh = $item->fresh(); + + expect($fresh->quantity_on_hand)->toBe(6) + ->and($fresh->quantity_reserved)->toBe(0); +}); + +it('restocks inventory', function (): void { + $item = makeInventoryItem($this->store, 10); + + $this->inventory->restock($item, 5); + + expect($item->fresh()->quantity_on_hand)->toBe(15); +}); diff --git a/tests/Feature/Products/MediaUploadTest.php b/tests/Feature/Products/MediaUploadTest.php new file mode 100644 index 00000000..53de3c32 --- /dev/null +++ b/tests/Feature/Products/MediaUploadTest.php @@ -0,0 +1,42 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('creates product media records', function (): void { + $product = Product::factory()->for($this->store)->create(); + + $media = ProductMedia::factory()->for($product)->create(); + + expect($media->fresh()) + ->product_id->toBe($product->id) + ->and($media->status)->toBe(MediaStatus::Processing); +}); + +it('marks media as ready after processing stub job', function (): void { + $product = Product::factory()->for($this->store)->create(); + $media = ProductMedia::factory()->for($product)->create([ + 'status' => MediaStatus::Processing->value, + ]); + + (new ProcessMediaUpload($media))->handle(); + + expect($media->fresh()->status)->toBe(MediaStatus::Ready); +}); diff --git a/tests/Feature/Products/ProductCrudTest.php b/tests/Feature/Products/ProductCrudTest.php new file mode 100644 index 00000000..e14f2b8b --- /dev/null +++ b/tests/Feature/Products/ProductCrudTest.php @@ -0,0 +1,105 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->service = new ProductService(new VariantMatrixService); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('creates a product with variants via ProductService', function (): void { + $product = $this->service->create($this->store, [ + 'title' => 'Linen Shirt', + 'status' => ProductStatus::Draft->value, + 'options' => [ + [ + 'name' => 'Size', + 'values' => ['S', 'M', 'L'], + ], + ], + ]); + + expect($product->variants)->toHaveCount(3) + ->and($product->handle)->toBe('linen-shirt') + ->and($product->options)->toHaveCount(1); +}); + +it('generates unique handles when title collides', function (): void { + $first = $this->service->create($this->store, ['title' => 'Classic Tee']); + $second = $this->service->create($this->store, ['title' => 'Classic Tee']); + $third = $this->service->create($this->store, ['title' => 'Classic Tee']); + + expect($first->handle)->toBe('classic-tee') + ->and($second->handle)->toBe('classic-tee-2') + ->and($third->handle)->toBe('classic-tee-3'); +}); + +it('transitions status from draft to active', function (): void { + $product = $this->service->create($this->store, [ + 'title' => 'Wool Coat', + 'status' => ProductStatus::Draft->value, + ]); + + $this->service->transitionStatus($product, ProductStatus::Active); + + expect($product->fresh()->status)->toBe(ProductStatus::Active); +}); + +it('rejects invalid status transitions', function (): void { + $product = $this->service->create($this->store, [ + 'title' => 'Bomber Jacket', + 'status' => ProductStatus::Active->value, + ]); + + $this->service->transitionStatus($product, ProductStatus::Draft); +})->throws(\InvalidArgumentException::class); + +it('prevents deletion of active products', function (): void { + $product = $this->service->create($this->store, [ + 'title' => 'Silk Scarf', + 'status' => ProductStatus::Active->value, + ]); + + $this->service->delete($product); +})->throws(\InvalidArgumentException::class); + +it('allows deletion of draft products', function (): void { + $product = $this->service->create($this->store, [ + 'title' => 'Cotton Socks', + 'status' => ProductStatus::Draft->value, + ]); + + $this->service->delete($product); + + expect(Product::withoutGlobalScopes()->find($product->id))->toBeNull(); +}); + +it('scopes products to the current store', function (): void { + $storeA = $this->store; + $storeB = Store::factory()->create(); + + app()->instance('current_store', $storeA); + $this->service->create($storeA, ['title' => 'Product A']); + + app()->instance('current_store', $storeB); + $this->service->create($storeB, ['title' => 'Product B']); + + expect(Product::count())->toBe(1) + ->and(Product::first()->title)->toBe('Product B'); + + app()->instance('current_store', $storeA); + expect(Product::count())->toBe(1) + ->and(Product::first()->title)->toBe('Product A'); +}); diff --git a/tests/Feature/Products/VariantTest.php b/tests/Feature/Products/VariantTest.php new file mode 100644 index 00000000..f07bbec5 --- /dev/null +++ b/tests/Feature/Products/VariantTest.php @@ -0,0 +1,79 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->service = new ProductService(new VariantMatrixService); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('builds a variant matrix from product options', function (): void { + $product = $this->service->create($this->store, [ + 'title' => 'Performance Tee', + 'status' => ProductStatus::Draft->value, + 'options' => [ + ['name' => 'Size', 'values' => ['S', 'M', 'L']], + ['name' => 'Color', 'values' => ['Red', 'Blue']], + ], + ]); + + expect($product->variants)->toHaveCount(6); + + foreach ($product->variants as $variant) { + expect($variant->optionValues)->toHaveCount(2); + } +}); + +it('archives orphaned variants when options change', function (): void { + $product = $this->service->create($this->store, [ + 'title' => 'Merino Cap', + 'options' => [ + ['name' => 'Size', 'values' => ['S', 'M']], + ], + ]); + + expect($product->variants)->toHaveCount(2); + + $this->service->update($product, [ + 'options' => [ + ['name' => 'Size', 'values' => ['M', 'L']], + ], + ]); + + $product = $product->fresh(['variants']); + + $active = $product->variants->where('status', VariantStatus::Active); + $archived = $product->variants->where('status', VariantStatus::Archived); + + expect($active)->toHaveCount(2) + ->and($archived)->toHaveCount(1); +}); + +it('stores prices as integers in minor units', function (): void { + $product = $this->service->create($this->store, [ + 'title' => 'Canvas Tote', + ]); + + $variant = $product->variants->first(); + $variant->price_amount = 1999; + $variant->compare_at_amount = 2499; + $variant->save(); + + $fresh = ProductVariant::find($variant->id); + + expect($fresh->price_amount)->toBe(1999) + ->and($fresh->compare_at_amount)->toBe(2499); +}); diff --git a/tests/Feature/Search/AutocompleteTest.php b/tests/Feature/Search/AutocompleteTest.php new file mode 100644 index 00000000..af02404a --- /dev/null +++ b/tests/Feature/Search/AutocompleteTest.php @@ -0,0 +1,36 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('returns empty collection for a short prefix', function (): void { + Product::factory()->for($this->store)->create(['title' => 'Amazing Product']); + + $results = app(SearchService::class)->autocomplete($this->store, 'A'); + + expect($results)->toHaveCount(0); +}); + +it('returns matches for a valid prefix', function (): void { + Product::factory()->for($this->store)->create(['title' => 'Arctic Parka']); + Product::factory()->for($this->store)->create(['title' => 'Aviator Jacket']); + Product::factory()->for($this->store)->create(['title' => 'Beach Towel']); + + $results = app(SearchService::class)->autocomplete($this->store, 'Ar'); + + expect($results)->toHaveCount(1) + ->and($results->first()->title)->toBe('Arctic Parka'); +}); diff --git a/tests/Feature/Search/SearchTest.php b/tests/Feature/Search/SearchTest.php new file mode 100644 index 00000000..d639e380 --- /dev/null +++ b/tests/Feature/Search/SearchTest.php @@ -0,0 +1,90 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('indexes a product on create', function (): void { + $product = Product::factory()->for($this->store)->create([ + 'title' => 'Arctic Wool Coat', + ]); + + $results = app(SearchService::class)->search($this->store, 'Arctic'); + + expect($results->total())->toBe(1) + ->and($results->first()->id)->toBe($product->id); +}); + +it('removes a product from index on delete', function (): void { + $product = Product::factory()->for($this->store)->create([ + 'title' => 'Desert Sandals', + ]); + + expect(app(SearchService::class)->search($this->store, 'Desert')->total())->toBe(1); + + $product->delete(); + + expect(app(SearchService::class)->search($this->store, 'Desert')->total())->toBe(0); +}); + +it('finds products by title', function (): void { + Product::factory()->for($this->store)->create(['title' => 'Blue Leather Wallet']); + Product::factory()->for($this->store)->create(['title' => 'Red Canvas Bag']); + + $results = app(SearchService::class)->search($this->store, 'Leather'); + + expect($results->total())->toBe(1) + ->and($results->first()->title)->toBe('Blue Leather Wallet'); +}); + +it('finds products by partial (prefix) match', function (): void { + Product::factory()->for($this->store)->create(['title' => 'Mountaineer Jacket']); + + $results = app(SearchService::class)->search($this->store, 'Mount'); + + expect($results->total())->toBe(1); +}); + +it('scopes results to the current store', function (): void { + $otherStore = Store::factory()->create(); + + Product::factory()->for($this->store)->create(['title' => 'Shared Title']); + Product::factory()->for($otherStore)->create(['title' => 'Shared Title']); + + $results = app(SearchService::class)->search($this->store, 'Shared'); + + expect($results->total())->toBe(1) + ->and($results->first()->store_id)->toBe($this->store->id); +}); + +it('returns no results for an empty query', function (): void { + Product::factory()->for($this->store)->create(['title' => 'Anything']); + + $results = app(SearchService::class)->search($this->store, ''); + + expect($results->total())->toBe(0); +}); + +it('excludes non-active products from search results', function (): void { + Product::factory()->for($this->store)->create([ + 'title' => 'Hidden Draft', + 'status' => ProductStatus::Draft->value, + ]); + + $results = app(SearchService::class)->search($this->store, 'Hidden'); + + expect($results->total())->toBe(0); +}); diff --git a/tests/Feature/Storefront/AccountTest.php b/tests/Feature/Storefront/AccountTest.php new file mode 100644 index 00000000..6e52c15d --- /dev/null +++ b/tests/Feature/Storefront/AccountTest.php @@ -0,0 +1,87 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->customer = Customer::factory()->for($this->store)->create([ + 'name' => 'Grace Hopper', + 'email' => 'grace@example.com', + ]); + + Auth::guard('customer')->login($this->customer); +}); + +afterEach(function (): void { + Auth::guard('customer')->logout(); + app()->forgetInstance('current_store'); +}); + +it('renders the account dashboard for a logged-in customer', function (): void { + Livewire::test(Dashboard::class) + ->assertStatus(200) + ->assertSee('Grace Hopper') + ->assertSee('grace@example.com'); +}); + +it('lists the customer orders', function (): void { + Order::factory()->for($this->store)->create([ + 'customer_id' => $this->customer->id, + 'order_number' => '#3001', + ]); + Order::factory()->for($this->store)->create([ + 'customer_id' => $this->customer->id, + 'order_number' => '#3002', + ]); + + Livewire::test(OrdersIndex::class) + ->assertStatus(200) + ->assertSee('#3001') + ->assertSee('#3002'); +}); + +it('shows a single customer order', function (): void { + Order::factory()->for($this->store)->create([ + 'customer_id' => $this->customer->id, + 'order_number' => '#4001', + 'email' => 'grace@example.com', + ]); + + Livewire::test(OrdersShow::class, ['orderNumber' => '4001']) + ->assertStatus(200) + ->assertSee('#4001'); +}); + +it('supports adding and deleting addresses', function (): void { + $component = Livewire::test(AddressesIndex::class) + ->set('label', 'Work') + ->set('firstName', 'Grace') + ->set('lastName', 'Hopper') + ->set('line1', 'Harvard Yard') + ->set('city', 'Cambridge') + ->set('postalCode', '02138') + ->set('country', 'US') + ->call('addAddress') + ->assertHasNoErrors(); + + expect(CustomerAddress::query()->where('customer_id', $this->customer->id)->count())->toBe(1); + + $addressId = CustomerAddress::query()->where('customer_id', $this->customer->id)->value('id'); + $component->call('deleteAddress', $addressId); + + expect(CustomerAddress::query()->where('customer_id', $this->customer->id)->count())->toBe(0); +}); diff --git a/tests/Feature/Storefront/CartTest.php b/tests/Feature/Storefront/CartTest.php new file mode 100644 index 00000000..2b8d3f2d --- /dev/null +++ b/tests/Feature/Storefront/CartTest.php @@ -0,0 +1,72 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function seedCartWithOneLine(Store $store): array +{ + $product = Product::factory()->for($store)->create(['title' => 'Ceramic Mug']); + $variant = ProductVariant::factory()->for($product)->create(['price_amount' => 1800]); + InventoryItem::factory() + ->for($store) + ->for($variant, 'variant') + ->create(['quantity_on_hand' => 10]); + + $cart = CartSession::getOrCreate($store); + app(CartService::class)->addLine($cart, $variant->id, 1); + + return [$cart, $variant]; +} + +it('renders an empty cart state', function (): void { + Livewire::test(Show::class) + ->assertStatus(200) + ->assertSee('Your cart is empty'); +}); + +it('renders cart lines with totals', function (): void { + [$cart, $variant] = seedCartWithOneLine($this->store); + + Livewire::test(Show::class) + ->assertStatus(200) + ->assertSee('Ceramic Mug'); +}); + +it('updates the quantity of a cart line', function (): void { + [$cart, $variant] = seedCartWithOneLine($this->store); + $line = $cart->lines()->first(); + + Livewire::test(Show::class) + ->call('updateQty', $line->id, 3) + ->assertHasNoErrors(); + + expect((int) $cart->lines()->first()->quantity)->toBe(3); +}); + +it('removes a cart line', function (): void { + [$cart, $variant] = seedCartWithOneLine($this->store); + $line = $cart->lines()->first(); + + Livewire::test(Show::class) + ->call('removeLine', $line->id); + + expect($cart->lines()->count())->toBe(0); +}); diff --git a/tests/Feature/Storefront/CheckoutTest.php b/tests/Feature/Storefront/CheckoutTest.php new file mode 100644 index 00000000..5b63048b --- /dev/null +++ b/tests/Feature/Storefront/CheckoutTest.php @@ -0,0 +1,118 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $product = Product::factory()->for($this->store)->create(['title' => 'Cotton Tee']); + $this->variant = ProductVariant::factory()->for($product)->create([ + 'price_amount' => 2500, + 'weight_g' => 300, + 'requires_shipping' => true, + ]); + InventoryItem::factory() + ->for($this->store) + ->for($this->variant, 'variant') + ->create(['quantity_on_hand' => 10]); + + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + 'regions_json' => [], + ]); + $this->rate = ShippingRate::factory()->for($zone, 'zone')->flat(599)->create(); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function seedCheckoutCart(Store $store, ProductVariant $variant): void +{ + $cart = CartSession::getOrCreate($store); + app(CartService::class)->addLine($cart, $variant->id, 2); +} + +it('redirects to the cart when the cart is empty', function (): void { + Livewire::test(Show::class) + ->assertRedirect(route('storefront.cart.show')); +}); + +it('completes a credit card checkout end-to-end', function (): void { + seedCheckoutCart($this->store, $this->variant); + + Livewire::test(Show::class) + ->set('email', 'buyer@example.com') + ->set('shippingAddress.first_name', 'Jane') + ->set('shippingAddress.last_name', 'Doe') + ->set('shippingAddress.line1', 'Karl-Marx-Allee 1') + ->set('shippingAddress.city', 'Berlin') + ->set('shippingAddress.postal_code', '10178') + ->set('shippingAddress.country', 'DE') + ->call('continueToShipping') + ->assertHasNoErrors() + ->set('shippingMethodId', $this->rate->id) + ->call('continueToPayment') + ->assertHasNoErrors() + ->set('paymentMethod', 'credit_card') + ->call('placeOrder') + ->assertHasNoErrors(); + + $order = Order::query()->latest('id')->first(); + expect($order)->not->toBeNull() + ->and($order->financial_status)->toBe(FinancialStatus::Paid) + ->and($order->email)->toBe('buyer@example.com'); + + expect(CartSession::current())->toBeNull(); +}); + +it('leaves the order pending for bank transfer', function (): void { + seedCheckoutCart($this->store, $this->variant); + + Livewire::test(Show::class) + ->set('email', 'bank@example.com') + ->set('shippingAddress.first_name', 'Max') + ->set('shippingAddress.last_name', 'Muster') + ->set('shippingAddress.line1', 'Hauptstr 1') + ->set('shippingAddress.city', 'Berlin') + ->set('shippingAddress.postal_code', '10115') + ->set('shippingAddress.country', 'DE') + ->call('continueToShipping') + ->set('shippingMethodId', $this->rate->id) + ->call('continueToPayment') + ->set('paymentMethod', 'bank_transfer') + ->call('placeOrder') + ->assertHasNoErrors(); + + $order = Order::query()->latest('id')->first(); + expect($order)->not->toBeNull() + ->and($order->financial_status)->toBe(FinancialStatus::Pending); +}); + +it('renders the confirmation page for an order', function (): void { + $order = Order::factory()->for($this->store)->create([ + 'order_number' => '#2001', + 'email' => 'confirm@example.com', + ]); + + Livewire::test(Confirmation::class, ['order_number' => '2001']) + ->assertStatus(200) + ->assertSee('Thank you') + ->assertSee('#2001'); +}); diff --git a/tests/Feature/Storefront/CollectionsTest.php b/tests/Feature/Storefront/CollectionsTest.php new file mode 100644 index 00000000..ba5af8d3 --- /dev/null +++ b/tests/Feature/Storefront/CollectionsTest.php @@ -0,0 +1,55 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('renders the collection index with active collections', function (): void { + $collection = Collection::factory()->for($this->store)->create([ + 'title' => 'Summer Picks', + 'handle' => 'summer-picks', + ]); + + Livewire::test(Index::class) + ->assertStatus(200) + ->assertSee('All collections') + ->assertSee('Summer Picks'); + + expect(Collection::query()->count())->toBe(1); +}); + +it('renders a collection detail with its products', function (): void { + $collection = Collection::factory()->for($this->store)->create([ + 'title' => 'Essentials', + 'handle' => 'essentials', + ]); + + $product = Product::factory()->for($this->store)->create(['title' => 'Linen Shirt']); + ProductVariant::factory()->for($product)->create(['price_amount' => 4999]); + $collection->products()->attach($product->id, ['position' => 0]); + + Livewire::test(Show::class, ['handle' => 'essentials']) + ->assertStatus(200) + ->assertSee('Essentials') + ->assertSee('Linen Shirt'); +}); + +it('aborts 404 for missing collection handle', function (): void { + Livewire::test(Show::class, ['handle' => 'missing']); +})->throws(Illuminate\Database\Eloquent\ModelNotFoundException::class); diff --git a/tests/Feature/Storefront/HomeTest.php b/tests/Feature/Storefront/HomeTest.php new file mode 100644 index 00000000..7df19d5b --- /dev/null +++ b/tests/Feature/Storefront/HomeTest.php @@ -0,0 +1,31 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('renders the storefront home Livewire component', function (): void { + Livewire::test(Home::class) + ->assertStatus(200) + ->assertSee('Thoughtfully made') + ->assertSee('New arrivals'); +}); + +it('responds from the / route with a 200', function (): void { + $response = $this->get('/'); + + $response->assertOk(); + $response->assertSee('Thoughtfully made', false); +}); diff --git a/tests/Feature/Storefront/PageShowTest.php b/tests/Feature/Storefront/PageShowTest.php new file mode 100644 index 00000000..aa2f5380 --- /dev/null +++ b/tests/Feature/Storefront/PageShowTest.php @@ -0,0 +1,37 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('renders a published page body', function (): void { + Page::factory()->for($this->store)->create([ + 'title' => 'About Us', + 'handle' => 'about', + 'body_html' => '

We make good things.

', + ]); + + Livewire::test(Show::class, ['handle' => 'about']) + ->assertStatus(200) + ->assertSee('About Us') + ->assertSee('We make good things.', false); +}); + +it('fails for a draft page', function (): void { + Page::factory()->for($this->store)->draft()->create(['handle' => 'hidden']); + + Livewire::test(Show::class, ['handle' => 'hidden']); +})->throws(Illuminate\Database\Eloquent\ModelNotFoundException::class); diff --git a/tests/Feature/Storefront/ProductDetailTest.php b/tests/Feature/Storefront/ProductDetailTest.php new file mode 100644 index 00000000..3e384fd2 --- /dev/null +++ b/tests/Feature/Storefront/ProductDetailTest.php @@ -0,0 +1,70 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('renders a product with its variants', function (): void { + $product = Product::factory()->for($this->store)->create([ + 'title' => 'Wool Coat', + 'handle' => 'wool-coat', + ]); + ProductVariant::factory()->for($product)->create(['price_amount' => 12999]); + + Livewire::test(Show::class, ['handle' => 'wool-coat']) + ->assertStatus(200) + ->assertSee('Wool Coat') + ->assertSee('Add to cart'); +}); + +it('adds the selected variant to the cart', function (): void { + $product = Product::factory()->for($this->store)->create([ + 'title' => 'Canvas Bag', + 'handle' => 'canvas-bag', + ]); + $variant = ProductVariant::factory()->for($product)->create(['price_amount' => 3200]); + InventoryItem::factory() + ->for($this->store) + ->for($variant, 'variant') + ->create(['quantity_on_hand' => 5]); + + Livewire::test(Show::class, ['handle' => 'canvas-bag']) + ->set('quantity', 2) + ->call('addToCart') + ->assertHasNoErrors(); + + $cart = CartSession::current(); + expect($cart)->not->toBeNull() + ->and($cart->lines()->count())->toBe(1) + ->and((int) $cart->lines()->first()->quantity)->toBe(2); +}); + +it('shows an error when inventory is insufficient', function (): void { + $product = Product::factory()->for($this->store)->create(['handle' => 'rare-item']); + $variant = ProductVariant::factory()->for($product)->create(['price_amount' => 1000]); + InventoryItem::factory() + ->for($this->store) + ->for($variant, 'variant') + ->create(['quantity_on_hand' => 1]); + + Livewire::test(Show::class, ['handle' => 'rare-item']) + ->set('quantity', 5) + ->call('addToCart') + ->assertHasErrors('cart'); +}); diff --git a/tests/Feature/Tenancy/StoreIsolationTest.php b/tests/Feature/Tenancy/StoreIsolationTest.php new file mode 100644 index 00000000..2a15748a --- /dev/null +++ b/tests/Feature/Tenancy/StoreIsolationTest.php @@ -0,0 +1,61 @@ +id(); + $table->foreignId('store_id')->constrained('stores')->cascadeOnDelete(); + $table->string('name'); + $table->timestamps(); + }); +}); + +afterEach(function (): void { + Schema::dropIfExists('widgets'); + app()->forgetInstance('current_store'); +}); + +it('applies store scope to models using BelongsToStore', function (): void { + $storeA = Store::factory()->create(); + $storeB = Store::factory()->create(); + + app()->instance('current_store', $storeA); + Widget::create(['name' => 'A1']); + Widget::create(['name' => 'A2']); + + app()->instance('current_store', $storeB); + Widget::create(['name' => 'B1']); + + app()->instance('current_store', $storeA); + expect(Widget::count())->toBe(2) + ->and(Widget::pluck('name')->all())->toEqual(['A1', 'A2']); + + app()->instance('current_store', $storeB); + expect(Widget::count())->toBe(1) + ->and(Widget::pluck('name')->all())->toEqual(['B1']); +}); + +it('auto-assigns store_id on creating when current store is bound', function (): void { + $store = Store::factory()->create(); + app()->instance('current_store', $store); + + $widget = Widget::create(['name' => 'auto']); + + expect($widget->store_id)->toBe($store->id); +}); + +class Widget extends Model +{ + use BelongsToStore; + + protected $table = 'widgets'; + + protected $fillable = ['name', 'store_id']; +} diff --git a/tests/Feature/Tenancy/TenantResolutionTest.php b/tests/Feature/Tenancy/TenantResolutionTest.php new file mode 100644 index 00000000..3261d076 --- /dev/null +++ b/tests/Feature/Tenancy/TenantResolutionTest.php @@ -0,0 +1,63 @@ +json([ + 'id' => $store->id, + 'handle' => $store->handle, + ]); + })->middleware(ResolveStore::class.':storefront'); +}); + +it('resolves store from hostname via store_domains', function (): void { + $store = Store::factory()->create(); + StoreDomain::factory()->for($store)->create(['hostname' => 'shop.example.test']); + + $response = $this->get('http://shop.example.test/__resolve_probe'); + + $response->assertOk() + ->assertJson([ + 'id' => $store->id, + 'handle' => $store->handle, + ]); +}); + +it('returns 404 for unknown hostnames', function (): void { + $response = $this->get('http://unknown.example.test/__resolve_probe'); + + $response->assertNotFound(); +}); + +it('returns 503 for suspended stores', function (): void { + $store = Store::factory()->suspended()->create(); + StoreDomain::factory()->for($store)->create(['hostname' => 'suspended.example.test']); + + $response = $this->get('http://suspended.example.test/__resolve_probe'); + + $response->assertStatus(503); +}); + +it('caches hostname lookup', function (): void { + $store = Store::factory()->create(); + StoreDomain::factory()->for($store)->create(['hostname' => 'cached.example.test']); + + $this->get('http://cached.example.test/__resolve_probe')->assertOk(); + + $cached = Cache::get('store_domain:cached.example.test'); + + expect($cached)->toBe($store->id); +}); diff --git a/tests/Feature/Themes/ThemeSettingsTest.php b/tests/Feature/Themes/ThemeSettingsTest.php new file mode 100644 index 00000000..862812e5 --- /dev/null +++ b/tests/Feature/Themes/ThemeSettingsTest.php @@ -0,0 +1,57 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->service = app(ThemeSettingsService::class); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); + Cache::flush(); +}); + +it('returns default settings for a store without a published theme', function (): void { + $settings = $this->service->forStore($this->store); + + expect($settings)->toMatchArray([ + 'colors' => ['primary' => '#111'], + 'announcement' => null, + 'footer_text' => '(c) Shop', + ]); +}); + +it('returns theme settings_json for a store with a published theme', function (): void { + $theme = Theme::factory()->published()->create(['store_id' => $this->store->id]); + + ThemeSettings::create([ + 'theme_id' => $theme->id, + 'settings_json' => [ + 'colors' => ['primary' => '#ff0066'], + 'announcement' => 'Free shipping over $50', + 'footer_text' => '(c) 2026 Test Store', + ], + ]); + + $settings = $this->service->forStore($this->store); + + expect($settings['colors']['primary'])->toBe('#ff0066') + ->and($settings['announcement'])->toBe('Free shipping over $50') + ->and($settings['footer_text'])->toBe('(c) 2026 Test Store'); +}); + +it('caches theme settings per store', function (): void { + $this->service->forStore($this->store); + + expect(Cache::has("theme:settings:store:{$this->store->id}"))->toBeTrue(); +}); diff --git a/tests/Feature/Themes/ThemeTest.php b/tests/Feature/Themes/ThemeTest.php new file mode 100644 index 00000000..60e416a9 --- /dev/null +++ b/tests/Feature/Themes/ThemeTest.php @@ -0,0 +1,66 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('creates a theme via factory', function (): void { + $theme = Theme::factory()->create(['store_id' => $this->store->id]); + + expect($theme->exists)->toBeTrue() + ->and($theme->status)->toBe(ThemeStatus::Draft) + ->and($theme->store_id)->toBe($this->store->id); +}); + +it('publishes a theme and sets published_at', function (): void { + $theme = Theme::factory()->create(['store_id' => $this->store->id]); + + $theme->update([ + 'status' => ThemeStatus::Published, + 'published_at' => now(), + ]); + + $theme->refresh(); + + expect($theme->status)->toBe(ThemeStatus::Published) + ->and($theme->published_at)->not->toBeNull(); +}); + +it('scopes themes to the current store', function (): void { + $storeA = $this->store; + $storeB = Store::factory()->create(); + + Theme::factory()->create(['store_id' => $storeA->id]); + + app()->instance('current_store', $storeB); + Theme::factory()->create(['store_id' => $storeB->id]); + + expect(Theme::count())->toBe(1) + ->and(Theme::first()->store_id)->toBe($storeB->id); + + app()->instance('current_store', $storeA); + expect(Theme::count())->toBe(1) + ->and(Theme::first()->store_id)->toBe($storeA->id); +}); + +it('rejects invalid status values at the database level', function (): void { + expect(fn () => \DB::table('themes')->insert([ + 'store_id' => $this->store->id, + 'name' => 'Broken', + 'status' => 'garbage', + 'created_at' => now(), + 'updated_at' => now(), + ]))->toThrow(Exception::class); +}); diff --git a/tests/Feature/Webhooks/WebhookDeliveryTest.php b/tests/Feature/Webhooks/WebhookDeliveryTest.php new file mode 100644 index 00000000..e1aa2514 --- /dev/null +++ b/tests/Feature/Webhooks/WebhookDeliveryTest.php @@ -0,0 +1,102 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('dispatches a delivery job for each active subscription', function (): void { + Queue::fake(); + + WebhookSubscription::factory()->for($this->store)->create([ + 'event_type' => 'order.created', + 'status' => 'active', + ]); + WebhookSubscription::factory()->for($this->store)->create([ + 'event_type' => 'order.created', + 'status' => 'active', + ]); + WebhookSubscription::factory()->for($this->store)->create([ + 'event_type' => 'order.created', + 'status' => 'paused', + ]); + WebhookSubscription::factory()->for($this->store)->create([ + 'event_type' => 'order.paid', + 'status' => 'active', + ]); + + app(WebhookService::class)->dispatch($this->store, 'order.created', ['order_id' => 1]); + + Queue::assertPushed(DeliverWebhook::class, 2); +}); + +it('records a successful delivery row', function (): void { + Http::fake([ + '*' => Http::response(['ok' => true], 200), + ]); + + $subscription = WebhookSubscription::factory()->for($this->store)->create([ + 'event_type' => 'order.created', + 'url' => 'https://example.test/webhooks/orders', + 'secret' => 'shh', + 'status' => 'active', + ]); + + (new DeliverWebhook($subscription, 'order.created', ['order_id' => 1])) + ->handle(app(WebhookService::class)); + + $delivery = WebhookDelivery::query()->first(); + + expect($delivery)->not->toBeNull() + ->and($delivery->event_type)->toBe('order.created') + ->and($delivery->response_status)->toBe(200) + ->and($delivery->delivered_at)->not->toBeNull(); + + Http::assertSent(function ($request) { + return $request->hasHeader('X-Platform-Signature') + && $request->hasHeader('X-Platform-Event', 'order.created') + && $request->hasHeader('X-Platform-Delivery-Id') + && $request->hasHeader('X-Platform-Timestamp'); + }); +}); + +it('pauses a subscription after five consecutive failures', function (): void { + Http::fake([ + '*' => Http::response('server down', 500), + ]); + + $subscription = WebhookSubscription::factory()->for($this->store)->create([ + 'event_type' => 'order.created', + 'url' => 'https://example.test/broken', + 'secret' => 'shh', + 'status' => 'active', + 'failed_count' => 4, + ]); + + try { + (new DeliverWebhook($subscription, 'order.created', ['x' => 1])) + ->handle(app(WebhookService::class)); + } catch (\Throwable $exception) { + // expected failure from non-2xx response + } + + $subscription->refresh(); + + expect($subscription->failed_count)->toBe(5) + ->and($subscription->status)->toBe('paused'); +}); diff --git a/tests/Feature/Webhooks/WebhookSignatureTest.php b/tests/Feature/Webhooks/WebhookSignatureTest.php new file mode 100644 index 00000000..ed51a5ae --- /dev/null +++ b/tests/Feature/Webhooks/WebhookSignatureTest.php @@ -0,0 +1,25 @@ +sign('{"hello":"world"}', 'secret'); + + expect($signature)->toStartWith('sha256=') + ->and(strlen($signature))->toBe(7 + 64); +}); + +it('verifies a valid signature', function (): void { + $service = app(WebhookService::class); + $payload = '{"a":1}'; + $signature = $service->sign($payload, 'topsecret'); + + expect($service->verify($payload, $signature, 'topsecret'))->toBeTrue(); +}); + +it('rejects an invalid signature', function (): void { + $service = app(WebhookService::class); + $payload = '{"a":1}'; + + expect($service->verify($payload, 'sha256=bogus', 'topsecret'))->toBeFalse(); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 60f04a45..e0cb22d6 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -15,6 +15,8 @@ // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); +pest()->extend(Tests\TestCase::class)->in('Unit'); + /* |-------------------------------------------------------------------------- | Expectations @@ -41,7 +43,25 @@ | */ -function something() +/** + * @return array{0: \App\Models\User, 1: \App\Models\Store} + */ +function loginAsAdmin(): array { - // .. + $org = \App\Models\Organization::factory()->create(); + $store = \App\Models\Store::factory()->for($org)->create(); + $user = \App\Models\User::factory()->create(); + + \Illuminate\Support\Facades\DB::table('store_users')->insert([ + 'store_id' => $store->id, + 'user_id' => $user->id, + 'role' => 'owner', + 'created_at' => now(), + ]); + + \Illuminate\Support\Facades\Auth::guard('web')->login($user); + session(['current_store_id' => $store->id]); + app()->instance('current_store', $store); + + return [$user, $store]; } diff --git a/tests/Unit/CartVersionTest.php b/tests/Unit/CartVersionTest.php new file mode 100644 index 00000000..b8fee1c3 --- /dev/null +++ b/tests/Unit/CartVersionTest.php @@ -0,0 +1,60 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->service = new CartService(new InventoryService); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +it('starts at version 1', function (): void { + $cart = $this->service->create($this->store); + expect($cart->cart_version)->toBe(1); +}); + +it('increments on add line', function (): void { + $cart = $this->service->create($this->store); + $variant = ProductVariant::factory() + ->for(Product::factory()->for($this->store)) + ->create(['price_amount' => 1000]); + + $this->service->addLine($cart, $variant->id, 1); + + expect($cart->fresh()->cart_version)->toBe(2); +}); + +it('increments on update quantity', function (): void { + $cart = $this->service->create($this->store); + $variant = ProductVariant::factory() + ->for(Product::factory()->for($this->store)) + ->create(['price_amount' => 1000]); + $line = $this->service->addLine($cart, $variant->id, 1); + + $this->service->updateLineQuantity($cart->fresh(), $line->id, 3); + + expect($cart->fresh()->cart_version)->toBe(3); +}); + +it('increments on remove line', function (): void { + $cart = $this->service->create($this->store); + $variant = ProductVariant::factory() + ->for(Product::factory()->for($this->store)) + ->create(['price_amount' => 1000]); + $line = $this->service->addLine($cart, $variant->id, 1); + + $this->service->removeLine($cart->fresh(), $line->id); + + expect($cart->fresh()->cart_version)->toBe(3); +}); diff --git a/tests/Unit/Discounts/DiscountCalculatorTest.php b/tests/Unit/Discounts/DiscountCalculatorTest.php new file mode 100644 index 00000000..6bccfcd3 --- /dev/null +++ b/tests/Unit/Discounts/DiscountCalculatorTest.php @@ -0,0 +1,167 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->service = new DiscountService; + $this->cart = Cart::factory()->for($this->store)->create(); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function makeCartLine(Cart $cart, int $price = 2000, int $qty = 1): CartLine +{ + $variant = ProductVariant::factory() + ->for(Product::factory()->for($cart->store)) + ->create(['price_amount' => $price]); + + return CartLine::factory()->for($cart)->for($variant, 'variant')->create([ + 'quantity' => $qty, + 'unit_price_amount' => $price, + 'line_subtotal_amount' => $price * $qty, + 'line_total_amount' => $price * $qty, + ]); +} + +it('validates an active code', function (): void { + $discount = Discount::factory()->for($this->store)->create(['code' => 'SAVE10']); + makeCartLine($this->cart); + + $result = $this->service->validate('SAVE10', $this->store, $this->cart->fresh('lines')); + + expect($result->id)->toBe($discount->id); +}); + +it('rejects expired discount', function (): void { + Discount::factory()->for($this->store)->expired()->create(['code' => 'OLD']); + + expect(fn () => $this->service->validate('OLD', $this->store, $this->cart)) + ->toThrow(InvalidDiscountException::class); +}); + +it('rejects not yet active discount', function (): void { + Discount::factory()->for($this->store)->notYetActive()->create(['code' => 'FUTURE']); + + $this->service->validate('FUTURE', $this->store, $this->cart); +})->throws(InvalidDiscountException::class, 'not yet active'); + +it('rejects usage limit reached', function (): void { + Discount::factory()->for($this->store)->create([ + 'code' => 'LIMITED', + 'usage_limit' => 5, + 'usage_count' => 5, + ]); + + $this->service->validate('LIMITED', $this->store, $this->cart); +})->throws(InvalidDiscountException::class, 'usage limit'); + +it('rejects unknown code', function (): void { + $this->service->validate('NOPE', $this->store, $this->cart); +})->throws(InvalidDiscountException::class, 'not found'); + +it('performs case insensitive lookup', function (): void { + Discount::factory()->for($this->store)->create(['code' => 'SaveMe']); + makeCartLine($this->cart); + + $result = $this->service->validate('SAVEME', $this->store, $this->cart->fresh('lines')); + + expect($result->code)->toBe('SaveMe'); +}); + +it('enforces minimum purchase rule', function (): void { + Discount::factory()->for($this->store)->create([ + 'code' => 'BIG', + 'rules_json' => ['min_purchase_amount' => 10000], + ]); + makeCartLine($this->cart, price: 1000, qty: 1); + + $this->service->validate('BIG', $this->store, $this->cart->fresh('lines')); +})->throws(InvalidDiscountException::class, 'minimum'); + +it('passes when minimum purchase is met', function (): void { + $discount = Discount::factory()->for($this->store)->create([ + 'code' => 'BIG2', + 'rules_json' => ['min_purchase_amount' => 1000], + ]); + makeCartLine($this->cart, price: 2000, qty: 1); + + $result = $this->service->validate('BIG2', $this->store, $this->cart->fresh('lines')); + + expect($result->id)->toBe($discount->id); +}); + +it('rejects disabled discount', function (): void { + Discount::factory()->for($this->store)->disabled()->create(['code' => 'OFF']); + + $this->service->validate('OFF', $this->store, $this->cart); +})->throws(InvalidDiscountException::class); + +it('calculates percent discount amount', function (): void { + $discount = Discount::factory()->for($this->store)->percent10()->create(['code' => 'P10']); + makeCartLine($this->cart, price: 10000, qty: 1); + $lines = $this->cart->fresh('lines')->lines; + + $result = $this->service->calculate($discount, 10000, $lines); + + expect($result->amount)->toBe(1000); +}); + +it('calculates fixed discount amount', function (): void { + $discount = Discount::factory()->for($this->store)->fixed500()->create(['code' => 'F500']); + makeCartLine($this->cart, price: 10000, qty: 1); + $lines = $this->cart->fresh('lines')->lines; + + $result = $this->service->calculate($discount, 10000, $lines); + + expect($result->amount)->toBe(500); +}); + +it('caps fixed discount at subtotal', function (): void { + $discount = Discount::factory()->for($this->store)->fixed500()->create(['code' => 'F500B']); + makeCartLine($this->cart, price: 300, qty: 1); + $lines = $this->cart->fresh('lines')->lines; + + $result = $this->service->calculate($discount, 300, $lines); + + expect($result->amount)->toBe(300); +}); + +it('flags free shipping discount', function (): void { + $discount = Discount::factory()->for($this->store)->freeShipping()->create(['code' => 'FREESHIP']); + makeCartLine($this->cart, price: 5000); + $lines = $this->cart->fresh('lines')->lines; + + $result = $this->service->calculate($discount, 5000, $lines); + + expect($result->freeShipping)->toBeTrue() + ->and($result->amount)->toBe(0); +}); + +it('allocates proportionally across lines with remainder on last', function (): void { + $discount = Discount::factory()->for($this->store)->percent10()->create(['code' => 'P10B']); + $line1 = makeCartLine($this->cart, price: 3333, qty: 1); + $line2 = makeCartLine($this->cart, price: 3333, qty: 1); + $line3 = makeCartLine($this->cart, price: 3334, qty: 1); + $subtotal = 10000; + $lines = $this->cart->fresh('lines')->lines; + + $result = $this->service->calculate($discount, $subtotal, $lines); + + expect($result->amount)->toBe(1000) + ->and(array_sum($result->allocations))->toBe(1000) + ->and($result->allocations)->toHaveCount(3); +}); diff --git a/tests/Unit/Pricing/PricingEngineTest.php b/tests/Unit/Pricing/PricingEngineTest.php new file mode 100644 index 00000000..e0b2f4bb --- /dev/null +++ b/tests/Unit/Pricing/PricingEngineTest.php @@ -0,0 +1,206 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + + $this->engine = new PricingEngine( + new DiscountService, + new ShippingCalculator, + new TaxCalculator, + ); + + $this->cart = Cart::factory()->for($this->store)->create(['currency' => 'EUR']); +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function addLine(Cart $cart, int $price, int $qty = 1, int $weight = 250, bool $requiresShipping = true): CartLine +{ + $variant = ProductVariant::factory() + ->for(Product::factory()->for($cart->store)) + ->create([ + 'price_amount' => $price, + 'weight_g' => $weight, + 'requires_shipping' => $requiresShipping, + ]); + + return CartLine::factory()->for($cart)->for($variant, 'variant')->create([ + 'quantity' => $qty, + 'unit_price_amount' => $price, + 'line_subtotal_amount' => $price * $qty, + 'line_total_amount' => $price * $qty, + ]); +} + +it('calculates subtotal from line items', function (): void { + addLine($this->cart, 2499, 2); + addLine($this->cart, 7999, 1); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create(); + + $result = $this->engine->calculate($checkout); + + expect($result->subtotal)->toBe(12997) + ->and($result->total)->toBe(12997); +}); + +it('applies percent discount', function (): void { + addLine($this->cart, 10000, 1); + Discount::factory()->for($this->store)->percent10()->create(['code' => 'P10']); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create([ + 'discount_code' => 'P10', + ]); + + $result = $this->engine->calculate($checkout); + + expect($result->subtotal)->toBe(10000) + ->and($result->discount)->toBe(1000) + ->and($result->total)->toBe(9000); +}); + +it('applies fixed discount', function (): void { + addLine($this->cart, 10000, 1); + Discount::factory()->for($this->store)->fixed500()->create(['code' => 'F500']); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create([ + 'discount_code' => 'F500', + ]); + + $result = $this->engine->calculate($checkout); + + expect($result->discount)->toBe(500) + ->and($result->total)->toBe(9500); +}); + +it('caps fixed discount at subtotal', function (): void { + addLine($this->cart, 300, 1); + Discount::factory()->for($this->store)->fixed500()->create(['code' => 'F500B']); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create([ + 'discount_code' => 'F500B', + ]); + + $result = $this->engine->calculate($checkout); + + expect($result->discount)->toBe(300) + ->and($result->total)->toBe(0); +}); + +it('applies free shipping discount', function (): void { + addLine($this->cart, 5000, 1); + Discount::factory()->for($this->store)->freeShipping()->create(['code' => 'FREESHIP']); + + $zone = ShippingZone::factory()->for($this->store)->create(['countries_json' => ['DE']]); + $rate = ShippingRate::factory()->for($zone, 'zone')->flat(499)->create(); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create([ + 'discount_code' => 'FREESHIP', + 'shipping_method_id' => $rate->id, + ]); + + $result = $this->engine->calculate($checkout); + + expect($result->shipping)->toBe(0) + ->and($result->freeShippingApplied)->toBeTrue() + ->and($result->total)->toBe(5000); +}); + +it('calculates tax exclusive', function (): void { + addLine($this->cart, 10000, 1); + TaxSettings::factory()->for($this->store)->create(); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create(); + + $result = $this->engine->calculate($checkout); + + expect($result->subtotal)->toBe(10000) + ->and($result->taxTotal)->toBe(1900) + ->and($result->total)->toBe(11900); +}); + +it('extracts tax when prices include tax', function (): void { + addLine($this->cart, 11900, 1); + TaxSettings::factory()->for($this->store)->pricesInclude()->create(); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create(); + + $result = $this->engine->calculate($checkout); + + expect($result->subtotal)->toBe(11900) + ->and($result->taxTotal)->toBe(1900) + ->and($result->total)->toBe(11900); +}); + +it('returns zero tax when rate is zero', function (): void { + addLine($this->cart, 10000, 1); + TaxSettings::factory()->for($this->store)->create([ + 'config_json' => ['name' => 'Tax', 'rate_basis_points' => 0], + ]); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create(); + + $result = $this->engine->calculate($checkout); + + expect($result->taxTotal)->toBe(0) + ->and($result->total)->toBe(10000); +}); + +it('calculates flat shipping', function (): void { + addLine($this->cart, 2000, 1); + + $zone = ShippingZone::factory()->for($this->store)->create(['countries_json' => ['DE']]); + $rate = ShippingRate::factory()->for($zone, 'zone')->flat(499)->create(); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create([ + 'shipping_method_id' => $rate->id, + ]); + + $result = $this->engine->calculate($checkout); + + expect($result->shipping)->toBe(499) + ->and($result->total)->toBe(2499); +}); + +it('calculates full checkout end-to-end', function (): void { + addLine($this->cart, 10000, 1); + Discount::factory()->for($this->store)->percent10()->create(['code' => 'P10']); + TaxSettings::factory()->for($this->store)->create(); + + $zone = ShippingZone::factory()->for($this->store)->create(['countries_json' => ['DE']]); + $rate = ShippingRate::factory()->for($zone, 'zone')->flat(499)->create(); + + $checkout = Checkout::factory()->for($this->store)->for($this->cart)->create([ + 'discount_code' => 'P10', + 'shipping_method_id' => $rate->id, + ]); + + $result = $this->engine->calculate($checkout); + + expect($result->subtotal)->toBe(10000) + ->and($result->discount)->toBe(1000) + ->and($result->shipping)->toBe(499) + ->and($result->taxTotal)->toBe(1805) + ->and($result->total)->toBe(11304); +}); diff --git a/tests/Unit/Shipping/ShippingCalculatorTest.php b/tests/Unit/Shipping/ShippingCalculatorTest.php new file mode 100644 index 00000000..2ecb36f7 --- /dev/null +++ b/tests/Unit/Shipping/ShippingCalculatorTest.php @@ -0,0 +1,161 @@ +store = Store::factory()->create(); + app()->instance('current_store', $this->store); + $this->calc = new ShippingCalculator; +}); + +afterEach(function (): void { + app()->forgetInstance('current_store'); +}); + +function makeCartWithLine(Store $store, int $weightGrams = 500, bool $requiresShipping = true, int $price = 2000, int $qty = 1): Cart +{ + $product = Product::factory()->for($store)->create(); + $variant = ProductVariant::factory()->for($product)->create([ + 'weight_g' => $weightGrams, + 'requires_shipping' => $requiresShipping, + 'price_amount' => $price, + ]); + + $cart = Cart::factory()->for($store)->create(); + CartLine::factory()->for($cart)->for($variant, 'variant')->create([ + 'quantity' => $qty, + 'unit_price_amount' => $price, + 'line_subtotal_amount' => $price * $qty, + 'line_total_amount' => $price * $qty, + ]); + + return $cart->fresh('lines.variant'); +} + +it('matches zone by country', function (): void { + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE', 'AT'], + 'regions_json' => [], + ]); + ShippingRate::factory()->for($zone, 'zone')->flat(799)->create(); + + $rates = $this->calc->getAvailableRates($this->store, ['country' => 'DE']); + + expect($rates)->toHaveCount(1); +}); + +it('matches zone by region', function (): void { + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => [], + 'regions_json' => ['US-NY'], + ]); + ShippingRate::factory()->for($zone, 'zone')->flat(799)->create(); + + $rates = $this->calc->getAvailableRates($this->store, [ + 'country' => 'US', + 'province_code' => 'NY', + ]); + + expect($rates)->toHaveCount(1); +}); + +it('returns empty when no zone matches', function (): void { + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + 'regions_json' => [], + ]); + ShippingRate::factory()->for($zone, 'zone')->flat(799)->create(); + + $rates = $this->calc->getAvailableRates($this->store, ['country' => 'FR']); + + expect($rates)->toBeEmpty(); +}); + +it('calculates flat rate', function (): void { + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + ]); + $rate = ShippingRate::factory()->for($zone, 'zone')->flat(799)->create(); + $cart = makeCartWithLine($this->store); + + expect($this->calc->calculate($rate, $cart))->toBe(799); +}); + +it('calculates weight-based rate using ranges', function (): void { + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + ]); + $rate = ShippingRate::factory()->for($zone, 'zone')->weight([ + ['min_g' => 0, 'max_g' => 1000, 'amount' => 500], + ['min_g' => 1001, 'max_g' => 5000, 'amount' => 1000], + ])->create(); + + $lightCart = makeCartWithLine($this->store, weightGrams: 400, qty: 1); + expect($this->calc->calculate($rate, $lightCart))->toBe(500); + + $heavyCart = makeCartWithLine($this->store, weightGrams: 1500, qty: 2); + expect($this->calc->calculate($rate, $heavyCart))->toBe(1000); +}); + +it('calculates price-based rate using ranges', function (): void { + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + ]); + $rate = ShippingRate::factory()->for($zone, 'zone')->price([ + ['min_amount' => 0, 'max_amount' => 5000, 'amount' => 799], + ['min_amount' => 5001, 'max_amount' => PHP_INT_MAX, 'amount' => 0], + ])->create(); + + $smallCart = makeCartWithLine($this->store, price: 3000, qty: 1); + expect($this->calc->calculate($rate, $smallCart))->toBe(799); + + $bigCart = makeCartWithLine($this->store, price: 6000, qty: 1); + expect($this->calc->calculate($rate, $bigCart))->toBe(0); +}); + +it('returns zero shipping when no items require shipping', function (): void { + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + ]); + $rate = ShippingRate::factory()->for($zone, 'zone')->flat(799)->create(); + $cart = makeCartWithLine($this->store, requiresShipping: false); + + expect($this->calc->calculate($rate, $cart))->toBe(0); +}); + +it('skips inactive rates in getAvailableRates', function (): void { + $zone = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + ]); + ShippingRate::factory()->for($zone, 'zone')->flat(799)->create(); + ShippingRate::factory()->for($zone, 'zone')->flat(1299)->inactive()->create(); + + $rates = $this->calc->getAvailableRates($this->store, ['country' => 'DE']); + + expect($rates)->toHaveCount(1); +}); + +it('returns all rates from multiple matching zones', function (): void { + $zone1 = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE'], + ]); + $zone2 = ShippingZone::factory()->for($this->store)->create([ + 'countries_json' => ['DE', 'AT'], + ]); + ShippingRate::factory()->for($zone1, 'zone')->flat(499)->create(); + ShippingRate::factory()->for($zone2, 'zone')->flat(799)->create(); + + $rates = $this->calc->getAvailableRates($this->store, ['country' => 'DE']); + + expect($rates)->toHaveCount(2); +}); diff --git a/tests/Unit/Taxes/TaxCalculatorTest.php b/tests/Unit/Taxes/TaxCalculatorTest.php new file mode 100644 index 00000000..ced5506e --- /dev/null +++ b/tests/Unit/Taxes/TaxCalculatorTest.php @@ -0,0 +1,58 @@ +calc = new TaxCalculator; +}); + +it('calculates exclusive tax at 19%', function (): void { + expect($this->calc->addExclusive(10000, 1900))->toBe(1900); +}); + +it('extracts inclusive tax at 19%', function (): void { + expect($this->calc->extractInclusive(11900, 1900))->toBe(1900); +}); + +it('returns 0 when rate is 0 for exclusive', function (): void { + expect($this->calc->addExclusive(10000, 0))->toBe(0); +}); + +it('returns 0 when rate is 0 for inclusive', function (): void { + expect($this->calc->extractInclusive(10000, 0))->toBe(0); +}); + +it('returns 0 when amount is 0', function (): void { + expect($this->calc->addExclusive(0, 1900))->toBe(0) + ->and($this->calc->extractInclusive(0, 1900))->toBe(0); +}); + +it('handles non-standard rate 7% exclusive', function (): void { + expect($this->calc->addExclusive(8999, 700))->toBe(630); +}); + +it('handles small inclusive extraction', function (): void { + expect($this->calc->extractInclusive(119, 1900))->toBe(19); +}); + +it('calculates high 25% exclusive rate', function (): void { + expect($this->calc->addExclusive(10000, 2500))->toBe(2500); +}); + +it('produces tax_lines via calculate', function (): void { + $settings = new TaxSettings([ + 'store_id' => 1, + 'mode' => 'manual', + 'prices_include_tax' => false, + 'config_json' => ['name' => 'VAT', 'rate_basis_points' => 1900], + ]); + + $result = $this->calc->calculate(10000, $settings, []); + + expect($result['tax_total'])->toBe(1900) + ->and($result['tax_lines'])->toHaveCount(1) + ->and($result['tax_lines'][0]->name)->toBe('VAT') + ->and($result['tax_lines'][0]->rate)->toBe(1900) + ->and($result['tax_lines'][0]->amount)->toBe(1900); +});