From c64c8ce12bc2e9bdc507025c5c2db3d848fb1c2d Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 12 Mar 2026 13:36:06 +0100 Subject: [PATCH] UPDATE --- .paul/PROJECT.md | 115 +++++++ .paul/ROADMAP.md | 62 ++++ .paul/SPECIAL-FLOWS.md | 37 +++ .paul/STATE.md | 58 ++++ .paul/codebase/README.md | 24 ++ .paul/codebase/architecture.md | 235 +++++++++++++ .paul/codebase/concerns.md | 127 +++++++ .paul/codebase/conventions.md | 198 +++++++++++ .paul/codebase/dependencies.md | 65 ++++ .paul/codebase/overview.md | 72 ++++ .paul/codebase/stack.md | 141 ++++++++ .paul/codebase/testing.md | 245 ++++++++++++++ .paul/phases/04-csrf-protection/04-01-PLAN.md | 246 ++++++++++++++ .../04-csrf-protection/04-01-SUMMARY.md | 119 +++++++ .../05-order-bugs-fix/05-01-FIX-SUMMARY.md | 46 +++ .paul/phases/05-order-bugs-fix/05-01-FIX.md | 313 ++++++++++++++++++ .../06-integrations-refactoring/06-01-PLAN.md | 188 +++++++++++ .../06-01-SUMMARY.md | 104 ++++++ .../06-integrations-refactoring/06-02-PLAN.md | 296 +++++++++++++++++ .../06-02-SUMMARY.md | 99 ++++++ .phpunit.result.cache | 2 +- .vscode/ftp-kr.diff.ver_0.338.2.zip | 1 + .vscode/ftp-kr.diff.ver_0.338.zip | 1 + .vscode/ftp-kr.json | 3 +- docs/PAUL_WORKFLOW.md | 150 +++++++++ 25 files changed, 2945 insertions(+), 2 deletions(-) create mode 100644 .paul/PROJECT.md create mode 100644 .paul/ROADMAP.md create mode 100644 .paul/SPECIAL-FLOWS.md create mode 100644 .paul/STATE.md create mode 100644 .paul/codebase/README.md create mode 100644 .paul/codebase/architecture.md create mode 100644 .paul/codebase/concerns.md create mode 100644 .paul/codebase/conventions.md create mode 100644 .paul/codebase/dependencies.md create mode 100644 .paul/codebase/overview.md create mode 100644 .paul/codebase/stack.md create mode 100644 .paul/codebase/testing.md create mode 100644 .paul/phases/04-csrf-protection/04-01-PLAN.md create mode 100644 .paul/phases/04-csrf-protection/04-01-SUMMARY.md create mode 100644 .paul/phases/05-order-bugs-fix/05-01-FIX-SUMMARY.md create mode 100644 .paul/phases/05-order-bugs-fix/05-01-FIX.md create mode 100644 .paul/phases/06-integrations-refactoring/06-01-PLAN.md create mode 100644 .paul/phases/06-integrations-refactoring/06-01-SUMMARY.md create mode 100644 .paul/phases/06-integrations-refactoring/06-02-PLAN.md create mode 100644 .paul/phases/06-integrations-refactoring/06-02-SUMMARY.md create mode 100644 .vscode/ftp-kr.diff.ver_0.338.2.zip create mode 100644 .vscode/ftp-kr.diff.ver_0.338.zip create mode 100644 docs/PAUL_WORKFLOW.md diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md new file mode 100644 index 0000000..507b05f --- /dev/null +++ b/.paul/PROJECT.md @@ -0,0 +1,115 @@ +# shopPRO + +## What This Is + +Autorski silnik sklepu internetowego pisany od podstaw — odpowiednik WooCommerce lub PrestaShop, ale bez zależności od zewnętrznych platform. Składa się z panelu administratora (zarządzanie zamówieniami, produktami, klientami) oraz części frontowej dla klienta końcowego. + +## Core Value + +Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online — produktami, zamówieniami i klientami — w jednym spójnym systemie pisanym od podstaw, bez narzutów zewnętrznych platform. + +## Current State + +| Attribute | Value | +|-----------|-------| +| Version | 0.333 | +| Status | Production | +| Last Updated | 2026-03-12 | + +## Requirements + +### Validated (Shipped) + +- [x] Panel administratora — zarządzanie produktami, kategoriami, atrybutami +- [x] Panel administratora — zarządzanie zamówieniami +- [x] Panel administratora — zarządzanie klientami +- [x] Część frontowa — przeglądanie i kupowanie produktów +- [x] Koszyk i składanie zamówień +- [x] Integracje płatności i dostaw +- [x] REST API (ordersPRO + Ekomi) +- [x] Redis caching +- [x] Ochrona przed podwójnym składaniem zamówienia +- [x] Domain-Driven Architecture (migracja z legacy zakończona) + +### Active (In Progress) + +- [ ] [Do zdefiniowania podczas planowania] + +### Planned (Next) + +- [ ] [Do zdefiniowania podczas planowania] + +### Out of Scope + +- Multitenancy (wiele sklepów w jednej instancji) — nie planowane + +## Target Users + +**Primary:** Właściciel/administrator sklepu internetowego +- Zarządza produktami, zamówieniami, klientami przez panel admina +- Potrzebuje niezawodnego, szybkiego narzędzia bez zbędnych zależności + +**Secondary:** Klient końcowy sklepu +- Przegląda produkty, dodaje do koszyka, składa zamówienia + +## Context + +**Technical Context:** +- PHP 7.4+ (produkcja: PHP < 8.0) +- Medoo ORM (`$mdb`), Redis caching +- Domain-Driven Design z Dependency Injection +- PHPUnit 9.6, 810+ testów +- Namespace: `\Domain\`, `\admin\`, `\front\`, `\api\`, `\Shared\` + +## Constraints + +### Technical Constraints +- PHP < 8.0 na produkcji (brak `match`, named arguments, union types) +- Medoo ORM — prepared statements bez wyjątków +- Redis wymagany dla cache + +### Business Constraints +- System wdrażany u klientów jako update package (ZIP) + +## Key Decisions + +| Decision | Rationale | Date | Status | +|----------|-----------|------|--------| +| DDD + DI zamiast legacy architektury | Testowalność, separacja odpowiedzialności | 2025 | Active | +| PHP < 8.0 kompatybilność | Klienci na starszych serwerach | 2025 | Active | +| Własny silnik zamiast frameworka | Pełna kontrola, brak narzutów | - | Active | + +## Success Metrics + +| Metric | Target | Current | Status | +|--------|--------|---------|--------| +| Testy | >800 | 810 | On track | +| Pokrycie architektury DDD | 100% | 100% | Achieved | + +## Tech Stack + +| Layer | Technology | Notes | +|-------|------------|-------| +| Backend | PHP 7.4+ | < 8.0 na produkcji | +| ORM | Medoo | `$mdb` global | +| Cache | Redis | CacheHandler singleton | +| Frontend | HTML/CSS/JS | Własny silnik szablonów (Tpl) | +| Auth | Sesje PHP | CSRF, XSS protection | +| Testy | PHPUnit 9.6 | phpunit.phar | + +## Specialized Flows + +See: .paul/SPECIAL-FLOWS.md + +Quick Reference: +- /feature-dev → Nowe funkcje, większe zmiany (required) +- /koniec-pracy → Release, update package (required) +- /frontend-design → Komponenty UI, szablony widoków +- /code-review → Przegląd kodu przed release +- /simplify → Upraszczanie po implementacji +- /claude-md-improver → Utrzymanie CLAUDE.md +- /zapisz + /wznow → Zapis i wznowienie sesji + +--- +*PROJECT.md — Updated when requirements or context change* +*Last updated: 2026-03-12* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md new file mode 100644 index 0000000..0ac04b6 --- /dev/null +++ b/.paul/ROADMAP.md @@ -0,0 +1,62 @@ +# Roadmap: shopPRO + +## Overview + +shopPRO to autorski silnik sklepu internetowego rozwijany iteracyjnie. Projekt jest już na produkcji (v0.333) — roadmap obejmuje planowane funkcje i usprawnienia kolejnych wersji. + +## Current Milestone + +**Security hardening** (v0.33x) +Status: In progress +Phases: 3 of 4 complete + +## Phases + +| Phase | Name | Plans | Status | Completed | +|-------|------|-------|--------|-----------| +| 1 | Sensitive data logging fix | 1 | Done | 2026-03 | +| 2 | Path traversal + XSS escaping | 1 | Done | 2026-03 (v0.335) | +| 3 | Error handling w krytycznych ścieżkach | 1 | Done | 2026-03 (v0.336) | +| 4 | CSRF protection — admin panel forms | 1 | Applied | 2026-03 (v0.337) | +| 5 | Order bugs fix — duplicate + COD status | 1 | Applied | 2026-03 (v0.338) | + +## Next Milestone + +**Tech debt — Integrations refactoring** +Status: Planning + +| Phase | Name | Plans | Status | Completed | +|-------|------|-------|--------|-----------| +| 6 | IntegrationsRepository split → ApiloRepository | 2 | Done | 2026-03 | + +## Phase Details + +### Phase 4 — CSRF protection + +**Problem:** Brak tokenów CSRF na formularzach panelu admina. State-changing POST endpointy (create/update/delete) są potencjalnie podatne na ataki CSRF. + +**Scope:** Dodanie CSRF tokenów do formularzy i walidacji w panelu administracyjnym. + +**Reference:** `.paul/codebase/concerns.md` — MEDIUM — Missing CSRF tokens + +### Phase 6 — IntegrationsRepository split + +**Problem:** `IntegrationsRepository` ma 875 linii — miesza logikę generyczną (settings, logi, product linking) z logiką specyficzną dla Apilo (~650 linii). Narusza zasadę jednej odpowiedzialności. + +**Scope:** +- Plan 06-01: Utwórz `ApiloRepository` z metodami apilo* (non-breaking) +- Plan 06-02: Zmigruj konsumentów (IntegrationsController, ShopProductController, OrderAdminService, cron.php), usuń apilo* z IntegrationsRepository + +--- + +### Phase 5 — Order bugs fix + +**Problem 1:** Zduplikowane zamówienia — klient widzi błąd i klika złóż zamówienie ponownie. Pierwsze zamówienie trafiło do bazy mimo błędu. Powrót do `/podsumowanie` regeneruje token i pozwala złożyć drugie zamówienie. + +**Problem 2:** Zamówienia COD (płatność przy odbiorze) dostają status "Zamówienie złożone" zamiast "Przyjęte do realizacji". Kod sprawdza hardkodowane `payment_id == 3`, które jest inne w tej instancji sklepu. + +**Scope:** Guard w `summaryView()`, try-catch w `basketSave()`, kolumna `is_cod` w `pp_shop_payment_methods`, użycie flagi zamiast hardkodowanego ID. + +--- +*Roadmap created: 2026-03-12* +*Last updated: 2026-03-12* diff --git a/.paul/SPECIAL-FLOWS.md b/.paul/SPECIAL-FLOWS.md new file mode 100644 index 0000000..bab7aa6 --- /dev/null +++ b/.paul/SPECIAL-FLOWS.md @@ -0,0 +1,37 @@ +# Specialized Flows: shopPRO + +## Project-Level Dependencies + +| Work Type | Skill/Command | Priority | Kiedy używać | +|-----------|---------------|----------|--------------| +| Komponenty UI, szablony widoków | /frontend-design | optional | Przy tworzeniu HTML/CSS | +| Nowe funkcje, większe zmiany | /feature-dev | required | Przed implementacją fazy | +| Przegląd kodu | /code-review | optional | Przed release / KONIEC PRACY | +| Upraszczanie po zmianach | /simplify | optional | Po zakończeniu implementacji | +| Utrzymanie CLAUDE.md | /claude-md-improver | optional | Co kilka faz / po dużych zmianach | +| Release, budowanie update package | /koniec-pracy | required | Na koniec każdej sesji roboczej | +| Zapis i wznowienie sesji | /zapisz + /wznow | optional | Na przerwę / powrót do pracy | + +## Phase Overrides + +Brak — domyślna konfiguracja obowiązuje dla wszystkich faz. + +## Templates & Assets + +| Asset Type | Location | When Used | +|------------|----------|-----------| +| CLAUDE.md | CLAUDE.md | Konwencje kodu, architektura, stack techniczny | +| Struktura bazy | docs/DATABASE_STRUCTURE.md | Przy zmianach schematu DB | +| Dokumentacja API | api-docs/api-reference.json | Przy zmianach API | +| TODO | docs/TODO.md | Planowanie nowych funkcji | + +## Verification (UNIFY) + +Podczas UNIFY sprawdź: +- `/feature-dev` — czy był użyty przed implementacją fazy? +- `/koniec-pracy` — czy release został wykonany? + +Braki dokumentuj w STATE.md (Deferred Issues), nie blokują UNIFY. + +--- +*SPECIAL-FLOWS.md — Created: 2026-03-12* diff --git a/.paul/STATE.md b/.paul/STATE.md new file mode 100644 index 0000000..906334d --- /dev/null +++ b/.paul/STATE.md @@ -0,0 +1,58 @@ +# Project State + +## Project Reference + +See: .paul/PROJECT.md (updated 2026-03-12) + +**Core value:** Właściciel sklepu ma pełną kontrolę nad sprzedażą online w jednym systemie pisanym od podstaw, bez narzutów zewnętrznych platform. +**Current focus:** Projekt zainicjalizowany — gotowy do planowania + +## Current Position + +Milestone: Tech debt — Integrations refactoring +Phase: 6 of ? — IntegrationsRepository split — Complete +Plan: 06-02 complete (phase done) +Status: UNIFY complete, phase 6 finished +Last activity: 2026-03-12 — 06-02 UNIFY complete + +Note: Previous milestone (Security hardening) — fazy 4 i 5 UNIFY zakończone. +Milestone Security hardening: COMPLETE. + +Progress: +- Milestone (Security hardening): [██████████] 100% (COMPLETE) +- Phase 6: [██████████] 100% (complete) + +## Loop Position + +Current loop state (phase 6, plan 02): +``` +PLAN ──▶ APPLY ──▶ UNIFY + ✓ ✓ ✓ [Phase 6 complete] +``` + +Deferred (previous milestone — now closed): +``` +Phase 4: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-12] +Phase 5: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-12] +``` + +## Accumulated Context + +### Decisions +None yet. + +### Deferred Issues +None yet. + +### Blockers/Concerns +None yet. + +## Session Continuity + +Last session: 2026-03-12 +Stopped at: Phase 04 UNIFY complete — wszystkie odroczone pętle zamknięte +Next action: /paul:progress — wybierz kolejny task lub milestone +Resume file: .paul/phases/04-csrf-protection/04-01-SUMMARY.md + +--- +*STATE.md — Updated after every significant action* diff --git a/.paul/codebase/README.md b/.paul/codebase/README.md new file mode 100644 index 0000000..b2c02f6 --- /dev/null +++ b/.paul/codebase/README.md @@ -0,0 +1,24 @@ +# Codebase Map — shopPRO + +Generated: 2026-03-12 + +## Documents + +| File | Contents | +|------|---------| +| [overview.md](overview.md) | Project summary, size metrics, quick reference | +| [stack.md](stack.md) | Technology stack, libraries, external integrations | +| [architecture.md](architecture.md) | Directory structure, routing, DI, domain modules, request lifecycle | +| [conventions.md](conventions.md) | Naming, Medoo patterns, cache patterns, security patterns | +| [testing.md](testing.md) | PHPUnit setup, test patterns, mocking, coverage | +| [concerns.md](concerns.md) | Security issues, technical debt, dead code, known bugs | +| [dependencies.md](dependencies.md) | Composer, vendored libs, PHP extensions | + +## Quick Facts + +- **PHP 7.4 – <8.0** — no match, union types, str_contains etc. +- **810 tests / 2264 assertions** +- **29 Domain modules**, all with tests +- **Medoo pitfall**: `delete()` takes 2 args, not 3 +- **Top concerns**: tpay.txt logging, path traversal in unlink, hardcoded payment seed +- **Largest files**: `ProductRepository.php` (3583 lines), `IntegrationsRepository.php` (875 lines) diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md new file mode 100644 index 0000000..6170e70 --- /dev/null +++ b/.paul/codebase/architecture.md @@ -0,0 +1,235 @@ +# Architecture & Structure + +## Directory Layout + +``` +shopPRO/ +├── autoload/ # Core application code (custom autoloader) +│ ├── Domain/ # Business logic — 29 modules +│ ├── Shared/ # Cross-cutting utilities +│ │ ├── Cache/ # CacheHandler, RedisConnection +│ │ ├── Email/ # Email (PHPMailer wrapper) +│ │ ├── Helpers/ # Static utility methods +│ │ ├── Html/ # HTML escaping/generation +│ │ ├── Image/ # ImageManipulator +│ │ └── Tpl/ # Template engine +│ ├── admin/ # Admin panel layer +│ │ ├── App.php # Router & DI factory +│ │ ├── Controllers/ # 28 DI controllers +│ │ ├── Support/ # Forms, TableListRequestFactory +│ │ ├── Validation/ # FormValidator +│ │ └── ViewModels/ # Forms/, Common/ +│ ├── front/ # Frontend layer +│ │ ├── App.php # Router & DI factory +│ │ ├── LayoutEngine.php # Placeholder-based layout engine +│ │ ├── Controllers/ # 8 DI controllers +│ │ └── Views/ # 11 static view classes +│ └── api/ # REST API layer +│ ├── ApiRouter.php # Auth + routing +│ └── Controllers/ # 4 DI controllers +├── admin/ +│ ├── index.php # Admin entry point +│ ├── ajax.php # Admin AJAX handler +│ ├── templates/ # Admin view templates +│ └── layout/ # Admin CSS/JS/icons +├── templates/ # Frontend view templates +├── libraries/ # Third-party libraries +├── tests/ # PHPUnit test suite +├── docs/ # Technical documentation +├── index.php # Frontend entry point +├── ajax.php # Frontend AJAX handler +├── api.php # REST API entry point +├── cron.php # Background job processor +└── config.php # DB/Redis config (NOT in repo) +``` + +## Autoloader + +Custom autoloader in each entry point — tries two conventions: +1. `autoload/{namespace}/class.{ClassName}.php` (legacy) +2. `autoload/{namespace}/{ClassName}.php` (PSR-4 style, preferred) + +**Namespace → directory mapping (case-sensitive on Linux):** +- `\Domain\` → `autoload/Domain/` +- `\admin\` → `autoload/admin/` (**lowercase a** — never `\Admin\`) +- `\front\` → `autoload/front/` +- `\api\` → `autoload/api/` +- `\Shared\` → `autoload/Shared/` + +## Dependency Injection + +Manual factory pattern in router classes. Each entry point wires dependencies once: + +```php +// Example from admin\App::getControllerFactories() +'ShopProduct' => function() { + global $mdb; + return new \admin\Controllers\ShopProductController( + new \Domain\Product\ProductRepository($mdb), + new \Domain\Integrations\IntegrationsRepository($mdb), + new \Domain\Languages\LanguagesRepository($mdb) + ); +} +``` + +DI wiring locations: +- Admin: `autoload/admin/App.php` → `getControllerFactories()` +- Frontend: `autoload/front/App.php` → `getControllerFactories()` +- API: `autoload/api/ApiRouter.php` → `getControllerFactories()` + +## Routing + +### Admin (`\admin\App`) +- URL: `/admin/?module=shop_product&action=view_list` +- `module` → PascalCase (`shop_product` → `ShopProduct`) → controller lookup +- `action` → method call on controller +- Auth checked before routing; 2FA supported + +### Frontend (`\front\App`) +- Routes stored in `pp_routes` table (regex patterns, cached in Redis as `pp_routes:all`) +- Match URI → extract destination params → merge with `$_GET` +- Special params: `?product=ID`, `?category=ID`, `?article=ID` +- Controller dispatch via `getControllerFactories()` +- Unmatched → static page content + +### API (`\api\ApiRouter`) +- URL: `/api.php?endpoint=orders&action=getOrders` +- Stateless — auth via `X-Api-Key` header (`hash_equals()`) +- `endpoint` → controller, `action` → method + +## Request Lifecycle (Frontend) + +``` +HTTP GET /produkt/nazwa-produktu + → index.php (autoload, init Medoo, session, language) + → Fetch pp_routes from Redis (or DB) + → Regex match → extract ?product=123 + → front\LayoutEngine::show() + → Determine layout (pp_layouts) + → Replace placeholders [MENU:ID], [BANER_STRONA_GLOWNA], etc. + → Call view classes / repositories for each placeholder + → Output HTML (with GTM, meta OG, WebP, lazy loading) +``` + +## Request Lifecycle (Admin) + +``` +HTTP GET /admin/?module=shop_order&action=view_list + → admin/index.php (IP check, session, auth cookie check) + → admin\App::update() (run pending DB migrations) + → admin\App::special_actions() (handle s-action=user-logon etc.) + → admin\App::render() + → Auth check → if not logged in, show login form + → admin\App::route() + → 'shop_order' → ShopOrder → factory() + → new ShopOrderController(OrderAdminService, ProductRepository) + → ShopOrderController::viewList() + → Tpl::view('shop-order/orders-list', [...]) + → Tpl::render('site/main-layout', ['content' => $html]) + → Output admin HTML +``` + +## Domain Modules (29) + +All in `autoload/Domain/{Module}/{Module}Repository.php`: + +| Module | Repository | Notes | +|--------|-----------|-------| +| Article | ArticleRepository | Blog/news | +| Attribute | AttributeRepository | Product attributes (color, size) | +| Banner | BannerRepository | Promo banners | +| Basket | (static) | Cart calculations | +| Cache | (utilities) | Cache key constants | +| Category | CategoryRepository | Category tree | +| Client | ClientRepository | Customer accounts | +| Coupon | CouponRepository | Discount codes | +| CronJob | CronJobRepository, CronJobProcessor | Job queue | +| Dashboard | DashboardRepository | Admin stats | +| Dictionaries | DictionariesRepository | Units, enums | +| Integrations | IntegrationsRepository | Apilo, Ekomi (**875 lines — too large**) | +| Languages | LanguagesRepository | i18n translations | +| Layouts | LayoutsRepository | Page templates | +| Newsletter | NewsletterRepository, NewsletterPreviewRenderer | Email campaigns | +| Order | OrderRepository, OrderAdminService | Orders, status | +| Pages | PagesRepository | Static pages | +| PaymentMethod | PaymentMethodRepository | Payment gateways | +| Producer | ProducerRepository | Brands | +| Product | ProductRepository | Core catalog (**3583 lines — too large**) | +| ProductSet | ProductSetRepository | Bundles | +| Promotion | PromotionRepository | Special offers | +| Scontainers | ScontainersRepository | Content blocks | +| Settings | SettingsRepository | Shop config | +| ShopStatus | ShopStatusRepository | Order statuses | +| Transport | TransportRepository | Shipping | +| Update | UpdateRepository | DB migrations | +| User | UserRepository | Admin users, 2FA | + +## Admin Controllers (28) + +All in `autoload/admin/Controllers/`: +`ArticlesController`, `ArticlesArchiveController`, `BannerController`, `DashboardController`, `DictionariesController`, `FilemanagerController`, `IntegrationsController`, `LanguagesController`, `LayoutsController`, `NewsletterController`, `PagesController`, `ProductArchiveController`, `ScontainersController`, `SettingsController`, `ShopAttributeController`, `ShopCategoryController`, `ShopClientsController`, `ShopCouponController`, `ShopOrderController`, `ShopPaymentMethodController`, `ShopProducerController`, `ShopProductController` (1199 lines), `ShopProductSetsController`, `ShopPromotionController`, `ShopStatusesController`, `ShopTransportController`, `UpdateController`, `UsersController` + +## Frontend Controllers (8) + +`autoload/front/Controllers/`: `NewsletterController`, `SearchController`, `ShopBasketController`, `ShopClientController`, `ShopCouponController`, `ShopOrderController`, `ShopProducerController`, `ShopProductController` + +## Frontend Views (11, static) + +`autoload/front/Views/`: `Articles`, `Banners`, `Languages`, `Menu`, `Newsletter`, `Scontainers`, `ShopCategory`, `ShopClient`, `ShopPaymentMethod`, `ShopProduct`, `ShopSearch` + +## API Controllers (4) + +`autoload/api/Controllers/`: `OrdersApiController`, `ProductsApiController`, `CategoriesApiController`, `DictionariesApiController` + +## Template System + +### Tpl Engine (`\Shared\Tpl\Tpl`) +```php +// Controller +return \Shared\Tpl\Tpl::view('shop-category/category-edit', [ + 'category' => $data, + 'languages' => $langs, +]); + +// Template (templates/shop-category/category-edit.php) +

category['name'] ?>

+``` + +Search order: `templates_user/`, `templates/`, `../templates_user/`, `../templates/` + +### Frontend Layout Engine (`\front\LayoutEngine`) +Replaces placeholders in layout HTML loaded from `pp_layouts.html`: +- `[MENU:ID]`, `[KONTENER:ID]`, `[LANG:key]` +- `[PROMOWANE_PRODUKTY:limit]`, `[PRODUKTY_TOP:limit]`, `[PRODUKTY_NEW:limit]` +- `[BANER_STRONA_GLOWNA]`, `[BANERY]`, `[COPYRIGHT]` +- `[AKTUALNOSCI:layout_id:limit]`, `[PRODUKTY_KATEGORIA:cat_id:limit]` + +## Admin Form System + +Universal form system for CRUD views. Full docs: `docs/FORM_EDIT_SYSTEM.md`. + +| Component | Class | Location | +|-----------|-------|----------| +| View model | `FormEditViewModel` | `autoload/admin/ViewModels/Forms/` | +| Field definition | `FormField` | same | +| Field type enum | `FormFieldType` | same | +| Tab | `FormTab` | same | +| Action | `FormAction` | same | +| Validation | `FormValidator` | `autoload/admin/Validation/` | +| POST parsing | `FormRequestHandler` | `autoload/admin/Support/Forms/` | +| Rendering | `FormFieldRenderer` | `autoload/admin/Support/Forms/` | +| Template | `form-edit.php` | `admin/templates/components/` | + +## Authentication + +### Admin +- Session: `$_SESSION['user']` after successful login +- 2FA: 6-digit code sent by email; `twofa_pending` in session during verification +- Remember Me: 14-day HMAC-SHA256 signed cookie + +### API +- Stateless; `X-Api-Key` header vs `pp_settings.api_key` via `hash_equals()` + +### Frontend +- Customer session in `$_SESSION['client']` +- IP validation on every request (`$_SESSION['ip']` vs `REMOTE_ADDR`) diff --git a/.paul/codebase/concerns.md b/.paul/codebase/concerns.md new file mode 100644 index 0000000..3f7dd1c --- /dev/null +++ b/.paul/codebase/concerns.md @@ -0,0 +1,127 @@ +# Concerns & Technical Debt + +> Last updated: 2026-03-12 + +## Security Issues + +### HIGH — Sensitive data logged to public file +**File**: `autoload/front/Controllers/ShopOrderController.php:32` +```php +file_put_contents('tpay.txt', print_r($_POST, true) . print_r($_GET, true), FILE_APPEND); +``` +- Logs entire POST/GET (including payment data) to `tpay.txt` likely in webroot +- Possible information disclosure +- **Fix**: Remove log or write to non-public path (e.g., `/logs/`) + +### HIGH — Hardcoded payment seed +**File**: `autoload/front/Controllers/ShopOrderController.php:105` +```php +hash("sha256", "ProjectPro1916;" . round($summary_tmp, 2) ...) +``` +- Hardcoded secret in source — should be in `config.php` + +### MEDIUM — SQL table name interpolated +**File**: `autoload/Domain/Integrations/IntegrationsRepository.php:31` +```php +$stmt = $this->db->query("SELECT * FROM $table"); +``` +- Technically mitigated by whitelist in `settingsTable()`, but violates "no SQL string concatenation" rule +- **Fix**: Use Medoo's native `select()` method + +### MEDIUM — Path traversal in unlink() +**Files**: `autoload/Domain/Product/ProductRepository.php:1605,1617,2129,2163` and `autoload/Domain/Article/ArticleRepository.php:321,340,823,840` +```php +if (file_exists('../' . $row['src'])) { + unlink('../' . $row['src']); +} +``` +- Path from DB, no traversal check +- A DB compromise could delete arbitrary files +- **Fix**: +```php +$basePath = realpath('../upload/'); +$fullPath = realpath('../' . $row['src']); +if ($fullPath && strpos($fullPath, $basePath) === 0) { + unlink($fullPath); +} +``` + +### MEDIUM — Unsanitized output in templates +**Files**: +- `templates/articles/article-full.php` — article title and `$_SERVER['SERVER_NAME']` concatenated without escaping +- `templates/articles/article-entry.php` — `$url` and article titles not escaped + +### MEDIUM — Missing CSRF tokens +- No evidence of CSRF tokens on admin panel forms +- State-changing POST endpoints (create/update/delete) are potentially CSRF-vulnerable + +--- + +## Architecture Issues + +### IntegrationsRepository too large (875 lines) +**File**: `autoload/Domain/Integrations/IntegrationsRepository.php` +Does too many things: settings CRUD, logging, Apilo OAuth, product sync, webhook handling, ShopPRO import. +**Suggested split**: `ApiloAuthManager`, `ApiloProductSyncService`, `ApiloWebhookHandler`, `IntegrationLogRepository`, `IntegrationSettingsRepository` + +### ProductRepository too large (3583 lines) +**File**: `autoload/Domain/Product/ProductRepository.php` +Candidate for extraction of: pricing logic, image handling, cache management, Google feed generation. + +### ShopProductController too large (1199 lines) +**File**: `autoload/admin/Controllers/ShopProductController.php` + +### Helpers.php too large (1101 lines) +**File**: `autoload/Shared/Helpers/Helpers.php` +Static utility god class. Extract into focused service classes. + +### Duplicate email logic +- `\Shared\Helpers\Helpers::send_email()` and `\Shared\Email\Email::send()` both wrap PHPMailer +- Should be unified in `\Shared\Email\Email` +- Documented in `docs/MEMORY.md` + +### 47 `global $mdb` usages remain +- DI is complete in Controllers, but some Helpers methods still use `global $mdb` +- Should be gradually eliminated + +--- + +## Dead Code / Unused Files + +| File | Issue | +|------|-------| +| `libraries/rb.php` | RedBeanPHP — no references found in autoload, candidate for removal | +| `cron-turstmate.php` (note: typo) | Legacy/questionable cron handler | +| `devel.html` | Development artifact in project root | +| `output.txt` | Artifact file | +| `libraries/filemanager-9.14.1/` + `9.14.2/` | Duplicate versions | + +--- + +## Missing Error Handling + +- `IntegrationsRepository.php:163-165` — DB operations after Apilo token refresh lack try-catch +- `ShopOrderController.php:32` — `file_put_contents()` return value not checked +- `ProductRepository.php:1605` — `unlink()` without error handling +- `cron.php:2` — `error_reporting(E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED)` silences all warnings, hiding potential bugs + +--- + +## Known Issues (from docs/TODO.md & docs/MEMORY.md) + +| Issue | Location | Status | +|-------|----------|--------| +| Newsletter save/unsubscribe needs testing | `Domain/Newsletter/` | Open | +| Duplicate email sending logic | `Helpers.php` vs `Email.php` | Open | +| `$mdb->delete()` 2-arg pitfall | Documented in MEMORY.md | Known pitfall | + +--- + +## Summary by Priority + +| Priority | Count | Key Action | +|----------|-------|-----------| +| **Immediate** (security) | 5 | Remove tpay.txt logging, fix path traversal, move hardcoded secret to config | +| **High** (architecture) | 3 | Split IntegrationsRepository, unify email logic, add CSRF | +| **Medium** (quality) | 4 | Escape template output, add try-catch, remove dead files | +| **Low** (maintenance) | 3 | Remove rb.php, reduce Helpers.php, document helpers usage | diff --git a/.paul/codebase/conventions.md b/.paul/codebase/conventions.md new file mode 100644 index 0000000..262d595 --- /dev/null +++ b/.paul/codebase/conventions.md @@ -0,0 +1,198 @@ +# Code Conventions + +## Naming + +| Entity | Convention | Example | +|--------|-----------|---------| +| Classes | PascalCase | `ProductRepository`, `ShopCategoryController` | +| Methods | camelCase | `getQuantity()`, `categoryDetails()` | +| Admin action methods | snake_case | `view_list()`, `category_edit()` | +| Variables | camelCase | `$mockDb`, `$formViewModel`, `$postData` | +| Constants | UPPER_SNAKE_CASE | `MAX_PER_PAGE`, `SORT_TYPES` | +| DB tables | `pp_` prefix + snake_case | `pp_shop_products` | +| DB columns | snake_case | `price_brutto`, `parent_id`, `lang_id` | +| File (new) | `ClassName.php` | `ProductRepository.php` | +| File (legacy) | `class.ClassName.php` | (leave, do not rename) | +| Templates | kebab-case | `shop-category/category-edit.php` | + +## Medoo ORM Patterns + +```php +// Get single record — returns array or null +$product = $this->db->get('pp_shop_products', '*', ['id' => $id]); + +// Get single column value +$qty = $this->db->get('pp_shop_products', 'quantity', ['id' => $id]); + +// Select multiple records — always guard against false return +$rows = $this->db->select('pp_shop_categories', '*', [ + 'parent_id' => $parentId, + 'ORDER' => ['o' => 'ASC'], +]); +if (!is_array($rows)) { return []; } + +// Count +$count = $this->db->count('pp_shop_products', ['category_id' => $catId]); + +// Update +$this->db->update('pp_shop_products', ['quantity' => 10], ['id' => $id]); + +// Delete — ALWAYS 2 arguments, never 3! +$this->db->delete('pp_shop_categories', ['id' => $id]); + +// Insert, then check ID for success +$this->db->insert('pp_shop_products', $data); +$newId = $this->db->id(); +``` + +**Critical pitfalls:** +- `$mdb->delete()` takes **2 args** — passing 3 causes silent bugs +- `$mdb->get()` returns `null` (not `false`) when no record found +- Always check `!is_array()` on `select()` results before iterating + +## Redis Cache Patterns + +```php +$cache = new \Shared\Cache\CacheHandler(); + +// Read (data is serialized) +$raw = $cache->get('shop\\product:' . $id . ':' . $lang . ':' . $hash); +if ($raw) { + return unserialize($raw); +} + +// Write +$cache->set( + 'shop\\product:' . $id . ':' . $lang . ':' . $hash, + serialize($data), + 86400 // TTL in seconds +); + +// Delete one key +$cache->delete($key); + +// Delete by pattern +$cache->deletePattern("shop\\product:$id:*"); + +// Clear all product cache variations +\Shared\Helpers\Helpers::clear_product_cache($productId); +``` + +## Template Rendering + +```php +// In controller — always return string +return \Shared\Tpl\Tpl::view('module/template-name', [ + 'varName' => $value, +]); + +// In template — variables available as $this->varName +

varName ?>

+ +// XSS escape +secureHTML($this->userInput) ?> +``` + +## AJAX Response Format + +```php +// Standard JSON response +echo json_encode([ + 'status' => 'ok', // or 'error' + 'msg' => 'Zapisano.', + 'id' => (int)$savedId, +]); +exit; +``` + +## Form Handling (Admin) + +```php +// Define form +$form = new FormEditViewModel('Category', 'Edit'); +$form->addField(FormField::text('name', ['label' => 'Nazwa', 'required' => true])); +$form->addField(FormField::select('status', ['label' => 'Status', 'options' => [...]])); +$form->addTab('General', [$field1, $field2]); +$form->addAction(new FormAction('save', 'Zapisz', FormAction::TYPE_SUBMIT)); + +// Validate & process POST +$handler = new FormRequestHandler($validator); +$result = $handler->handleSubmit($form, $_POST); +if (!$result['success']) { + // return form with errors +} + +// Render form +return Tpl::view('components/form-edit', ['form' => $form]); +``` + +## Error Handling + +```php +// Wrap risky operations — especially external API calls and file operations +try { + $cache->deletePattern("shop\\product:$id:*"); +} catch (\Exception $e) { + error_log("Cache clear failed: " . $e->getMessage()); +} + +// API — always return structured error +if (!$this->authenticate()) { + self::sendError('UNAUTHORIZED', 'Invalid API key', 401); + return; +} +``` + +## Security + +### XSS +```php +// In templates — use secureHTML for user-sourced strings +secureHTML($this->categoryName) ?> + +// Or use htmlspecialchars directly + +``` + +### SQL Injection +- All queries via Medoo — never concatenate SQL strings +- Use Medoo array syntax or `?` placeholders only + +### Session Security +```php +// IP-binding on every request +if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) { + session_destroy(); + header('Location: /'); + exit; +} +``` + +### API Auth +```php +// Timing-safe comparison +return hash_equals($storedKey, $headerKey); +``` + +## i18n / Translations + +- Language stored in `$_SESSION['current-lang']` +- Translations cached in `$_SESSION['lang-{lang_id}']` +- DB table: `pp_langs`, keys fetched via `LanguagesRepository` +- Helper: `\Shared\Helpers\Helpers::lang($key)` returns translation string + +## PHP Version Constraints (< 8.0) + +```php +// ❌ FORBIDDEN +$result = match($x) { 1 => 'a' }; +function foo(int|string $x) {} +str_contains($s, 'needle'); +str_starts_with($s, 'pre'); + +// ✅ USE INSTEAD +$result = $x === 1 ? 'a' : 'b'; +function foo($x) {} // + @param int|string in docblock +strpos($s, 'needle') !== false +strncmp($pre, $s, strlen($pre)) === 0 +``` diff --git a/.paul/codebase/dependencies.md b/.paul/codebase/dependencies.md new file mode 100644 index 0000000..a3e517f --- /dev/null +++ b/.paul/codebase/dependencies.md @@ -0,0 +1,65 @@ +# Dependencies + +## Composer (PHP) + +**File**: `composer.json` +**PHP requirement**: `>=7.4` (production runs <8.0) + +| Package | Version | Purpose | +|---------|---------|---------| +| `phpunit/phpunit` | ^9.5 | Testing framework | + +## Vendored Libraries (`libraries/`) + +These are NOT managed by Composer — bundled directly. + +| Library | Version | Status | Purpose | +|---------|---------|--------|---------| +| `medoo/` | 1.7.10 | Active | Database ORM | +| `phpmailer/` | classic | Active | Email sending | +| `rb.php` | — | **Unused** — remove | RedBeanPHP legacy ORM | +| `ckeditor/` | 4.x | Active | Rich text editor | +| `apexcharts/` | — | Active | Admin charts | +| `bootstrap/` | 4.1.3 + 4.5.2 | Active | CSS framework (two versions present) | +| `fontawesome-5.7.0/` | 5.7.0 | Active | Icons | +| `filemanager-9.14.1/` | 9.14.1 | Active | File manager | +| `filemanager-9.14.2/` | 9.14.2 | Duplicate? | File manager | +| `codemirror/` | — | Active | Code editor in admin | +| `fancyBox/` + `fancybox3/` | 2 + 3 | Active | Lightbox | +| `plupload/` | — | Active | File uploads | +| `grid/` | — | Active | CSS grid system | + +## Frontend (JS, served directly) + +| Library | Version | Source | +|---------|---------|--------| +| jQuery | 2.1.3 | `libraries/` | +| jQuery Migrate | 1.0.0 | `libraries/` | +| jQuery UI | — | `libraries/` | +| jQuery Autocomplete | 1.4.11 | `libraries/` | +| jQuery Nested Sortable | — | `libraries/` | +| jQuery-confirm | — | `libraries/` | +| Selectize.js | — | `libraries/` | +| Lozad.js | — | `libraries/` | +| Swiper | — | `libraries/` | +| taboverride.min.js | — | `libraries/` | +| validator.js | — | `libraries/` | + +## PHP Extensions Required + +| Extension | Purpose | +|-----------|---------| +| `redis` | Redis caching | +| `curl` | External API calls (Apilo, image downloads) | +| `pdo` + `pdo_mysql` | Medoo ORM database access | +| `mbstring` | String handling | +| `gd` or `imagick` | Image manipulation (ImageManipulator) | +| `json` | JSON encode/decode | +| `session` | Session management | + +## Notes + +- **No npm/package.json** — no JS build pipeline +- **SCSS is pre-compiled** — CSS served as static files +- **No Composer autoload at runtime** — custom autoloader in each entry point +- `libraries/rb.php` (RedBeanPHP, 536 KB) — confirmed unused, safe to delete diff --git a/.paul/codebase/overview.md b/.paul/codebase/overview.md new file mode 100644 index 0000000..45ee390 --- /dev/null +++ b/.paul/codebase/overview.md @@ -0,0 +1,72 @@ +# shopPRO — Codebase Overview + +> Generated: 2026-03-12 + +## What is this project? + +shopPRO is a PHP e-commerce platform with an admin panel, customer-facing storefront, and REST API. It uses a Domain-Driven Design architecture with Dependency Injection (migration from legacy architecture complete). + +## Size & Health + +| Metric | Value | +|--------|-------| +| PHP files (autoload/) | ~588 | +| Lines of code (autoload/) | ~71,668 | +| Test suite | **810 tests, 2264 assertions** | +| Domain modules | 29 | +| Admin controllers | 28 | +| Frontend controllers | 8 | +| API controllers | 4 | +| Frontend views (static) | 11 | + +## Tech Snapshot + +| Layer | Technology | +|-------|-----------| +| Language | PHP 7.4–7.x (production **< 8.0**) | +| Database ORM | Medoo 1.7.10 + MySQL | +| Caching | Redis via `CacheHandler` | +| Email | PHPMailer (classic) | +| Frontend JS | jQuery 2.1.3 | +| CSS | Bootstrap 4.x (pre-compiled SCSS) | +| HTTP Client | Native cURL | +| Testing | PHPUnit 9.6 via `phpunit.phar` | +| Build tools | **None** | + +## Entry Points + +| File | Role | +|------|------| +| `index.php` | Frontend storefront | +| `admin/index.php` | Admin panel | +| `ajax.php` | Frontend AJAX | +| `admin/ajax.php` | Admin AJAX | +| `api.php` | REST API (ordersPRO) | +| `cron.php` | Background job processor | + +## External Integrations + +| Integration | Purpose | +|-------------|---------| +| **Apilo** | ERP/WMS — order sync, inventory, pricing (OAuth 2.0) | +| **Ekomi** | Customer review CSV export | +| **TrustMate** | Review invitation (browser-based, separate cron) | +| **Google XML Feed** | Google Shopping product feed | +| **shopPRO Import** | Import products from another shopPRO instance | + +## Key Architecture Decisions + +- **DI via manual factories** in `admin\App`, `front\App`, `api\ApiRouter` +- **Repository pattern** — all DB access in `autoload/Domain/{Module}/{Module}Repository.php` +- **Redis caching** for products (TTL 24h), routes, and settings +- **No Composer autoload at runtime** — custom dual-convention autoloader in each entry point +- **Stateless REST API** — auth via `X-Api-Key` header + `hash_equals()` +- **Job queue** — cron jobs stored in `pp_cron_jobs` table, processed by `cron.php` + +## Quick Reference + +- Full stack details: `stack.md` +- Architecture & routing: `architecture.md` +- Code conventions: `conventions.md` +- Testing patterns: `testing.md` +- Known issues & debt: `concerns.md` diff --git a/.paul/codebase/stack.md b/.paul/codebase/stack.md new file mode 100644 index 0000000..9b0a7b0 --- /dev/null +++ b/.paul/codebase/stack.md @@ -0,0 +1,141 @@ +# Technology Stack & Integrations + +## Languages + +| Language | Version | Notes | +|----------|---------|-------| +| PHP | 7.4 – <8.0 | Production constraint — no PHP 8.0+ syntax | +| JavaScript | ES5 + jQuery 2.1.3 | No modern framework | +| CSS | Bootstrap 4.x (pre-compiled SCSS) | No build pipeline | + +**PHP 8.0+ features explicitly forbidden:** +- `match` expressions → use ternary / if-else +- Named arguments +- Union types (`int|string`) → use single type + docblock +- `str_contains()`, `str_starts_with()`, `str_ends_with()` → use `strpos()` + +## Core Libraries + +| Library | Version | Location | Purpose | +|---------|---------|----------|---------| +| Medoo | 1.7.10 | `libraries/medoo/medoo.php` | Database ORM | +| PHPMailer | classic | `libraries/phpmailer/` | Email sending | +| RedBeanPHP | — | `libraries/rb.php` | Legacy ORM — **unused, candidate for removal** | + +## Frontend Libraries + +| Library | Location | Purpose | +|---------|----------|---------| +| jQuery | 2.1.3 | DOM / AJAX | +| jQuery Migrate | 1.0.0 | Backward compat | +| Bootstrap | 4.1.3 / 4.5.2 | `libraries/bootstrap*/` | +| CKEditor | 4.x | `libraries/ckeditor/` | Rich text editor | +| ApexCharts | — | `libraries/apexcharts/` | Admin charts | +| FancyBox | 2 + 3 | `libraries/fancyBox/`, `fancybox3/` | Lightbox | +| Plupload | — | `libraries/plupload/` | File uploads | +| Selectize.js | — | — | Select dropdowns | +| Lozad.js | — | — | Lazy loading | +| Swiper | — | — | Carousel/slider | +| CodeMirror | — | `libraries/codemirror/` | Code editor | +| Font Awesome | 5.7.0 | `libraries/fontawesome-5.7.0/` | Icons | +| File Manager | 9.14.1 & 9.14.2 | `libraries/filemanager-9.14.*/` | File browsing | + +## Database + +- **ORM**: Medoo 1.7.10 (custom-extended with Redis support) +- **Engine**: MySQL +- **Table prefix**: `pp_` +- **Connection**: `new medoo([...])` in each entry point via credentials from `config.php` +- **Key tables**: `pp_shop_products`, `pp_shop_orders`, `pp_shop_categories`, `pp_shop_clients` + +## Caching + +- **Technology**: Redis +- **PHP extension**: Native `Redis` class +- **Wrapper**: `\Shared\Cache\CacheHandler` (singleton via `RedisConnection`) +- **Config**: `config.php` → `$config['redis']['host/port/password']` +- **Serialization**: PHP `serialize()` / `unserialize()` +- **Default TTL**: 86400 seconds (24h) +- **Key patterns**: + - `shop\product:{id}:{lang_id}:{hash}` — product details + - `ProductRepository::getProductPermutationQuantityOptions:v2:{id}:*` + - `pp_routes:all` — URL routing patterns + - `pp_settings_cache` — shop settings + +## Email + +- **Library**: PHPMailer (classic, not v6) +- **Config**: `config.php` (host, port, login, password) +- **Helpers**: + - `\Shared\Helpers\Helpers::send_email($to, $subject, $text, $reply, $file)` + - `\Shared\Email\Email::send(...)` — newsletter / template-based +- **Issue**: Duplicate PHPMailer logic in both classes — should be unified + +## HTTP Client + +- **Technology**: Native PHP cURL (`curl_init`, `curl_setopt`, `curl_exec`) +- **No abstraction library** (no Guzzle, Symfony HTTP Client) +- **Used in**: `IntegrationsRepository.php` (Apilo calls), `cron.php` (image downloads) + +## Dev & Build Tools + +| Tool | Purpose | +|------|---------| +| Composer | PHP dependency management | +| PHPUnit 9.6 | Testing (`phpunit.phar`) | +| PowerShell `test.ps1` | Recommended test runner | +| No webpack/Vite/Gulp | SCSS pre-compiled, assets served as-is | + +## External Integrations + +### Apilo (ERP/WMS) +- **Auth**: OAuth 2.0 Bearer token (client_id + client_secret from `pp_shop_apilo_settings`) +- **Base URL**: `https://projectpro.apilo.com/rest/api/` +- **Sync operations**: order sending, payment sync, status polling, product qty/price sync, pricelist sync +- **Code**: `autoload/Domain/Integrations/IntegrationsRepository.php` +- **Cron jobs**: `APILO_SEND_ORDER`, `APILO_SYNC_PAYMENT`, `APILO_STATUS_POLL`, `APILO_PRODUCT_SYNC`, `APILO_PRICELIST_SYNC` +- **Logging**: `\Domain\Integrations\ApiloLogger` → `pp_log` table + +### Ekomi (Reviews) +- **Type**: CSV export +- **Code**: `api.php` → generates `/ekomi/ekomi-{date}.csv` + +### TrustMate (Review Invitations) +- **Type**: Browser-based (requires JS execution) +- **Code**: `cron.php` (line ~741), `cron-trustmate.php` +- **Config**: `$config['trustmate']['enabled']` + +### Google Shopping Feed +- **Type**: XML feed generation +- **Cron job**: `GOOGLE_XML_FEED` +- **Code**: `cron.php` → `ProductRepository::generateGoogleFeedXml()` + +### shopPRO Product Import +- **Type**: Direct MySQL connection to remote shopPRO instance +- **Config**: `pp_shop_shoppro_settings` (domain, db credentials) +- **Code**: `IntegrationsRepository.php` (lines 668–850) +- **Logs**: `/logs/shoppro-import-debug.log` + +### REST API (ordersPRO — outbound) +- **Auth**: `X-Api-Key` header +- **Endpoints**: orders (list/get/status/paid), products (list/get), dictionaries, categories +- **Code**: `api.php` → `autoload/api/ApiRouter.php` → `autoload/api/Controllers/` + +## Cron Job System + +| Job Type | Purpose | +|----------|---------| +| `APILO_TOKEN_KEEPALIVE` | OAuth token refresh | +| `APILO_SEND_ORDER` | Sync orders to Apilo (priority 40) | +| `APILO_SYNC_PAYMENT` | Sync payment status | +| `APILO_STATUS_POLL` | Poll order status changes | +| `APILO_PRODUCT_SYNC` | Update product qty & prices | +| `APILO_PRICELIST_SYNC` | Update pricelist | +| `PRICE_HISTORY` | Record price history | +| `ORDER_ANALYSIS` | Order/product correlation | +| `TRUSTMATE_INVITATION` | Review invitations | +| `GOOGLE_XML_FEED` | Google Shopping XML | + +- **Priority levels**: CRITICAL(10), HIGH(50), NORMAL(100), LOW(200) +- **Backoff**: Exponential on failure (60s → 3600s max) +- **Storage**: `pp_cron_jobs` table diff --git a/.paul/codebase/testing.md b/.paul/codebase/testing.md new file mode 100644 index 0000000..3783f32 --- /dev/null +++ b/.paul/codebase/testing.md @@ -0,0 +1,245 @@ +# Testing Patterns + +## Overview + +| Metric | Value | +|--------|-------| +| Total tests | **810** | +| Total assertions | **2264** | +| Framework | PHPUnit 9.6 (`phpunit.phar`) | +| Bootstrap | `tests/bootstrap.php` | +| Config | `phpunit.xml` | + +## Running Tests + +```bash +# Full suite (PowerShell — recommended) +./test.ps1 + +# Specific file +./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php + +# Specific test method +./test.ps1 --filter testGetQuantityReturnsCorrectValue + +# Alternatives +composer test # standard output +./test.bat # testdox (readable list) +./test-simple.bat # dots +./test-debug.bat # debug output +./test.sh # Git Bash +``` + +## Test Structure + +Tests mirror source structure: + +``` +tests/Unit/ +├── Domain/ +│ ├── Product/ProductRepositoryTest.php +│ ├── Category/CategoryRepositoryTest.php +│ ├── Order/OrderRepositoryTest.php +│ └── ... (all 29 modules covered) +├── admin/Controllers/ +│ ├── ShopCategoryControllerTest.php +│ └── ... +└── api/ + └── ... +``` + +## Test Class Pattern + +```php +namespace Tests\Unit\Domain\Category; + +use PHPUnit\Framework\TestCase; +use Domain\Category\CategoryRepository; + +class CategoryRepositoryTest extends TestCase +{ + private $mockDb; + private CategoryRepository $repository; + + protected function setUp(): void + { + $this->mockDb = $this->createMock(\medoo::class); + $this->repository = new CategoryRepository($this->mockDb); + } + + // Tests follow below... +} +``` + +## AAA Pattern (Arrange-Act-Assert) + +```php +public function testGetQuantityReturnsCorrectValue(): void +{ + // Arrange + $this->mockDb->expects($this->once()) + ->method('get') + ->with( + 'pp_shop_products', + 'quantity', + ['id' => 123] + ) + ->willReturn(42); + + // Act + $result = $this->repository->getQuantity(123); + + // Assert + $this->assertSame(42, $result); +} +``` + +## Mock Patterns + +### Simple return value +```php +$this->mockDb->method('get')->willReturn(['id' => 1, 'name' => 'Test']); +``` + +### Multiple calls with different return values +```php +$this->mockDb->method('get') + ->willReturnCallback(function ($table, $columns, $where) { + if ($table === 'pp_shop_categories') { + return ['id' => 15, 'status' => '1']; + } + return null; + }); +``` + +### Verify exact call arguments +```php +$this->mockDb->expects($this->once()) + ->method('delete') + ->with('pp_shop_categories', ['id' => 5]); +``` + +### Verify method never called +```php +$this->mockDb->expects($this->never())->method('update'); +``` + +### Mock complex PDO statement (for `->query()` calls) +```php +$countStmt = $this->createMock(\PDOStatement::class); +$countStmt->method('fetchAll')->willReturn([[25]]); + +$productsStmt = $this->createMock(\PDOStatement::class); +$productsStmt->method('fetchAll')->willReturn([['id' => 301], ['id' => 302]]); + +$callIndex = 0; +$this->mockDb->method('query') + ->willReturnCallback(function () use (&$callIndex, $countStmt, $productsStmt) { + $callIndex++; + return $callIndex === 1 ? $countStmt : $productsStmt; + }); +``` + +## Controller Test Pattern + +```php +class ShopCategoryControllerTest extends TestCase +{ + protected function setUp(): void + { + $this->repository = $this->createMock(CategoryRepository::class); + $this->languagesRepository = $this->createMock(LanguagesRepository::class); + $this->controller = new ShopCategoryController( + $this->repository, + $this->languagesRepository + ); + } + + // Verify constructor signature + public function testConstructorRequiresCorrectRepositories(): void + { + $reflection = new \ReflectionClass(ShopCategoryController::class); + $params = $reflection->getConstructor()->getParameters(); + + $this->assertCount(2, $params); + $this->assertEquals( + 'Domain\\Category\\CategoryRepository', + $params[0]->getType()->getName() + ); + } + + // Verify action methods return string + public function testViewListReturnsString(): void + { + $this->repository->method('categoriesList')->willReturn([]); + $result = $this->controller->view_list(); + $this->assertIsString($result); + } + + // Verify expected methods exist + public function testHasExpectedActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'view_list')); + $this->assertTrue(method_exists($this->controller, 'category_edit')); + } +} +``` + +## Test Naming Convention + +Pattern: `test{What}{WhenCondition}` + +```php +testGetQuantityReturnsCorrectValue() +testGetQuantityReturnsNullWhenProductNotFound() +testCategoryDetailsReturnsDefaultForInvalidId() +testCategoryDeleteReturnsFalseWhenHasChildren() +testCategoryDeleteReturnsTrueWhenDeleted() +testSaveCategoriesOrderReturnsFalseForNonArray() +testPaginatedCategoryProductsClampsPage() +``` + +## Common Assertions + +```php +$this->assertTrue($bool); +$this->assertFalse($bool); +$this->assertEquals($expected, $actual); +$this->assertSame($expected, $actual); // type-strict +$this->assertNull($value); +$this->assertIsArray($value); +$this->assertIsInt($value); +$this->assertIsString($value); +$this->assertEmpty($array); +$this->assertCount(3, $array); +$this->assertArrayHasKey('id', $array); +$this->assertArrayNotHasKey('foo', $array); +$this->assertGreaterThanOrEqual(3, $count); +$this->assertInstanceOf(ClassName::class, $obj); +``` + +## Available Stubs (`tests/stubs/`) + +| Stub | Purpose | +|------|---------| +| `Helpers.php` | `Helpers::seo()`, `::lang()`, `::send_email()`, `::normalize_decimal()` | +| `ShopProduct.php` | Legacy `shop\Product` class stub | +| `RedisConnection` | Redis singleton stub (auto-loaded from bootstrap) | +| `CacheHandler` | Cache stub (no actual Redis needed in tests) | + +## What's Covered + +- All 29 Domain repositories ✓ +- Core business logic (quantity, pricing, category tree) ✓ +- Query behavior with mocked Medoo ✓ +- Cache patterns ✓ +- Controller constructor injection ✓ +- `FormValidator` behavior ✓ +- API controllers ✓ + +## What's Lightly Covered + +- Full controller action execution (template rendering) +- Session state in tests +- AJAX response integration +- Frontend Views (static classes) diff --git a/.paul/phases/04-csrf-protection/04-01-PLAN.md b/.paul/phases/04-csrf-protection/04-01-PLAN.md new file mode 100644 index 0000000..a442ac5 --- /dev/null +++ b/.paul/phases/04-csrf-protection/04-01-PLAN.md @@ -0,0 +1,246 @@ +--- +phase: 04-csrf-protection +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - autoload/Shared/Security/CsrfToken.php + - autoload/admin/Support/Forms/FormRequestHandler.php + - admin/templates/components/form-edit.php + - admin/templates/site/unlogged-layout.php + - admin/templates/users/user-2fa.php + - autoload/admin/App.php + - tests/Unit/Shared/Security/CsrfTokenTest.php +autonomous: true +--- + + +## Goal +Dodać ochronę CSRF do wszystkich state-changing POST endpointów panelu administracyjnego. + +## Purpose +Brak tokenów CSRF umożliwia atakującemu wymuszenie na zalogowanym adminie wykonania akcji (zapis/usuń/aktualizuj) poprzez spreparowany link lub stronę. Jest to podatność MEDIUM wg concerns.md. + +## Output +- Nowa klasa `\Shared\Security\CsrfToken` z generowaniem i walidacją tokenu +- Integracja w `FormRequestHandler` (walidacja) + `form-edit.php` (token w formularzu) +- Integracja w formularzach logowania i 2FA +- Test jednostkowy dla CsrfToken + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md + +## Source Files +@autoload/admin/Support/Forms/FormRequestHandler.php +@admin/templates/components/form-edit.php +@admin/templates/site/unlogged-layout.php +@admin/templates/users/user-2fa.php +@autoload/admin/App.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| /feature-dev | required | Przed APPLY — nowe klasy, zmiany wielu plików | ○ | + +**BLOCKING:** /feature-dev musi być załadowany przed /paul:apply. + +## Skill Invocation Checklist +- [ ] /feature-dev loaded (uruchom przed apply) + + + + +## AC-1: Formularz edycji chroni przed CSRF +```gherkin +Given admin jest zalogowany i otwiera dowolny formularz edycji +When formularz jest renderowany +Then zawiera ukryte pole _csrf_token z aktualnym tokenem z sesji +``` + +## AC-2: Zapis przez formularz bez tokenu jest odrzucany +```gherkin +Given admin endpoint odbiera POST z FormRequestHandler +When żądanie nie zawiera _csrf_token lub token jest nieprawidłowy +Then handleSubmit() zwraca ['success' => false, 'errors' => ['csrf' => '...']] + And żadna operacja na danych nie jest wykonywana +``` + +## AC-3: Formularz logowania zawiera CSRF token +```gherkin +Given niezalogowany użytkownik otwiera stronę logowania /admin/ +When strona jest renderowana +Then formularz logowania zawiera ukryte pole _csrf_token +``` + +## AC-4: special_actions waliduje CSRF dla user-logon i user-2fa-verify +```gherkin +Given żądanie POST trafia do special_actions() +When s-action to 'user-logon' lub 'user-2fa-verify' +Then token jest walidowany przed przetworzeniem danych + And brak tokenu kończy się przekierowaniem z komunikatem błędu +``` + +## AC-5: Token jest unikalny per sesja +```gherkin +Given sesja PHP jest aktywna +When CsrfToken::getToken() jest wywołany wielokrotnie +Then zwraca ten sam token w ramach jednej sesji + And token ma co najmniej 64 znaki hex (32 bajty) +``` + + + + + + + Task 1: Utwórz klasę CsrfToken + test jednostkowy + autoload/Shared/Security/CsrfToken.php, tests/Unit/Shared/Security/CsrfTokenTest.php + + Utwórz `autoload/Shared/Security/CsrfToken.php` z namespace `\Shared\Security`: + + ```php + class CsrfToken { + const SESSION_KEY = 'csrf_token'; + + public static function getToken(): string + // Jeśli nie ma tokenu w sesji — generuje bin2hex(random_bytes(32)) i zapisuje + // Zwraca istniejący lub nowy token + + public static function validate(string $token): bool + // Pobiera token z sesji, używa hash_equals() dla bezpiecznego porównania + // Zwraca false jeśli sesja nie ma tokenu lub tokeny się różnią + + public static function regenerate(): void + // Generuje nowy token i nadpisuje w sesji + // Używać po udanym logowaniu (session fixation prevention) + } + ``` + + Utwórz `tests/Unit/Shared/Security/CsrfTokenTest.php`: + - test getToken() zwraca string długości 64 + - test getToken() zwraca ten sam token przy kolejnym wywołaniu (idempotency) + - test validate() zwraca true dla poprawnego tokenu + - test validate() zwraca false dla pustego stringa + - test validate() zwraca false dla błędnego tokenu + - test regenerate() zmienia token + + Uwaga PHP < 8.0: brak `match`, brak named arguments, brak union types. + Użyj `isset($_SESSION[...])` zamiast `??` na zmiennych sesji w metodach static (sesja musi być started przed wywołaniem). + + ./test.ps1 tests/Unit/Shared/Security/CsrfTokenTest.php + AC-5 satisfied: token unikalny, 64 znaki, idempotentny + + + + Task 2: Integracja CSRF w formularzach edycji (form-edit.php + FormRequestHandler) + admin/templates/components/form-edit.php, autoload/admin/Support/Forms/FormRequestHandler.php + + **1. form-edit.php** — dodaj token CSRF jako hidden field zaraz po `_form_id`: + ```php + + ``` + Dodaj po linii z `_form_id` (linia ~80). + + **2. FormRequestHandler::handleSubmit()** — dodaj walidację CSRF jako PIERWSZĄ operację, przed walidacją pól: + ```php + $csrfToken = isset($postData['_csrf_token']) ? (string)$postData['_csrf_token'] : ''; + if (!\Shared\Security\CsrfToken::validate($csrfToken)) { + return [ + 'success' => false, + 'errors' => ['csrf' => 'Nieprawidłowy token bezpieczeństwa. Odśwież stronę i spróbuj ponownie.'], + 'data' => [] + ]; + } + ``` + + Unikaj: modyfikowania logiki walidacji pól — CSRF check to osobny guard przed walidacją. + + + Ręcznie: sprawdź źródło strony formularza edycji — musi zawierać input[name="_csrf_token"]. + Testy: ./test.ps1 (suite nie powinna się zepsuć). + + AC-1 i AC-2 satisfied + + + + Task 3: CSRF w formularzach logowania i special_actions + admin/templates/site/unlogged-layout.php, admin/templates/users/user-2fa.php, autoload/admin/App.php + + **1. unlogged-layout.php** — dodaj hidden field CSRF do formularza logowania (zaraz po `s-action`): + ```php + + ``` + + **2. user-2fa.php** — sprawdź czy jest formularz POST i dodaj analogicznie token CSRF. + + **3. App::special_actions()** — dodaj walidację CSRF na początku, dla akcji które mają konsekwencje: + - `user-logon` — waliduj token, przy błędzie: alert + redirect `/admin/` + - `user-2fa-verify` i `user-2fa-resend` — waliduj token + - Po udanym logowaniu (`user-logon` case 1) — wywołaj `\Shared\Security\CsrfToken::regenerate()` PRZED `self::finalize_admin_login()` (zapobiega session fixation) + + Wzorzec walidacji w special_actions (na początku switch lub przed każdym case): + ```php + $csrfToken = isset($_POST['_csrf_token']) ? (string)$_POST['_csrf_token'] : ''; + if (!\Shared\Security\CsrfToken::validate($csrfToken)) { + \Shared\Helpers\Helpers::alert('Nieprawidłowy token bezpieczeństwa. Spróbuj ponownie.'); + header('Location: /admin/'); + exit; + } + ``` + Umieść ten blok PRZED switch ($sa), aby był wspólny dla wszystkich case. + + Unikaj: dodawania CSRF do user-logout (to GET link, nie POST — zmiana na POST wykracza poza zakres). + + + Ręcznie: sprawdź źródło strony logowania — musi zawierać input[name="_csrf_token"]. + ./test.ps1 (suite nie powinna się zepsuć). + + AC-3 i AC-4 satisfied + + + + + + +## DO NOT CHANGE +- Logika walidacji pól w `FormValidator` — tylko dodajemy CSRF guard przed walidacją +- Mechanizm sesji w `admin/index.php` — sesja jest już startowana przed wywołaniem kodu +- Routing w `admin\App::route()` — nie zmieniamy routingu +- Jakiekolwiek pliki frontendowe (front/) — CSRF dotyczy tylko admina w tej fazie +- Pliki testów innych niż nowy CsrfTokenTest.php + +## SCOPE LIMITS +- Nie zmieniać logout z GET na POST — to osobna zmiana wykraczająca poza zakres +- Nie dodawać CSRF do admin/ajax.php (shop-category, users ajax) — to osobna iteracja +- Nie refaktoryzować FormRequestHandler — tylko dodać CSRF check +- Nie zmieniać struktury sesji poza `csrf_token` key + + + + +Przed uznaniem planu za zakończony: +- [ ] ./test.ps1 — wszystkie testy przechodzą (w tym nowe CsrfTokenTest) +- [ ] Strona formularza edycji zawiera hidden input[name="_csrf_token"] +- [ ] Strona logowania /admin/ zawiera hidden input[name="_csrf_token"] +- [ ] POST bez tokenu do FormRequestHandler zwraca error 'csrf' +- [ ] Brak regresji w istniejących testach (810 testów nadal przechodzi) + + + +- Wszystkie 3 taski wykonane +- CsrfTokenTest przechodzi (min. 6 assertions) +- Pełna suite testów przechodzi bez regresji +- Wszystkie acceptance criteria AC-1 do AC-5 spełnione +- Token regenerowany po udanym logowaniu + + + +Po zakończeniu utwórz `.paul/phases/04-csrf-protection/04-01-SUMMARY.md` + diff --git a/.paul/phases/04-csrf-protection/04-01-SUMMARY.md b/.paul/phases/04-csrf-protection/04-01-SUMMARY.md new file mode 100644 index 0000000..c180618 --- /dev/null +++ b/.paul/phases/04-csrf-protection/04-01-SUMMARY.md @@ -0,0 +1,119 @@ +--- +phase: 04-csrf-protection +plan: 01 +subsystem: auth +tags: [csrf, security, session, admin] + +requires: + - phase: [] + provides: [] +provides: + - "CsrfToken class — token generation, validation, regeneration" + - "CSRF protection on all admin FormRequestHandler POSTs" + - "CSRF protection on login and 2FA forms" + - "Token regeneration after successful login (session fixation prevention)" +affects: [] + +tech-stack: + added: [] + patterns: ["CSRF guard before field validation in FormRequestHandler", "bin2hex(random_bytes(32)) per-session token"] + +key-files: + created: + - autoload/Shared/Security/CsrfToken.php + - tests/Unit/Shared/Security/CsrfTokenTest.php + modified: + - autoload/admin/Support/Forms/FormRequestHandler.php + - admin/templates/components/form-edit.php + - admin/templates/site/unlogged-layout.php + - admin/templates/users/user-2fa.php + - autoload/admin/App.php + +key-decisions: + - "Single CSRF validate() call placed before switch($sa) in special_actions() — covers all POST actions uniformly" + - "regenerate() called on successful login AND after 2FA verify — both session fixation points" + +patterns-established: + - "CSRF check = first operation in handleSubmit(), before field validation" + - "CsrfToken::getToken() in templates via htmlspecialchars() escape" + +duration: ~ +started: 2026-03-12T00:00:00Z +completed: 2026-03-12T00:00:00Z +--- + +# Phase 4 Plan 01: CSRF Protection Summary + +**CSRF protection added to entire admin panel — all state-changing POST endpoints now validate a per-session token.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | single session | +| Completed | 2026-03-12 | +| Tasks | 3 completed | +| Files modified | 7 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Formularz edycji zawiera _csrf_token | Pass | form-edit.php linia 81 | +| AC-2: POST bez tokenu odrzucany przez FormRequestHandler | Pass | FormRequestHandler.php linia 36–42 | +| AC-3: Formularz logowania zawiera _csrf_token | Pass | unlogged-layout.php linia 46 | +| AC-4: special_actions() waliduje CSRF dla user-logon i 2FA | Pass | App.php linia 47–51, przed switch | +| AC-5: Token unikalny per sesja, min. 64 znaki hex | Pass | bin2hex(random_bytes(32)) = 64 znaków | + +## Accomplishments + +- Nowa klasa `\Shared\Security\CsrfToken` z `getToken()`, `validate()`, `regenerate()` +- Guard w `FormRequestHandler::handleSubmit()` jako pierwsza operacja przed walidacją pól +- Token w szablonach: `form-edit.php`, `unlogged-layout.php`, `user-2fa.php` (oba formularze) +- `regenerate()` wywoływany po udanym logowaniu (linia 96) i po weryfikacji 2FA (linia 140) — zapobiega session fixation +- 6 testów jednostkowych w `CsrfTokenTest.php` + +## Task Commits + +| Task | Commit | Type | Description | +|------|--------|------|-------------| +| Wszystkie 3 taski | `55988887` | security | faza 4 - ochrona CSRF panelu administracyjnego | + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `autoload/Shared/Security/CsrfToken.php` | Created | Token generation, validation, regeneration | +| `tests/Unit/Shared/Security/CsrfTokenTest.php` | Created | 6 unit tests dla CsrfToken | +| `autoload/admin/Support/Forms/FormRequestHandler.php` | Modified | CSRF guard w handleSubmit() | +| `admin/templates/components/form-edit.php` | Modified | Hidden input _csrf_token | +| `admin/templates/site/unlogged-layout.php` | Modified | Token w formularzu logowania | +| `admin/templates/users/user-2fa.php` | Modified | Token w obu formularzach 2FA | +| `autoload/admin/App.php` | Modified | CSRF walidacja w special_actions() + regenerate() | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Jeden blok validate() przed switch($sa) | Pokrywa wszystkie case jednym sprawdzeniem | Prostota, mniej kodu | +| `\Exception` catch (nie `\Throwable`) | PHP 7.4 compat, wystarczy dla typowych wyjątków | Akceptowalny tradeoff | +| Logout poza zakresem (GET link) | Zmiana na POST wykracza poza tę fazę | Zostawione do osobnej iteracji | + +## Deviations from Plan + +Brak — plan wykonany zgodnie ze specyfikacją. + +## Next Phase Readiness + +**Ready:** +- Cały admin panel chroniony przed CSRF +- Wzorzec do replikacji: `CsrfToken::getToken()` w szablonie + `validate()` w handlerze + +**Concerns:** +- `admin/ajax.php` (shop-category, users ajax) jeszcze nie pokryty — odnotowane w planie jako out-of-scope + +**Blockers:** None + +--- +*Phase: 04-csrf-protection, Plan: 01* +*Completed: 2026-03-12* diff --git a/.paul/phases/05-order-bugs-fix/05-01-FIX-SUMMARY.md b/.paul/phases/05-order-bugs-fix/05-01-FIX-SUMMARY.md new file mode 100644 index 0000000..152582a --- /dev/null +++ b/.paul/phases/05-order-bugs-fix/05-01-FIX-SUMMARY.md @@ -0,0 +1,46 @@ +# FIX SUMMARY — 05-01 + +**Phase:** 05-order-bugs-fix +**Plan:** 05-01-FIX +**Date:** 2026-03-12 +**Status:** COMPLETE + +## Tasks executed + +| # | Task | Status | +|---|------|--------| +| 1 | Guard summaryView() — redirect do istniejącego zamówienia | PASS | +| 2 | try-catch createFromBasket w basketSave() | PASS | +| 3 | Migracja SQL migrations/0.338.sql + DATABASE_STRUCTURE.md | PASS | +| 4 | PaymentMethodRepository — is_cod w normalizacji i forTransport() | PASS | +| 5 | Admin form — switch "Platnosc przy odbiorze" + save | PASS | +| 6 | OrderRepository — is_cod zamiast hardkodowanego payment_id == 3 | PASS | +| 7 | Checkpoint: migracja DB + ustawienie flagi w adminie | DONE | + +## Files modified + +- `autoload/front/Controllers/ShopBasketController.php` +- `autoload/Domain/Order/OrderRepository.php` +- `autoload/Domain/PaymentMethod/PaymentMethodRepository.php` +- `autoload/admin/Controllers/ShopPaymentMethodController.php` +- `migrations/0.338.sql` +- `docs/DATABASE_STRUCTURE.md` + +## Deviations + +Brak. + +## Post-deploy checklist + +- [x] Migracja `migrations/0.338.sql` uruchomiona na produkcji +- [x] Flaga `is_cod = 1` ustawiona na metodzie "Płatność przy odbiorze" w /admin/shop_payment_method/ +- [ ] Redis cache zflushowany (lub poczekać na wygaśnięcie 24h TTL) + +## AC coverage + +| AC | Status | +|----|--------| +| AC-1: Brak duplikatów przy powrocie do /podsumowanie | SATISFIED | +| AC-2: Wyjątki z createFromBasket obsługiwane | SATISFIED | +| AC-3: Admin może ustawić is_cod na metodzie płatności | SATISFIED | +| AC-4: Zamówienie COD dostaje status 4 "Przyjęte do realizacji" | SATISFIED | diff --git a/.paul/phases/05-order-bugs-fix/05-01-FIX.md b/.paul/phases/05-order-bugs-fix/05-01-FIX.md new file mode 100644 index 0000000..94426a4 --- /dev/null +++ b/.paul/phases/05-order-bugs-fix/05-01-FIX.md @@ -0,0 +1,313 @@ +--- +phase: 05-order-bugs-fix +plan: 05-01 +type: fix +wave: 1 +depends_on: [] +files_modified: + - autoload/front/Controllers/ShopBasketController.php + - autoload/Domain/Order/OrderRepository.php + - autoload/Domain/PaymentMethod/PaymentMethodRepository.php + - autoload/admin/Controllers/ShopPaymentMethodController.php + - migrations/0.338.sql + - docs/DATABASE_STRUCTURE.md +autonomous: true +--- + + +## Goal +Fix 2 production bugs reported by customer: (1) duplicate orders on retry after error, (2) wrong initial status for cash-on-delivery orders. + +## Purpose +Production issues affecting real customers. Bug 1 causes double-billed orders. Bug 2 causes wrong order flow for COD payments. + +## Output +- `summaryView()` guards against re-submission after successful order +- `basketSave()` handles exceptions from `createFromBasket()` safely +- `is_cod` column added to `pp_shop_payment_methods` +- COD status promotion uses `is_cod` flag instead of hardcoded `payment_id == 3` +- Admin form for payment methods shows `is_cod` switch + + + +@.paul/STATE.md +@.paul/ROADMAP.md +@autoload/front/Controllers/ShopBasketController.php +@autoload/Domain/Order/OrderRepository.php +@autoload/Domain/PaymentMethod/PaymentMethodRepository.php +@autoload/admin/Controllers/ShopPaymentMethodController.php + + + +## AC-1: No duplicate order on retry +Given a customer submits an order and it is created successfully (order_id saved in session), +When the customer navigates back to `/podsumowanie` and tries to submit again, +Then they are redirected to the existing order page — no new order is created. + +## AC-2: Exception in createFromBasket does not duplicate order +Given `createFromBasket()` throws an uncaught exception after the INSERT succeeds (partial failure), +When the customer retries submission with the same basket, +Then the exception is caught, an error message is shown, basket session is preserved, and no second order is inserted via normal retry flow (AC-1 guards subsequent summary visit). + +## AC-3: COD flag is configurable in admin +Given an admin opens any payment method in `/admin/shop_payment_method/edit/`, +When they toggle "Płatność przy odbiorze" switch and save, +Then the `is_cod` flag is persisted in `pp_shop_payment_methods.is_cod`. + +## AC-4: COD order gets correct initial status +Given a customer places an order with a payment method where `is_cod = 1`, +When the order is created, +Then `pp_shop_order_statuses` contains status_id = 4 ("Przyjęte do realizacji") and the old status 0 entry is updated. + + + + + + Fix BUG-1: Guard summaryView() against re-submission after successful order + autoload/front/Controllers/ShopBasketController.php + +In `summaryView()`, BEFORE calling `createOrderSubmitToken()`, check if `ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY` is set in session. If it is, look up that order's hash via `$this->orderRepository->findHashById($existingOrderId)`. If the hash exists, redirect to `/zamowienie/{hash}` and exit. + +This means the customer who navigates back to the summary page after a successful order is immediately redirected to their order instead of seeing the form again (which would regenerate a token and allow double-submission). + +Do NOT call `createOrderSubmitToken()` in this guard path — just redirect. + +Current problematic code at the top of `summaryView()`: +```php +$orderSubmitToken = $this->createOrderSubmitToken(); +``` +Must become: +```php +$existingOrderId = isset($_SESSION[self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY]) + ? (int)$_SESSION[self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY] + : 0; +if ($existingOrderId > 0) { + $existingOrderHash = $this->orderRepository->findHashById($existingOrderId); + if ($existingOrderHash) { + header('Location: /zamowienie/' . $existingOrderHash); + exit; + } +} +$orderSubmitToken = $this->createOrderSubmitToken(); +``` + + + 1. Create a test order successfully + 2. Navigate back to /podsumowanie in the same browser session + 3. Confirm browser redirects to /zamowienie/{hash} without showing the summary form + + AC-1 satisfied: navigating back to summary after successful order redirects, no form shown + + + + Fix BUG-1: Wrap createFromBasket in try-catch in basketSave() + autoload/front/Controllers/ShopBasketController.php + +In `basketSave()`, wrap the call to `$this->orderRepository->createFromBasket(...)` in a try-catch block. On exception: log with `error_log()`, show user error message via `Helpers::error()`, and redirect to `/koszyk`. Do NOT clear the basket session in the catch block. + +Replace the current `if ($order_id = $this->orderRepository->createFromBasket(...))` pattern with: + +```php +$order_id = null; +try { + $order_id = $this->orderRepository->createFromBasket( + // ... all current args unchanged ... + ); +} catch (\Exception $e) { + error_log('[basketSave] createFromBasket exception: ' . $e->getMessage()); + \Shared\Helpers\Helpers::error(\Shared\Helpers\Helpers::lang('zamowienie-zostalo-zlozone-komunikat-blad')); + header('Location: /koszyk'); + exit; +} + +if ($order_id) { + // ... existing success block unchanged ... +} else { + // ... existing error block unchanged ... +} +``` + +Use `\Exception` catch (not `\Throwable`) — the project targets PHP 7.4 which supports both, but `\Exception` covers the common cases (DB exceptions, mail exceptions). If there are any `\Error` throws in the chain they won't be caught — acceptable tradeoff for PHP 7.4 compatibility. + + + Confirm no PHP syntax errors: `php -l autoload/front/Controllers/ShopBasketController.php` + + AC-2 satisfied: exceptions from createFromBasket are caught and handled gracefully + + + + Fix BUG-2: Add is_cod column migration + migrations/0.338.sql, docs/DATABASE_STRUCTURE.md + +Create the migration file at `migrations/0.338.sql` (kolejna wersja po 0.337): + +```sql +ALTER TABLE `pp_shop_payment_methods` + ADD COLUMN `is_cod` TINYINT(1) NOT NULL DEFAULT 0 + COMMENT 'Platnosc przy odbiorze (cash on delivery): 1 = tak, 0 = nie'; +``` + +Also update `docs/DATABASE_STRUCTURE.md` — in the `pp_shop_payment_methods` table section, add the new column: +| is_cod | Płatność przy odbiorze: 1 = tak, 0 = nie (TINYINT DEFAULT 0) | + +The migration must be run on production DB manually (document this in the plan summary). + + + File `migrations/0.338.sql` exists and contains valid ALTER TABLE statement. + `docs/DATABASE_STRUCTURE.md` mentions `is_cod` in `pp_shop_payment_methods` section. + + AC-3 precondition: column definition prepared for migration + + + + Fix BUG-2: Add is_cod to PaymentMethodRepository normalization and queries + autoload/Domain/PaymentMethod/PaymentMethodRepository.php + +1. In `normalizePaymentMethod(array $row)`: add `$row['is_cod'] = (int)($row['is_cod'] ?? 0);` + +2. In `findActiveById()`: the method already uses `SELECT *` via Medoo `get('pp_shop_payment_methods', '*', ...)` so `is_cod` will be included automatically once the column exists. + +3. In `forTransport()`: the method uses explicit column list in raw SQL. Add `spm.is_cod` to the SELECT list (around line ~241, alongside `spm.apilo_payment_type_id`). + +4. In `paymentMethodsByTransport()` (if exists as a separate raw SQL method): similarly add `spm.is_cod` to the SELECT. Search for any other raw SQL selects in this file that list columns explicitly and add `is_cod` to them. + +5. In the `allActive()` / `paymentMethodsCached()` path: if `allActive()` uses raw SQL with explicit columns, add `spm.is_cod` there too. If it uses `SELECT *`, nothing needed. + +Cache keys that include payment method data (`payment_method{id}`, `payment_methods`) will return stale data until Redis is flushed. The post-deploy step is to flush Redis cache. + + + `php -l autoload/Domain/PaymentMethod/PaymentMethodRepository.php` — no syntax errors. + All explicit SQL SELECTs in this file now include `is_cod`. + + AC-3 + AC-4 precondition: repository returns is_cod field + + + + Fix BUG-2: Add is_cod switch to admin payment method form + autoload/admin/Controllers/ShopPaymentMethodController.php + +In `buildFormViewModel()`: + +1. Add `'is_cod' => (int)($paymentMethod['is_cod'] ?? 0)` to the `$data` array. + +2. Add a switch field after the `status` field: +```php +FormField::switch('is_cod', [ + 'label' => 'Platnosc przy odbiorze', + 'tab' => 'settings', +]), +``` + +In the `save()` / `update()` method of this controller: ensure `is_cod` is read from POST and included in the DB update data. Find where the other fields (description, status, apilo_payment_type_id, etc.) are read from request and add: +```php +'is_cod' => (int)(\Shared\Helpers\Helpers::get('is_cod') ? 1 : 0), +``` + +Check if there is a `FormRequestHandler` or similar save mechanism — if so, `is_cod` may need to be added to the allowed fields list. Read the save method to confirm. + + + `php -l autoload/admin/Controllers/ShopPaymentMethodController.php` — no syntax errors. + Check that `is_cod` appears in both the form field list and the save data array. + + AC-3 satisfied: admin can set is_cod flag on any payment method + + + + Fix BUG-2: Use is_cod flag instead of hardcoded payment_id == 3 in OrderRepository + autoload/Domain/Order/OrderRepository.php + +In `createFromBasket()`, at lines 817-820, replace the hardcoded check: + +```php +// BEFORE: +if ($payment_id == 3) { + $this->updateOrderStatus($order_id, 4); + $this->insertStatusHistory($order_id, 4, 1); +} +``` + +With: +```php +// AFTER: +if (!empty($payment_method['is_cod'])) { + $this->updateOrderStatus($order_id, 4); + $this->insertStatusHistory($order_id, 4, 1); +} +``` + +`$payment_method` is already fetched at line 669: +```php +$payment_method = ( new \Domain\PaymentMethod\PaymentMethodRepository( $this->db ) )->findActiveById( (int)$payment_id ); +``` +So `$payment_method['is_cod']` is available without any additional DB query. + + + `php -l autoload/Domain/Order/OrderRepository.php` — no syntax errors. + Confirm the old `$payment_id == 3` no longer exists in createFromBasket(). + + AC-4 satisfied: COD status promotion is driven by is_cod flag, not hardcoded ID + + + + Run the database migration on production server + + Claude has prepared the migration file at `migrations/0.338.sql`. + The SQL is: ALTER TABLE pp_shop_payment_methods ADD COLUMN is_cod TINYINT(1) NOT NULL DEFAULT 0 + + You need to run this on the production database manually (via phpMyAdmin, SSH, or your DB client). + + After running, go to /admin/shop_payment_method/list/ → edit the "Płatność przy odbiorze" payment method → enable the "Płatnosc przy odbiorze" switch → Save. + + Also flush Redis cache (or wait for TTL expiry — payment methods cache is 24h). + + + Claude will verify the code changes are in place. The DB migration must be confirmed by you. + + Type "done" when migration and admin flag set + + + + + +## DO NOT CHANGE +- The CSRF token mechanism (separate from order submit token) +- The basket session structure +- The order submission token logic (ORDER_SUBMIT_TOKEN_SESSION_KEY) — only guard summaryView, don't change how tokens are generated/consumed +- Email sending logic in createFromBasket +- Any other payment method fields or behavior + +## SCOPE LIMITS +- Do NOT add database-level unique constraints or idempotency key columns to pp_shop_orders (over-engineering for now) +- Do NOT change the order status values or their meaning +- Do NOT modify test files unless directly testing the changed methods +- Do NOT change the frontend templates + + + +Before declaring plan complete: +- [ ] `php -l` passes on all modified PHP files +- [ ] summaryView() guard redirects to existing order when ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY is set +- [ ] createFromBasket call in basketSave() is wrapped in try-catch +- [ ] `is_cod` column exists in migration SQL +- [ ] normalizePaymentMethod() includes is_cod normalization +- [ ] admin form shows is_cod switch +- [ ] admin save includes is_cod in update data +- [ ] OrderRepository uses $payment_method['is_cod'] not $payment_id == 3 +- [ ] DATABASE_STRUCTURE.md updated + + + +- All PHP files lint-clean +- No more duplicate orders when customer navigates back to summary after successful order +- COD payment method (when is_cod=1) automatically promotes order to status 4 +- Admin can configure which payment method is COD + + + +After completion, create `.paul/phases/05-order-bugs-fix/05-01-FIX-SUMMARY.md` with: +- List of files changed +- Note that DB migration in `migrations/0.338.sql` must be run on production +- Note that admin must set is_cod=1 on the COD payment method after migration + +Then run: `/koniec-pracy` + diff --git a/.paul/phases/06-integrations-refactoring/06-01-PLAN.md b/.paul/phases/06-integrations-refactoring/06-01-PLAN.md new file mode 100644 index 0000000..75115a6 --- /dev/null +++ b/.paul/phases/06-integrations-refactoring/06-01-PLAN.md @@ -0,0 +1,188 @@ +--- +phase: 06-integrations-refactoring +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - autoload/Domain/Integrations/ApiloRepository.php + - tests/Unit/Domain/Integrations/ApiloRepositoryTest.php +autonomous: true +--- + + +## Goal +Wyekstrahować wszystkie metody Apilo z `IntegrationsRepository` do nowej klasy `ApiloRepository` — non-breaking (IntegrationsRepository pozostaje bez zmian do planu 06-02). + +## Purpose +`IntegrationsRepository` ma 875 linii z czego ~650 to logika Apilo (OAuth, keepalive, fetchList, produkty). Po ekstrakcji każda klasa będzie mieć jedną odpowiedzialność, zgodnie z zasadami projektu (jedna klasa = jedna odpowiedzialność, max ~50 linii na metodę). + +## Output +- Nowy plik: `autoload/Domain/Integrations/ApiloRepository.php` (~650 linii) +- Nowy plik testów: `tests/Unit/Domain/Integrations/ApiloRepositoryTest.php` +- `IntegrationsRepository` bez zmian (backward compatible) + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Source Files +@autoload/Domain/Integrations/IntegrationsRepository.php +@tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php + + + + +## AC-1: ApiloRepository zawiera wszystkie metody Apilo +```gherkin +Given plik autoload/Domain/Integrations/ApiloRepository.php istnieje +When przeglądamy jego publiczne metody +Then klasa ma: apiloAuthorize, apiloGetAccessToken, apiloKeepalive, + apiloIntegrationStatus, apiloFetchList, apiloFetchListResult, + apiloProductSearch, apiloCreateProduct +``` + +## AC-2: ApiloRepository ma własny dostęp do DB (DI przez konstruktor) +```gherkin +Given ApiloRepository(db: $mdb) jest tworzona +When wywoływana jest dowolna metoda apilo* +Then używa $db do zapytań bez zależności od IntegrationsRepository +``` + +## AC-3: IntegrationsRepository nie zmieniona (backward compatible) +```gherkin +Given istniejące testy IntegrationsRepositoryTest przechodzą +When uruchamiane jest ./test.ps1 +Then wszystkie 817+ testów green, brak nowych błędów +``` + +## AC-4: Testy ApiloRepository pokrywają kluczowe metody +```gherkin +Given nowy plik ApiloRepositoryTest.php +When uruchamiane jest ./test.ps1 +Then testy dla: apiloGetAccessToken, apiloKeepalive, apiloIntegrationStatus, + apiloFetchListResult, apiloFetchList (invalid type), prywatnych helperów przechodzą +``` + + + + + + + Task 1: Utwórz ApiloRepository — ekstrakcja metod Apilo + autoload/Domain/Integrations/ApiloRepository.php + + Utwórz nowy plik `autoload/Domain/Integrations/ApiloRepository.php`. + + Namespace: `Domain\Integrations` + + Klasa ma: + - `private $db;` + - `private const SETTINGS_TABLE = 'pp_shop_apilo_settings';` + - Konstruktor: `public function __construct($db)` + + Przenieś (skopiuj) z IntegrationsRepository **bez modyfikacji logiki**: + - Metody publiczne: `apiloAuthorize`, `apiloGetAccessToken`, `apiloKeepalive`, + `apiloIntegrationStatus`, `apiloFetchList`, `apiloFetchListResult`, + `apiloProductSearch`, `apiloCreateProduct` + - Metody prywatne: `refreshApiloAccessToken`, `shouldRefreshAccessToken`, + `isFutureDate`, `normalizeApiloMapList`, `isMapListShape`, `extractApiloErrorMessage` + + Dostosowania niezbędne po przeniesieniu: + - Wszędzie gdzie metody apilo* wewnętrznie wołają `$this->getSettings('apilo')` — + zamień na `$this->db->select(self::SETTINGS_TABLE, ['name', 'value'])` i mapuj + na `[$row['name'] => $row['value']]` (ta sama logika co w IntegrationsRepository::getSettings) + - Wszędzie gdzie wołają `$this->saveSetting('apilo', ...)` — zamień na bezpośrednie + `$this->db->update(self::SETTINGS_TABLE, ['value' => $value], ['name' => $name])` + i `$this->db->insert(self::SETTINGS_TABLE, ['name' => $name, 'value' => $value])` + z `count()` przed jak w saveSetting (dokładna kopia logiki) + + Unikaj: dziedziczenia z IntegrationsRepository, jakichkolwiek zależności poza $db. + PHP < 8.0: brak match, named args, union types, str_contains. + + + php -l autoload/Domain/Integrations/ApiloRepository.php zwraca "No syntax errors" + Klasa ma dokładnie 8 publicznych metod apilo* + 6 prywatnych helperów. + + AC-1 i AC-2 spełnione + + + + Task 2: Utwórz ApiloRepositoryTest + tests/Unit/Domain/Integrations/ApiloRepositoryTest.php + + Utwórz `tests/Unit/Domain/Integrations/ApiloRepositoryTest.php`. + + Namespace: `Tests\Unit\Domain\Integrations` + Klasa extends `PHPUnit\Framework\TestCase` + + Przenieś (skopiuj) z IntegrationsRepositoryTest wszystkie testy dotyczące metod Apilo: + - `testApiloGetAccessTokenReturnsNullWithoutSettings` + - `testShouldRefreshAccessTokenReturnsFalseForFarFutureDate` + - `testShouldRefreshAccessTokenReturnsTrueForNearExpiryDate` + - `testApiloFetchListThrowsForInvalidType` + - `testApiloFetchListResultReturnsDetailedErrorWhenConfigMissing` + - `testApiloIntegrationStatusReturnsMissingConfigMessage` + - `testNormalizeApiloMapListRejectsErrorPayload` + - `testNormalizeApiloMapListAcceptsIdNameList` + + Dostosuj w skopiowanych testach: + - Zmień `new IntegrationsRepository($this->mockDb)` → `new ApiloRepository($this->mockDb)` + - Use statement: `use Domain\Integrations\ApiloRepository;` + - setUp: `$this->repository = new ApiloRepository($this->mockDb);` + + Uwaga: w testach mockujących `select` z `pp_shop_apilo_settings` — sprawdź czy + ApiloRepository używa dokładnie tej samej tabeli i struktury zapytania co IntegrationsRepository. + Jeśli zmieniło się wywołanie (np. bezpośrednie select zamiast przez getSettings), + dostosuj expect() w testach. + + Nie usuwaj tych testów z IntegrationsRepositoryTest — zostają tam do planu 06-02. + + + ./test.ps1 tests/Unit/Domain/Integrations/ApiloRepositoryTest.php — wszystkie testy green + ./test.ps1 — pełna suite green (817+ testów, brak regresji) + + AC-3 i AC-4 spełnione + + + + + + +## DO NOT CHANGE +- `autoload/Domain/Integrations/IntegrationsRepository.php` — bez żadnych zmian w tym planie +- `tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php` — tylko dodajemy, nie usuwamy +- Żadne kontrolery, App.php, cron.php — migracja konsumentów to plan 06-02 +- Żadne zmiany logiki biznesowej — czysta ekstrakcja, zero refaktoringu logiki + +## SCOPE LIMITS +- Ten plan tworzy tylko nową klasę + testy. Konsumenci nadal używają IntegrationsRepository. +- Nie zmieniamy nazw metod, sygnatur, zachowania. +- Nie optymalizujemy kodu Apilo podczas przenoszenia. + + + + +Before declaring plan complete: +- [ ] php -l autoload/Domain/Integrations/ApiloRepository.php — no syntax errors +- [ ] ApiloRepository ma 8 publicznych metod: apiloAuthorize, apiloGetAccessToken, + apiloKeepalive, apiloIntegrationStatus, apiloFetchList, apiloFetchListResult, + apiloProductSearch, apiloCreateProduct +- [ ] ./test.ps1 tests/Unit/Domain/Integrations/ApiloRepositoryTest.php — all green +- [ ] ./test.ps1 — full suite green, żadna regresja w IntegrationsRepositoryTest +- [ ] IntegrationsRepository.php nie został zmodyfikowany + + + +- ApiloRepository.php istnieje z pełnym zestawem metod Apilo +- ApiloRepositoryTest.php istnieje z testami dla kluczowych metod +- Pełna suite testów green (817+ testów) +- IntegrationsRepository niezmieniony (backward compatible) + + + +After completion, create `.paul/phases/06-integrations-refactoring/06-01-SUMMARY.md` + diff --git a/.paul/phases/06-integrations-refactoring/06-01-SUMMARY.md b/.paul/phases/06-integrations-refactoring/06-01-SUMMARY.md new file mode 100644 index 0000000..e4d4d9b --- /dev/null +++ b/.paul/phases/06-integrations-refactoring/06-01-SUMMARY.md @@ -0,0 +1,104 @@ +--- +phase: 06-integrations-refactoring +plan: 01 +subsystem: domain +tags: [apilo, integrations, refactoring, repository] + +requires: [] +provides: + - "ApiloRepository — klasa z 8 pub metodami Apilo (OAuth, keepalive, fetchList, products)" + - "ApiloRepositoryTest — 9 testów jednostkowych" +affects: [06-02-consumers-migration] + +tech-stack: + added: [] + patterns: + - "ApiloRepository: własna stała SETTINGS_TABLE, prywatne getApiloSettings/saveApiloSetting zamiast delegacji do IntegrationsRepository" + +key-files: + created: + - autoload/Domain/Integrations/ApiloRepository.php + - tests/Unit/Domain/Integrations/ApiloRepositoryTest.php + modified: [] + +key-decisions: + - "ApiloRepository nie dziedziczy z IntegrationsRepository — własny $db, własna const SETTINGS_TABLE" + - "Non-breaking: IntegrationsRepository zachowany bez zmian do planu 06-02" + - "saveApiloSetting/getApiloSettings jako prywatne — nie duplikują interfejsu publicznego" + +patterns-established: + - "Ekstrakcja domenowej podklasy: nowa klasa z własnym $db, prywatnym dostępem do settings swojej tabeli" + +duration: ~15min +started: 2026-03-12T00:00:00Z +completed: 2026-03-12T00:00:00Z +--- + +# Phase 6 Plan 01: IntegrationsRepository split — ApiloRepository Summary + +**Wyekstrahowano 8 metod Apilo (~330 linii) z IntegrationsRepository do nowego ApiloRepository — non-breaking, 826/826 testów green.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~15 min | +| Completed | 2026-03-12 | +| Tasks | 2 / 2 | +| Files created | 2 | +| Files modified | 0 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: ApiloRepository zawiera wszystkie metody Apilo | Pass | 8 pub metod + 6 priv helperów | +| AC-2: Własny DI przez konstruktor ($db) | Pass | brak zależności od IntegrationsRepository | +| AC-3: IntegrationsRepository niezmieniony (backward compatible) | Pass | plik nie był modyfikowany | +| AC-4: Testy ApiloRepository przechodzą | Pass | 9/9 testów, 826/826 full suite | + +## Accomplishments + +- `ApiloRepository.php` — 330 linii: OAuth (authorize, getAccessToken, keepalive, refresh), integracja status, fetchList/fetchListResult, productSearch, createProduct +- `ApiloRepositoryTest.php` — 9 testów: getAccessToken, shouldRefreshAccessToken (×2), fetchList invalid type, fetchListResult config missing, integrationStatus missing config, normalizeApiloMapList (×2), allPublicMethodsExist +- Full suite wzrosła z 817 do 826 testów (zero regresji) + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `autoload/Domain/Integrations/ApiloRepository.php` | Created | Klasa Apilo: OAuth, keepalive, fetchList, produkty | +| `tests/Unit/Domain/Integrations/ApiloRepositoryTest.php` | Created | Testy jednostkowe ApiloRepository | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Prywatne `getApiloSettings()` / `saveApiloSetting()` zamiast dziedziczenia | Unika coupling z IntegrationsRepository, czysta encapsulacja | 06-02 nie potrzebuje IntegrationsRepository w ApiloRepository | +| Zachowanie `APILO_ENDPOINTS` i `APILO_SETTINGS_KEYS` jako class constants | Były private const w IntegrationsRepository — logicznie należą do ApiloRepository | Stałe są prywatne, nie wymuszają zmian w konsumentach | +| Non-breaking w 06-01 | Migracja konsumentów w 06-02 — mniejsze ryzyko, łatwiejszy review | IntegrationsRepository nadal działa dla wszystkich konsumentów | + +## Deviations from Plan + +Brak — plan wykonany dokładnie jak napisano. + +## Issues Encountered + +Brak. + +## Next Phase Readiness + +**Ready:** +- `ApiloRepository` gotowy do użycia przez konsumentów +- Interfejs publiczny identyczny z metodami `apilo*` w IntegrationsRepository +- Testy stanowią baseline dla weryfikacji po migracji konsumentów + +**Concerns:** +- `IntegrationsController` używa zarówno metod Apilo jak i Settings/ShopPRO — po 06-02 będzie potrzebować obu repozytoriów w konstruktorze +- `OrderAdminService` tworzy `new IntegrationsRepository($db)` lokalnie w 5 miejscach — po 06-02 trzeba zmienić na `new ApiloRepository($db)` + +**Blockers:** Brak + +--- +*Phase: 06-integrations-refactoring, Plan: 01* +*Completed: 2026-03-12* diff --git a/.paul/phases/06-integrations-refactoring/06-02-PLAN.md b/.paul/phases/06-integrations-refactoring/06-02-PLAN.md new file mode 100644 index 0000000..07c5785 --- /dev/null +++ b/.paul/phases/06-integrations-refactoring/06-02-PLAN.md @@ -0,0 +1,296 @@ +--- +phase: 06-integrations-refactoring +plan: 02 +type: execute +wave: 1 +depends_on: ["06-01"] +files_modified: + - autoload/admin/Controllers/IntegrationsController.php + - autoload/admin/App.php + - autoload/Domain/Order/OrderAdminService.php + - cron.php + - autoload/Domain/Integrations/IntegrationsRepository.php + - tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php +autonomous: true +--- + + +## Goal +Zmigrować wszystkich konsumentów metod `apilo*` z `IntegrationsRepository` na nowy `ApiloRepository`, a następnie usunąć metody Apilo z `IntegrationsRepository` (cleanup). + +## Purpose +Po tym planie `IntegrationsRepository` będzie lean (~225 linii): tylko settings, logi, product linking, ShopPRO import. `ApiloRepository` jest jedynym miejscem logiki Apilo. + +## Output +- IntegrationsController: używa obu repozytoriów (IntegrationsRepository dla settings/logi, ApiloRepository dla apilo*) +- OrderAdminService: 3 metody używają ApiloRepository dla apiloGetAccessToken +- cron.php: apilo* wywołania przez $apiloRepository +- IntegrationsRepository: usunięte metody apilo* (~650 linii mniej) +- IntegrationsRepositoryTest: oczyszczony z duplikatów testów apilo* + + + +## Project Context +@.paul/PROJECT.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/06-integrations-refactoring/06-01-SUMMARY.md + +## Source Files +@autoload/admin/Controllers/IntegrationsController.php +@autoload/admin/App.php +@autoload/Domain/Order/OrderAdminService.php +@autoload/Domain/Integrations/IntegrationsRepository.php +@tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php + + + + +## AC-1: IntegrationsController używa ApiloRepository dla apilo* +```gherkin +Given IntegrationsController ma dwa repozytoria: $repository i $apiloRepository +When wywoływana jest dowolna metoda apilo* (apilo_settings, apilo_authorization, itp.) +Then używa $this->apiloRepository->apilo*() a nie $this->repository->apilo*() +``` + +## AC-2: OrderAdminService i cron.php używają ApiloRepository dla apiloGetAccessToken +```gherkin +Given OrderAdminService::resendToApilo, syncApiloPayment, syncApiloStatus + oraz cron.php potrzebują access tokenu +When wywoływana jest metoda apiloGetAccessToken() +Then używają new ApiloRepository($db) lub $apiloRepository, nie IntegrationsRepository +``` + +## AC-3: IntegrationsRepository nie zawiera metod apilo* +```gherkin +Given plik IntegrationsRepository.php po cleanup +When sprawdzamy publiczne metody klasy +Then metody apilo* NIE ISTNIEJĄ, pozostają tylko: + getSettings, getSetting, saveSetting, + getLogs, deleteLog, clearLogs, + linkProduct, unlinkProduct, getProductSku, + shopproImportProduct +``` + +## AC-4: Pełna suite testów green +```gherkin +Given wszystkie zmiany wprowadzone +When uruchamiane jest php phpunit.phar +Then wszystkie testy green (826+ testów, zero regresji) +``` + + + + + + + Task 1: Zaktualizuj IntegrationsController i App.php + autoload/admin/Controllers/IntegrationsController.php, autoload/admin/App.php + + **IntegrationsController.php:** + + 1. Dodaj import: `use Domain\Integrations\ApiloRepository;` + 2. Dodaj property: `private ApiloRepository $apiloRepository;` + 3. Zmień konstruktor na: + ```php + public function __construct( IntegrationsRepository $repository, ApiloRepository $apiloRepository ) + { + $this->repository = $repository; + $this->apiloRepository = $apiloRepository; + } + ``` + 4. Zamień wszystkie wywołania `$this->repository->apilo*()` na `$this->apiloRepository->apilo*()`: + - linia ~128: `$this->repository->apiloIntegrationStatus()` → `$this->apiloRepository->apiloIntegrationStatus()` + - linia ~150: `$this->repository->apiloAuthorize(...)` → `$this->apiloRepository->apiloAuthorize(...)` + - linia ~159: `$this->repository->apiloIntegrationStatus()` → `$this->apiloRepository->apiloIntegrationStatus()` + - linia ~194: `$this->repository->apiloCreateProduct(...)` → `$this->apiloRepository->apiloCreateProduct(...)` + - linia ~211: `$this->repository->apiloProductSearch(...)` → `$this->apiloRepository->apiloProductSearch(...)` + - linia ~270: `$this->repository->apiloFetchListResult(...)` → `$this->apiloRepository->apiloFetchListResult(...)` + + Pozostaw bez zmian: getLogs, clearLogs, getSettings, saveSetting, getProductSku, + linkProduct, unlinkProduct, getSettings('shoppro'), saveSetting('shoppro'), shopproImportProduct + — wszystkie przez `$this->repository`. + + **App.php:** + + W fabryce 'Integrations' (linia ~384) zmień: + ```php + return new \admin\Controllers\IntegrationsController( + new \Domain\Integrations\IntegrationsRepository( $mdb ) + ); + ``` + na: + ```php + return new \admin\Controllers\IntegrationsController( + new \Domain\Integrations\IntegrationsRepository( $mdb ), + new \Domain\Integrations\ApiloRepository( $mdb ) + ); + ``` + + + php -l autoload/admin/Controllers/IntegrationsController.php — no syntax errors + php -l autoload/admin/App.php — no syntax errors + grep "apiloRepository" autoload/admin/Controllers/IntegrationsController.php — pokazuje 6+ wystąpień + + AC-1 spełnione + + + + Task 2: Zaktualizuj OrderAdminService i cron.php + autoload/Domain/Order/OrderAdminService.php, cron.php + + **OrderAdminService.php** — 3 metody tworzą IntegrationsRepository i wołają apiloGetAccessToken(). + Zmień tylko te 3 miejsca (linie ~422, ~678, ~751): + + ```php + // PRZED (w każdym z 3 miejsc): + $integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db); + // lub: new \Domain\Integrations\IntegrationsRepository( $mdb ); + $accessToken = $integrationsRepository->apiloGetAccessToken(); + + // PO (w każdym z 3 miejsc): + $apiloRepository = new \Domain\Integrations\ApiloRepository($db); + // lub z $mdb gdzie używano $mdb + $accessToken = $apiloRepository->apiloGetAccessToken(); + ``` + + POZOSTAW BEZ ZMIAN (linie ~579, ~628) — te tworzą IntegrationsRepository + i wołają tylko getSettings('apilo') — to metoda generyczna, zostaje w IntegrationsRepository. + + **cron.php** — linia ~133: + Po linii `$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );` + dodaj: + ```php + $apiloRepository = new \Domain\Integrations\ApiloRepository( $mdb ); + ``` + + Zamień wywołania apilo* przez `$integrationsRepository` na `$apiloRepository`: + - linia ~191: `$integrationsRepository->apiloKeepalive(300)` → `$apiloRepository->apiloKeepalive(300)` + - linia ~279: `$integrationsRepository->apiloGetAccessToken()` → `$apiloRepository->apiloGetAccessToken()` + - linia ~560: `$integrationsRepository->apiloGetAccessToken()` → `$apiloRepository->apiloGetAccessToken()` + - linia ~589: `$integrationsRepository->apiloGetAccessToken()` → `$apiloRepository->apiloGetAccessToken()` + - linia ~642: `$integrationsRepository->apiloGetAccessToken()` → `$apiloRepository->apiloGetAccessToken()` + + POZOSTAW BEZ ZMIAN w cron.php: + - `$integrationsRepository->getSettings('apilo')` (linie ~188, ~198, ~553, ~586, ~632) + - `$integrationsRepository->saveSetting('apilo', ...)` (linia ~625) + + + php -l autoload/Domain/Order/OrderAdminService.php — no syntax errors + php -l cron.php — no syntax errors + grep "integrationsRepository->apilo" cron.php — brak wyników (wszystkie apilo przeniesione) + grep "integrationsRepository->apilo" autoload/Domain/Order/OrderAdminService.php — brak wyników + + AC-2 spełnione + + + + Task 3: Usuń metody apilo* z IntegrationsRepository + cleanup testów + autoload/Domain/Integrations/IntegrationsRepository.php, tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php + + **IntegrationsRepository.php:** + + Usuń następujące bloki (cały kod między komentarzami sekcji a kolejną sekcją): + + 1. Sekcję "// ── Apilo OAuth" z metodami: + - `apiloAuthorize()` + - `apiloGetAccessToken()` + - `apiloKeepalive()` + - `refreshApiloAccessToken()` (private) + - `shouldRefreshAccessToken()` (private) + - `isFutureDate()` (private) + + 2. Stałe klasy: + - `private const APILO_ENDPOINTS = [...]` + - `private const APILO_SETTINGS_KEYS = [...]` + + 3. Sekcję "// ── Apilo API fetch lists" z metodami: + - `apiloFetchList()` + - `apiloFetchListResult()` + - `normalizeApiloMapList()` (private) + - `isMapListShape()` (private) + - `extractApiloErrorMessage()` (private) + + 4. Z sekcji "// ── Apilo product operations" usuń tylko: + - `apiloProductSearch()` + - `apiloCreateProduct()` + (ZACHOWAJ `getProductSku()` — jest generyczna, używana też przez ShopProductController) + + Po usunięciu IntegrationsRepository powinna zawierać: + - settings (settingsTable, getSettings, getSetting, saveSetting) + - logs (getLogs, deleteLog, clearLogs) + - product linking (linkProduct, unlinkProduct, getProductSku) + - ShopPRO import (shopproImportProduct, missingShopproSetting, shopproDb) + + **IntegrationsRepositoryTest.php:** + + Usuń następujące metody testowe (zostały już przeniesione do ApiloRepositoryTest): + - `testApiloGetAccessTokenReturnsNullWithoutSettings()` + - `testShouldRefreshAccessTokenReturnsFalseForFarFutureDate()` + - `testShouldRefreshAccessTokenReturnsTrueForNearExpiryDate()` + - `testApiloFetchListThrowsForInvalidType()` + - `testApiloFetchListResultReturnsDetailedErrorWhenConfigMissing()` + - `testApiloIntegrationStatusReturnsMissingConfigMessage()` + - `testNormalizeApiloMapListRejectsErrorPayload()` + - `testNormalizeApiloMapListAcceptsIdNameList()` + + W metodzie `testAllPublicMethodsExist()` usuń z tablicy `$expectedMethods` wpisy apilo*: + - `'apiloAuthorize'`, `'apiloGetAccessToken'`, `'apiloKeepalive'`, `'apiloIntegrationStatus'` + - `'apiloFetchList'`, `'apiloFetchListResult'`, `'apiloProductSearch'`, `'apiloCreateProduct'` + (Pozostaw: `'getSettings'`, `'getSetting'`, `'saveSetting'`, `'linkProduct'`, `'unlinkProduct'`, + `'getProductSku'`, `'shopproImportProduct'`, `'getLogs'`, `'deleteLog'`, `'clearLogs'`) + + Usuń też `testSettingsTableMapping()` i `testShopproProviderWorks()` tylko jeśli są duplikatami + (sprawdź przed usunięciem — jeśli nie mają odpowiedników, zostaw). + + + php -l autoload/Domain/Integrations/IntegrationsRepository.php — no syntax errors + grep "apilo" autoload/Domain/Integrations/IntegrationsRepository.php — brak wyników (lub tylko komentarze) + php phpunit.phar — wszystkie testy green (826+, zero regresji) + php phpunit.phar tests/Unit/Domain/Integrations/ — oba pliki testów green + + AC-3 i AC-4 spełnione + + + + + + +## DO NOT CHANGE +- `autoload/Domain/Integrations/ApiloRepository.php` — gotowy, nie modyfikować +- `tests/Unit/Domain/Integrations/ApiloRepositoryTest.php` — gotowy, nie modyfikować +- `autoload/admin/Controllers/ShopProductController.php` — używa tylko getSetting(), nie apilo* +- `autoload/admin/Controllers/ShopStatusesController.php` — używa tylko getSetting(), nie apilo* +- `autoload/admin/Controllers/ShopTransportController.php` — używa tylko getSetting(), nie apilo* +- `autoload/admin/Controllers/ShopPaymentMethodController.php` — używa tylko getSetting(), nie apilo* +- Logika biznesowa nie zmienia się — czysta migracja wywołań + +## SCOPE LIMITS +- Nie refaktoryzujemy OrderAdminService poza zmianą 3 instancji na ApiloRepository +- Nie zmieniamy sygnatury metod ani logiki +- Nie przenosimy ShopPRO import do osobnej klasy (to nie ten plan) + + + + +Before declaring plan complete: +- [ ] php -l na wszystkich zmodyfikowanych plikach — no syntax errors +- [ ] grep "apiloRepository->apilo" w IntegrationsController — 6 wystąpień (apilo metody) +- [ ] grep "this->repository->apilo" w IntegrationsController — brak wyników +- [ ] grep "integrationsRepository->apilo" w cron.php — brak wyników +- [ ] grep "integrationsRepository->apilo" w OrderAdminService — brak wyników +- [ ] grep "public function apilo" w IntegrationsRepository — brak wyników +- [ ] php phpunit.phar — 826+ testów green + + + +- IntegrationsController używa ApiloRepository dla wszystkich metod apilo* +- OrderAdminService i cron.php używają ApiloRepository dla apiloGetAccessToken +- IntegrationsRepository nie zawiera żadnych metod apilo* +- Pełna suite testów green bez regresji + + + +After completion, create `.paul/phases/06-integrations-refactoring/06-02-SUMMARY.md` + diff --git a/.paul/phases/06-integrations-refactoring/06-02-SUMMARY.md b/.paul/phases/06-integrations-refactoring/06-02-SUMMARY.md new file mode 100644 index 0000000..8321ff1 --- /dev/null +++ b/.paul/phases/06-integrations-refactoring/06-02-SUMMARY.md @@ -0,0 +1,99 @@ +--- +phase: 06-integrations-refactoring +plan: 02 +subsystem: domain +tags: [apilo, integrations, refactoring, migration] + +requires: + - phase: 06-01 + provides: ApiloRepository class with all apilo* methods +provides: + - "Wszyscy konsumenci apilo* używają ApiloRepository" + - "IntegrationsRepository lean (~225 linii): settings, logi, product linking, ShopPRO" +affects: [] + +tech-stack: + added: [] + patterns: + - "IntegrationsController z dwoma repozytoriami: IntegrationsRepository + ApiloRepository" + +key-files: + created: [] + modified: + - autoload/admin/Controllers/IntegrationsController.php + - autoload/admin/App.php + - autoload/Domain/Order/OrderAdminService.php + - cron.php + - autoload/Domain/Integrations/IntegrationsRepository.php + - tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php + - tests/Unit/admin/Controllers/IntegrationsControllerTest.php + +key-decisions: + - "IntegrationsController dostał ApiloRepository jako drugi argument konstruktora" + - "OrderAdminService: tylko 3 z 5 instancji zmienione na ApiloRepository (2 używają getSettings — zostają)" + - "cron.php: $apiloRepository obok $integrationsRepository (oba potrzebne)" + +patterns-established: + - "Kontroler używający dwóch repozytoriów: każde do swojej domeny" + +duration: ~20min +started: 2026-03-12T00:00:00Z +completed: 2026-03-12T00:00:00Z +--- + +# Phase 6 Plan 02: Migracja konsumentów + cleanup IntegrationsRepository + +**Wszyscy konsumenci apilo* zmigrowano na ApiloRepository; IntegrationsRepository oczyszczono do ~225 linii; 818/818 testów green.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~20 min | +| Completed | 2026-03-12 | +| Tasks | 3 / 3 | +| Files modified | 7 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: IntegrationsController używa ApiloRepository dla apilo* | Pass | 6 wywołań przeniesione | +| AC-2: OrderAdminService i cron.php używają ApiloRepository | Pass | 3 metody + 5 wywołań w cron | +| AC-3: IntegrationsRepository nie zawiera metod apilo* | Pass | 0 wystąpień apilo* | +| AC-4: Pełna suite green | Pass | 818/818 testów | + +## Accomplishments + +- IntegrationsRepository: ~650 linii usunięte, zostały settings + logi + product linking + ShopPRO +- IntegrationsController: nowy konstruktor `(IntegrationsRepository, ApiloRepository)` +- OrderAdminService: 3 metody (resendToApilo, syncApiloPayment, syncApiloStatus) używają ApiloRepository +- cron.php: `$apiloRepository` dla 5 wywołań apilo*; `$integrationsRepository` dla getSettings/saveSetting +- IntegrationsRepositoryTest: oczyszczony z 8 duplikatów apilo testów + przywrócone 3 testy generyczne +- IntegrationsControllerTest: zaktualizowany do nowego 2-arg konstruktora + +## Files Modified + +| File | Zmiana | +|------|--------| +| `autoload/admin/Controllers/IntegrationsController.php` | +ApiloRepository dependency, 6 apilo* calls rerouted | +| `autoload/admin/App.php` | Inject ApiloRepository do IntegrationsController | +| `autoload/Domain/Order/OrderAdminService.php` | 3× IntegrationsRepository → ApiloRepository | +| `cron.php` | +$apiloRepository, 5 apilo* calls rerouted | +| `autoload/Domain/Integrations/IntegrationsRepository.php` | Usunięto ~650 linii apilo* | +| `tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php` | Cleanup + przywrócone testy generyczne | +| `tests/Unit/admin/Controllers/IntegrationsControllerTest.php` | Zaktualizowany do 2-arg konstruktora | + +## Deviations from Plan + +- IntegrationsControllerTest wymagał aktualizacji (nie był w planie) — auto-fix podczas weryfikacji +- 3 testy przypadkowo usunięte przez regex (testAllPublicMethodsExist, testSettingsTableMapping, testShopproProviderWorks) — przywrócone + +## Next Phase Readiness + +**Ready:** Refaktoring fazy 6 kompletny. IntegrationsRepository lean, ApiloRepository izolowany. +**Blockers:** Brak + +--- +*Phase: 06-integrations-refactoring, Plan: 02* +*Completed: 2026-03-12* diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 2f5d95b..e8e3d8a 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":1,"defects":{"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":4,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsContainerWithLanguage":3,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":4,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":4,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsArticleWithRelations":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendCopyFromFallback":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testTopArticlesOrderByViews":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsListArticlesOrderByDateDesc":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenNoApiKey":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenWrongApiKey":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenStoredKeyEmpty":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingEndpoint":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingAction":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns404ForUnknownEndpoint":4,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull":3,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts":3,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeRejectsGetMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeReturns400WhenNoBody":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueRejectsGetMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueReturns400WhenNoBody":4,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextUpdatesStatusToProcessing":3,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorAcceptsRepository":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasMainViewMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testMainViewReturnsString":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateAllMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorRequiresRepository":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":3,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsTrueWhenDeleted":3},"times":{"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsCorrectValue":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsNullWhenProductNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindReturnsProductData":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateQuantitySuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsRegularPrice":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsPromoPrice":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsRegularWhenPromoIsHigher":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetNameReturnsProductName":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetNameReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsInteger":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUnarchiveUpdatesProductAndChildren":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testArchiveUpdatesProductAndChildren":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUnarchiveReturnsBool":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testArchiveReturnsBool":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasUnarchiveMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testUnarchiveMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testConstructorRequiresProductRepository":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsBannerWithTranslations":0.001,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testDeleteReturnsTrue":0.001,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveInsertsNewBanner":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithRedis":0.001,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheRedisUnavailable":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithoutRedis":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheReturnStructure":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testCanBeInstantiated":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasSaveSettingsMethod":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasGetSettingsMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheAjaxMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasViewMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testIsNotAbstract":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testCanCreateController":0.004,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testEditMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorAcceptsRepository":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsArticleWithRelations":0.005,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsNullWhenArticleDoesNotExist":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedFilesDeletesDbRows":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedImagesDeletesDbRows":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveReturnsZeroWhenInsertFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveSetsStatusToMinusOne":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveReturnsFalseWhenUpdateFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderUpdatesImageOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderSkipsEmptyValues":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasBrowseListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasGalleryOrderSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testBrowseListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testGalleryOrderSaveMethodReturnType":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminUsesBoundParamsForTitleFilter":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveWithLegacyFormat":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveUpdatesExistingTranslationsByBannerAndLang":0.001,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testListForAdminIncludesThumbnailSrc":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testRestoreSetsStatusToZero":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListArchivedForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsUnitWithTranslations":0.001,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsNullWhenUnitNotFound":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testSaveInsertsNewUnitAndTranslationsForStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testDeleteRemovesUnitAndTranslations":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdReturnsTextFromDatabase":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdSupportsStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testAllUnitsReturnsArrayIndexedById":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsArrayOrNull":0.002,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveLanguageRejectsInvalidLanguageId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveTranslationInsertsNewTranslationAndReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDeleteTranslationReturnsBoolean":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdReturnsLanguageWithStartFlag":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdFallsBackToFirstLanguageOrPl":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsLayoutWithRelations":0.002,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testDeleteReturnsFalseWhenOnlyOneLayoutExists":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsDefaultLayoutWhenRecordDoesNotExist":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testSaveInsertsNewLayoutAndReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testListAllReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsNullForInvalidId":0.003,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSaveSettingsUpdatesHeaderAndFooter":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testDeleteTemplateReturnsFalseForAdminTemplate":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateByNameReturnsText":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsDefaultContainerForInvalidId":0.001,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsContainerWithTranslations":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDetailsForLanguageReturnsNullForInvalidData":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserWhenExists":0.002,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsErrorWhenLoginIsTaken":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsOkWhenAvailable":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveCreatesUserWithNormalizedSwitches":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithoutPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrue":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsFalseOnFailure":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsUserByLogin":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsSuccessForValidCredentials":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsNegativeOneForBlockedUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseAfterMaxAttempts":0.074,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.075,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.173,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseWhen2FADisabled":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseForInvalidEmail":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testUpdateByIdCallsDbUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorAcceptsRepository":0.004,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorRequiresDictionariesRepository":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorRequiresLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorRequiresLayoutsRepository":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorRequiresRepositoryAndRenderer":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorAcceptsDependencies":0.001,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorRequiresRepositoryAndLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasViewListMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserEditMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasTwofaMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasLoginFormMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorRequiresUserRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserReturnsDefaultsForNull":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserCastsTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserHandlesPartialData":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderUpdatesFilesOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderSkipsEmptyValues":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPagesSummaryForArticlesBuildsLabels":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testUpdateImageAltDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testMarkFileToDeleteDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindReturnsDefaultCouponForInvalidId":0.001,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindNormalizesCouponData":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveInsertsCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveUpdatesCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingsReturnsArray":0.002,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingUpdatesExistingValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingInsertsNewValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testInvalidProviderThrowsException":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testLinkProductUpdatesDatabase":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testUnlinkProductClearsFields":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloGetAccessTokenReturnsNullWithoutSettings":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListThrowsForInvalidType":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testAllPublicMethodsExist":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSettingsTableMapping":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShopproProviderWorks":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenusListReturnsArray":0.002,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenuDeleteReturnsFalseWhenMenuHasPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testGenerateSeoLinkAddsSuffixWhenBaseSlugExists":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testPageUrlPreviewBuildsLanguagePrefixedUrlForNonDefaultLanguage":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testFindReturnsDefaultPromotionForInvalidId":0.001,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testSaveInsertsPromotionAndReturnsId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageAltChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileNameChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageAltChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileNameChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloSettingsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloDataFetchMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloProductMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllShopproMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testApiloSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testShopproSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testVoidReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveSellasistMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveBaselinkerMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorRequiresPagesLanguagesAndLayoutsRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorRequiresPromotionRepository":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsNullForNegativeId":0.001,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsStatusWithIdZero":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindNormalizesNullApiloStatusId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveUpdatesColorAndApiloStatusId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveWithIdZeroWorks":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveWithEmptyApiloStatusIdSetsNull":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveRejectsNegativeId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetApiloStatusIdReturnsValue":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetApiloStatusIdReturnsNullWhenNotSet":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetByIntegrationStatusIdForApilo":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetByIntegrationStatusIdReturnsNullForUnknownIntegration":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testAllStatusesReturnsOrderedList":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testConstructorRequiresShopStatusRepository":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShouldRefreshAccessTokenReturnsFalseForFarFutureDate":0.001,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShouldRefreshAccessTokenReturnsTrueForNearExpiryDate":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListResultReturnsDetailedErrorWhenConfigMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloIntegrationStatusReturnsMissingConfigMessage":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testNormalizeApiloMapListRejectsErrorPayload":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testNormalizeApiloMapListAcceptsIdNameList":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindReturnsNullForInvalidId":0.001,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesData":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveUpdatesRowAndReturnsId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSavePreservesNonNumericApiloPaymentTypeId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllActiveReturnsNormalizedRows":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllForAdminReturnsRowsIncludingInactive":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNullForNotFound":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindKeepsNonNumericApiloPaymentTypeId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveNormalizesStatusValue":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdHandlesNullAndInt":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdReturnsStringForNonNumericValue":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testForTransportReturnsRows":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindNormalizesDataAndIncludesPaymentMethods":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindHandlesNullMaxWpAndApiloId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveInsertReturnsNewId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveUpdateReturnsExistingId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveInsertReturnsNullOnFailure":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveResetsDefaultWhenSettingNew":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveSwitchValuesNormalization":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testListForAdminWhitelistsSortColumn":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testAllActiveReturnsNormalizedRows":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetApiloCarrierAccountIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetApiloCarrierAccountIdReturnsIntOrNull":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetTransportCostReturnsFloatOrNull":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testAllForAdminReturnsAllTransports":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testConstructorRequiresPaymentMethodRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFindAttributeReturnsDefaultAttributeForInvalidId":0.003,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForAdminWhitelistsSortDirectionAndPerPage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testSaveValuesRemovesObsoleteRowsAndSetsDefault":0.001,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testSaveValuesDeletesTranslationWhenNameIsEmpty":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testGetAttributeValueByIdUsesDefaultLanguageWhenNotProvided":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSortTypesReturnsExpectedKeys":0.002,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDetailsReturnsDefaultForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDetailsLoadsTranslations":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveCategoriesOrderReturnsFalseForNonArray":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveCategoriesOrderUpdatesOrderAndParent":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveProductOrderReturnsFalseForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveProductOrderUpdatesCategoryProductOrder":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsFalseWhenHasChildren":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsTrueWhenDeleted":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryTitleReturnsEmptyWhenNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryTitleReturnsFirstAvailableTitle":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testListForAdminWhitelistsSortAndPagination":0.002,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testOrdersForClientReturnsEmptyOnMissingInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testOrdersForClientNormalizesRows":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testTotalsForClientReturnsZeroForMissingInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testTotalsForClientReturnsAggregatedValues":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testConstructorAcceptsDb":0.001,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testHasAllPublicMethods":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testSalesGridReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testLastOrdersReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testMostViewedProductsReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testBestSalesProductsReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageReturnsFallbackWhenEmpty":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsList":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyArrayWhenNone":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsDefaultsToPl":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsForDifferentLanguage":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testUnsubscribeReturnsFalseForInvalidHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testUnsubscribeDeletesSubscriber":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConfirmSubscriptionReturnsFalseForInvalidHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConfirmSubscriptionUpdatesStatus":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testGetHashByEmailReturnsHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testGetHashByEmailReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testRemoveByEmailDeletesSubscriber":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testRemoveByEmailReturnsFalseForMissing":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSignupReturnsFalseForExistingEmail":0.001,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConstructorAcceptsOptionalDependencies":0.003,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusesReturnsMappedArray":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testNextAndPrevOrderIdReturnNullForInvalidInput":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindReturnsDefaultProducerForInvalidId":0.001,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindNormalizesProducerData":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testSaveInsertsNewProducer":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testSaveUpdatesExistingProducer":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testListForAdminWhitelistsSortAndPagination":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllProducersReturnsFormattedList":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testProducerProductsReturnsPaginatedResults":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testFindReturnsDefaultSetForInvalidId":0.001,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testFindNormalizesSetData":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testSaveInsertsNewSetAndSyncsProducts":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testSaveUpdatesExistingSet":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testListForAdminWhitelistsSortAndPagination":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testAllSetsReturnsFormattedList":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testAllProductsForMassEditReturnsMap":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testAllProductsForMassEditEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetProductsByCategoryReturnsList":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetProductsByCategoryReturnsEmptyArray":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentReturnsNullForInvalidProduct":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentReturnsCorrectPrices":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentZeroPercentNullsPromo":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsAssociativeArray":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenNoSettings":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsHandlesNullFromDb":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueReturnsCorrectParam":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueUsesParamNotHardcoded":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueReturnsEmptyStringWhenNotFound":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testConstructorAcceptsDb":0.002,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasUpdateMethod":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testUpdateReturnsArray":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasRunPendingMigrationsMethod":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testRunPendingMigrationsWithNoResults":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasPrivateHelperMethods":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testHasMainViewMethod":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testMainViewReturnsString":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testConstructorRequiresRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testValidateValuesRowsReturnsErrorsForMissingDefaultLanguageAndDefaultSelection":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testValidateValuesRowsReturnsEmptyArrayForValidRows":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testHasExpectedActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testViewActionsReturnString":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testMutationActionsReturnVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testConstructorRequiresCategoryAndLanguagesRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testConstructorRequiresClientRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testConstructorAcceptsService":0.002,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testHasExpectedActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testViewActionsReturnString":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testMutationActionsReturnVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testConstructorRequiresOrderAdminService":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testConstructorAcceptsRepositories":0.003,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasMassEditActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasViewListMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasEditAndSaveMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasOperationMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasCombinationMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasImageAndFileMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testMassEditReturnsString":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testMassEditSaveReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testGetProductsByCategoryReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testConstructorRequiresRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasFormBuildingHelpers":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testSaveMethodReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testConstructorRequiresProductSetRepository":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasMainViewMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testMainViewReturnsString":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateAllMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsArticleWithRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendCopyFromFallback":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticlesIdsReturnsSortedIds":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticlesIdsReturnsNullForEmpty":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesCountReturnsInt":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesCountReturnsZeroForEmpty":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesPagination":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleNoindexReturnsBool":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleNoindexReturnsFalseForNonNoindex":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsReturnsArticlesArray":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testTopArticlesOrderByViews":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsListArticlesOrderByDateDesc":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testBannersReturnsActiveBannersWithFlatLanguages":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testBannersReturnsNullWhenNoBanners":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testMainBannerReturnsActiveBannerWithFlatLanguages":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testMainBannerReturnsNullWhenNoBanner":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testCategoryDefaultLayoutIdReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testCategoryDefaultLayoutIdReturnsNullWhenNone":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetDefaultLayoutReturnsLayoutFromDb":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetDefaultLayoutReturnsNullWhenNoLayout":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetArticleLayoutReturnsLayoutFromDb":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetArticleLayoutReturnsNullWhenNoLayout":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetActiveLayoutFallsBackToDefault":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetActiveLayoutReturnsNullWhenNothingFound":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageDetailsReturnsPageWithLanguage":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMainPageIdReturnsStartPage":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMainPageIdFallsBackToFirstActive":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageSortReturnsValue":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuDetailsReturnsMenuWithPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuDetailsReturnsNullForInvalidMenu":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuPagesReturnsEmptyForNoPages":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsContainerWithLanguage":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontAttributeDetailsReturnsAttributeWithLanguage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontAttributeDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontValueDetailsReturnsValueWithLanguage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontValueDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testSummaryWpCalculatesTotal":0.001,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testSummaryWpReturnsZeroForEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsSumsQuantities":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsReturnsZeroForEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextSingular":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextPlural2to4":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextPlural5Plus":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextCastsToInt":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testGetCategorySortReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testGetCategorySortReturnsSortType":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsTitle":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsEmptyWhenNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsCategoryWithLanguage":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsEmptyWhenCategoryNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoriesTreeReturnsEmptyWhenNoCategories":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryProductsCountReturnsZeroForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryProductsCountReturnsCount":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testProductsIdReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testProductsIdReturnsProductIds":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testBlogCategoryProductsReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testBlogCategoryProductsReturnsIds":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testPaginatedCategoryProductsReturnsEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testPaginatedCategoryProductsClampsPage":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsRowOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsStringOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesReturnsRows":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesHandlesFalseFromDb":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDetailsReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDetailsReturnsRow":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveReturnsFalseForInvalidClientId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveInsertsNewAddress":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveUpdatesExistingAddress":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testMarkAddressAsCurrentReturnsFalseForInvalidIds":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testMarkAddressAsCurrentResetsAndSets":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorOnEmptyInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorWhenClientNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsInactiveForUnconfirmedAccount":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorOnWrongPassword":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsOkOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsNullOnEmptyInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsNullWhenEmailTaken":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsIdAndHashOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationReturnsNullOnEmptyHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationActivatesAndReturnsEmail":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsNullOnEmptyHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsEmailAndPassword":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoveryReturnsNullOnEmptyEmail":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoveryReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoverySetsRecoveryFlagAndReturnsHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientOrdersReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsObjectWhenFound":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsNullForEmptyName":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsTrueForActiveCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForUsedCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForInactiveCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForNullCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableWorksWithArray":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testMarkAsUsedCallsUpdate":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testMarkAsUsedSkipsInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIncrementUsedCountCallsUpdate":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIncrementUsedCountSkipsInvalidId":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsIdWhenFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsNullForEmptyHash":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindHashByIdReturnsHashWhenFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindHashByIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendByIdReturnsArrayWithProducts":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendByHashReturnsArrayWithProducts":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGenerateOrderNumberFormatsCorrectly":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGenerateOrderNumberStartsAt001":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllActiveProducersReturnsFullData":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllActiveProducersReturnsEmptyOnNull":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsProducerWithLanguage":0,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testConstructorRequiresOrderRepository":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testConstructorRequiresProducerRepository":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveReturnsOneForActivePayment":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNormalizedData":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllActiveReturnsEmptyOnNull":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testForTransportReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackReturnsSku":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackFromParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetEanWithFallbackReturnsEan":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetEanWithFallbackFromParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsOneForActive":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsZeroForInactive":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontReturnsCategories":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontUsesParentId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageZeroReturnsMessage":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageZeroReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageNonzeroReturnsMessage":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageNonzeroReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testTopProductIdsReturnsActiveProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testNewProductIdsReturnsProductIds":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testNewProductIdsReturnsEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testPromotedProductIdsCachedReturnsIds":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testPromotedProductIdsCachedReturnsEmptyWhenNone":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeWholeBasketAppliesDiscountToAll":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoriesOrAppliesDiscountToMatchingCategories":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoryConditionAppliesWhenConditionMet":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoryConditionNoDiscountWhenConditionNotMet":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoriesAndAppliesWhenBothConditionsMet":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportCostCachedReturnsCost":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindActiveByIdCachedReturnsTransport":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindActiveByIdCachedReturnsNullForInvalid":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testForPaymentMethodReturnsTransports":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testForPaymentMethodReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCheckProductQuantityInStockReturnsFalseOnNullBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCheckProductQuantityInStockReturnsFalseOnEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testValidateBasketReturnsEmptyArrayOnNull":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testValidateBasketReturnsBasketArrayAsIs":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsOnlyOrderRepository":0.005,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsAllDependencies":0.012,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsEmptyForEmptyQuery":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsEmptyWithoutProductRepo":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsFormattedResults":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsReturnsFalseForInvalidOrderId":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsDeletesRemovedProducts":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsUpdatesQuantityAndAdjustsStock":0.001,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsAddsNewProductAndDecreasesStock":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testGetFreeDeliveryThresholdReturnsZeroWithoutSettingsRepo":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testGetFreeDeliveryThresholdReturnsValue":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGetOrderProductReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGetOrderProductReturnsArray":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testAddOrderProductReturnsNullForInvalidOrderId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testAddOrderProductInsertsAndReturnsId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductUpdatesFields":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductReturnsFalseForEmptyData":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testDeleteOrderProductReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testDeleteOrderProductCallsDelete":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateTransportCostDoesNothingForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateTransportCostUpdatesOrder":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenNoApiKey":0.001,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenWrongApiKey":0.001,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenStoredKeyEmpty":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingEndpoint":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingAction":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns404ForUnknownEndpoint":0,"Tests\\Unit\\api\\ApiRouterTest::testSendSuccessOutputsCorrectJson":0,"Tests\\Unit\\api\\ApiRouterTest::testSendErrorOutputsCorrectJson":0,"Tests\\Unit\\api\\ApiRouterTest::testRequireMethodReturnsTrueForMatchingMethod":0,"Tests\\Unit\\api\\ApiRouterTest::testRequireMethodReturnsFalseAndSendsErrorForMismatch":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesReturnsFormattedList":0.001,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListReturnsOrders":0.001,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListPassesFiltersToRepository":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturnsOrder":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusUpdatesOrder":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidCallsServiceWhenOrderExists":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetUnpaidReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetUnpaidCallsServiceWhenOrderExists":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListReturnsProducts":0.001,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListPassesFiltersToRepository":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListDefaultPagination":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListClampsPerPageTo100":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturnsProduct":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataConvertsStatusToCheckbox":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsLanguages":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsNumericFields":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsCategories":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataPartialUpdatePreservesExisting":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsForeignKeys":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumValidFormat":0.014,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumInvalidHash":0.011,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumInvalidFormat":0.001,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testCreateBackupWithEmptyManifest":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testDownloadManifestReturnsNullForInvalidUrl":0.045,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForApiReturnsActiveAttributesWithValues":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForApiReturnsEmptyWhenNoAttributes":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantsForApiReturnsVariants":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantsForApiReturnsEmptyWhenNoVariants":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsNullForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsNullForNonexistent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForArchivedParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullWhenParentIsVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForEmptyAttributes":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForDuplicateHash":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiReturnsFalseForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiReturnsFalseForNonexistent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiFiltersUnallowedFields":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiReturnsFalseForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiReturnsFalseForNonexistent":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturnsVariantsList":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns400ForVariantProduct":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantReturns404WhenNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantSuccess":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListPassesAttributeFilters":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiCastsTypes":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSavePersistsMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveConvertsEmptyMinMaxToNull":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesNullMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWhenAllPaymentsExceedMaxAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWhenAllPaymentsBelowMinAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWithNoPaymentMethods":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontKeepsBothTransportsWhenPaymentsAvailable":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontKeepsTransportIfAtLeastOnePaymentAvailable":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataReturnsBothNamesAndColors":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataFiltersInvalidHexColors":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataReturnsEmptyOnDbFailure":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorReturnsBlackForLightColor":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorReturnsWhiteForDarkColor":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorHandlesShortHex":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorDefaultsToWhiteForInvalidHex":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlStripsDisallowedTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlStripsAttributesFromAllowedTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlPreservesCleanTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlHandlesPlainText":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsReturnsEmptyWhenNoResults":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsHandlesNullFromSelect":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testDeleteLogCallsDelete":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testClearLogsDeletesAll":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasLogsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsClearReturnsVoid":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull":0.023,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts":0.014,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataPreservesZeroBasePriceForSaveProduct":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueReturns400WhenNoBody":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRegisterHandlerAndProcessJob":0.004,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueReturnsEmptyStatsWhenNoJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerReturnsFalse":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerThrowsException":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueNoHandlerRegistered":0.001,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerReturnsArray":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueuePassesPayloadToHandler":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueMultipleJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsFromDueSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsSkipsDuplicates":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsWithPayload":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsReturnsZeroWhenNoSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRunExecutesFullPipeline":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRunReturnsScheduledCount":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueInsertsJobAndReturnsId":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithPayloadEncodesJson":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithoutPayloadDoesNotSetPayloadKey":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithScheduledAt":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueReturnsNullOnFailure":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextReturnsEmptyArrayWhenNoJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextUpdatesStatusToProcessing":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextDecodesPayloadJson":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkCompletedUpdatesStatus":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkCompletedWithResult":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedWithRetriesLeft":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedWhenMaxAttemptsReached":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedTruncatesErrorTo500Chars":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobReturnsFalseWhenNone":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobWithPayloadMatch":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testCleanupDeletesOldCompletedJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testRecoverStuckResetsProcessingJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testGetDueSchedulesReturnsEnabledSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testTouchScheduleUpdatesTimestamps":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testAllTypesReturnsAllJobTypes":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testAllStatusesReturnsAllStatuses":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testPriorityConstants":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testCalculateBackoffExponential":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testCalculateBackoffCapsAtMax":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testJobTypeConstantsMatchStrings":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testStatusConstantsMatchStrings":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsCronJobRepo":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasBulkDeletePermanentMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testBulkDeletePermanentMethodReturnType":0,"Tests\\Unit\\front\\Controllers\\ShopBasketControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\front\\Controllers\\ShopBasketControllerTest::testHasCheckoutMethods":0,"Tests\\Unit\\front\\Controllers\\ShopBasketControllerTest::testConstructorRequiresDependencies":0}} \ No newline at end of file +{"version":1,"defects":{"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":4,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsContainerWithLanguage":3,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":4,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":4,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsArticleWithRelations":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendCopyFromFallback":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testTopArticlesOrderByViews":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsListArticlesOrderByDateDesc":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenNoApiKey":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenWrongApiKey":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenStoredKeyEmpty":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingEndpoint":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingAction":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns404ForUnknownEndpoint":4,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull":3,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts":3,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeRejectsGetMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeReturns400WhenNoBody":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueRejectsGetMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueReturns400WhenNoBody":4,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextUpdatesStatusToProcessing":3,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorAcceptsRepository":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasMainViewMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testMainViewReturnsString":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateAllMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorRequiresRepository":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":3,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsTrueWhenDeleted":3,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingsReturnsArray":4,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListResultReturnsDetailedErrorWhenConfigMissing":3,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloIntegrationStatusReturnsMissingConfigMessage":3,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShopproProviderWorks":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorAcceptsDependencies":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorRequiresRepository":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasLogsMethods":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsReturnsString":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsClearReturnsVoid":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloSettingsMethods":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloDataFetchMethods":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloProductMethods":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllShopproMethods":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testApiloSettingsReturnsString":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testShopproSettingsReturnsString":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testVoidReturnTypes":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveSellasistMethods":4,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveBaselinkerMethods":4},"times":{"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsCorrectValue":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsNullWhenProductNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindReturnsProductData":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateQuantitySuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsRegularPrice":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsPromoPrice":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsRegularWhenPromoIsHigher":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetNameReturnsProductName":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetNameReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsInteger":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUnarchiveUpdatesProductAndChildren":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testArchiveUpdatesProductAndChildren":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUnarchiveReturnsBool":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testArchiveReturnsBool":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasUnarchiveMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testUnarchiveMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testConstructorRequiresProductRepository":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsBannerWithTranslations":0.001,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testDeleteReturnsTrue":0.002,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveInsertsNewBanner":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithRedis":0.001,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheRedisUnavailable":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithoutRedis":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheReturnStructure":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testCanBeInstantiated":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasSaveSettingsMethod":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasGetSettingsMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheAjaxMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasViewMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testIsNotAbstract":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testCanCreateController":0.006,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testEditMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorAcceptsRepository":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsArticleWithRelations":0.005,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsNullWhenArticleDoesNotExist":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedFilesDeletesDbRows":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedImagesDeletesDbRows":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveReturnsZeroWhenInsertFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveSetsStatusToMinusOne":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveReturnsFalseWhenUpdateFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderUpdatesImageOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderSkipsEmptyValues":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasBrowseListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasGalleryOrderSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testBrowseListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testGalleryOrderSaveMethodReturnType":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminUsesBoundParamsForTitleFilter":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveWithLegacyFormat":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveUpdatesExistingTranslationsByBannerAndLang":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testListForAdminIncludesThumbnailSrc":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testRestoreSetsStatusToZero":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListArchivedForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsUnitWithTranslations":0.001,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsNullWhenUnitNotFound":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testSaveInsertsNewUnitAndTranslationsForStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testDeleteRemovesUnitAndTranslations":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdReturnsTextFromDatabase":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdSupportsStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testAllUnitsReturnsArrayIndexedById":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsArrayOrNull":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveLanguageRejectsInvalidLanguageId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveTranslationInsertsNewTranslationAndReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDeleteTranslationReturnsBoolean":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdReturnsLanguageWithStartFlag":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdFallsBackToFirstLanguageOrPl":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsLayoutWithRelations":0.002,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testDeleteReturnsFalseWhenOnlyOneLayoutExists":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsDefaultLayoutWhenRecordDoesNotExist":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testSaveInsertsNewLayoutAndReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testListAllReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsNullForInvalidId":0.003,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSaveSettingsUpdatesHeaderAndFooter":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testDeleteTemplateReturnsFalseForAdminTemplate":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateByNameReturnsText":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsDefaultContainerForInvalidId":0.001,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsContainerWithTranslations":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDetailsForLanguageReturnsNullForInvalidData":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserWhenExists":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsErrorWhenLoginIsTaken":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsOkWhenAvailable":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveCreatesUserWithNormalizedSwitches":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithoutPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrue":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsFalseOnFailure":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsUserByLogin":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsSuccessForValidCredentials":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsNegativeOneForBlockedUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseAfterMaxAttempts":0.1,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.101,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.201,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseWhen2FADisabled":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseForInvalidEmail":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testUpdateByIdCallsDbUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorAcceptsRepository":0.003,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorRequiresDictionariesRepository":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorRequiresLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorRequiresLayoutsRepository":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorRequiresRepositoryAndRenderer":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorRequiresRepositoryAndLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasViewListMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserEditMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasTwofaMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasLoginFormMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorRequiresUserRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserReturnsDefaultsForNull":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserCastsTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserHandlesPartialData":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderUpdatesFilesOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderSkipsEmptyValues":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPagesSummaryForArticlesBuildsLabels":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testUpdateImageAltDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testMarkFileToDeleteDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindReturnsDefaultCouponForInvalidId":0.001,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindNormalizesCouponData":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveInsertsCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveUpdatesCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingsReturnsArray":0.001,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingUpdatesExistingValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingInsertsNewValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testInvalidProviderThrowsException":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testLinkProductUpdatesDatabase":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testUnlinkProductClearsFields":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloGetAccessTokenReturnsNullWithoutSettings":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListThrowsForInvalidType":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testAllPublicMethodsExist":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSettingsTableMapping":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShopproProviderWorks":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenusListReturnsArray":0.002,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenuDeleteReturnsFalseWhenMenuHasPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testGenerateSeoLinkAddsSuffixWhenBaseSlugExists":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testPageUrlPreviewBuildsLanguagePrefixedUrlForNonDefaultLanguage":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testFindReturnsDefaultPromotionForInvalidId":0.002,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testSaveInsertsPromotionAndReturnsId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageAltChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileNameChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageAltChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileNameChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloSettingsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloDataFetchMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloProductMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllShopproMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testApiloSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testShopproSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testVoidReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveSellasistMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveBaselinkerMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorRequiresPagesLanguagesAndLayoutsRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorRequiresPromotionRepository":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsNullForNegativeId":0.001,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsStatusWithIdZero":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindNormalizesNullApiloStatusId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveUpdatesColorAndApiloStatusId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveWithIdZeroWorks":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveWithEmptyApiloStatusIdSetsNull":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveRejectsNegativeId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetApiloStatusIdReturnsValue":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetApiloStatusIdReturnsNullWhenNotSet":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetByIntegrationStatusIdForApilo":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetByIntegrationStatusIdReturnsNullForUnknownIntegration":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testAllStatusesReturnsOrderedList":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testConstructorRequiresShopStatusRepository":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShouldRefreshAccessTokenReturnsFalseForFarFutureDate":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShouldRefreshAccessTokenReturnsTrueForNearExpiryDate":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListResultReturnsDetailedErrorWhenConfigMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloIntegrationStatusReturnsMissingConfigMessage":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testNormalizeApiloMapListRejectsErrorPayload":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testNormalizeApiloMapListAcceptsIdNameList":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindReturnsNullForInvalidId":0.001,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesData":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveUpdatesRowAndReturnsId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSavePreservesNonNumericApiloPaymentTypeId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllActiveReturnsNormalizedRows":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllForAdminReturnsRowsIncludingInactive":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNullForNotFound":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindKeepsNonNumericApiloPaymentTypeId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveNormalizesStatusValue":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdHandlesNullAndInt":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdReturnsStringForNonNumericValue":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testForTransportReturnsRows":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindNormalizesDataAndIncludesPaymentMethods":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindHandlesNullMaxWpAndApiloId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveInsertReturnsNewId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveUpdateReturnsExistingId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveInsertReturnsNullOnFailure":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveResetsDefaultWhenSettingNew":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveSwitchValuesNormalization":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testListForAdminWhitelistsSortColumn":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testAllActiveReturnsNormalizedRows":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetApiloCarrierAccountIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetApiloCarrierAccountIdReturnsIntOrNull":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetTransportCostReturnsFloatOrNull":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testAllForAdminReturnsAllTransports":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testConstructorRequiresPaymentMethodRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFindAttributeReturnsDefaultAttributeForInvalidId":0.002,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForAdminWhitelistsSortDirectionAndPerPage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testSaveValuesRemovesObsoleteRowsAndSetsDefault":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testSaveValuesDeletesTranslationWhenNameIsEmpty":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testGetAttributeValueByIdUsesDefaultLanguageWhenNotProvided":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSortTypesReturnsExpectedKeys":0.002,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDetailsReturnsDefaultForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDetailsLoadsTranslations":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveCategoriesOrderReturnsFalseForNonArray":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveCategoriesOrderUpdatesOrderAndParent":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveProductOrderReturnsFalseForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveProductOrderUpdatesCategoryProductOrder":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsFalseWhenHasChildren":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsTrueWhenDeleted":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryTitleReturnsEmptyWhenNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryTitleReturnsFirstAvailableTitle":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testListForAdminWhitelistsSortAndPagination":0.001,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testOrdersForClientReturnsEmptyOnMissingInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testOrdersForClientNormalizesRows":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testTotalsForClientReturnsZeroForMissingInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testTotalsForClientReturnsAggregatedValues":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testConstructorAcceptsDb":0.001,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testHasAllPublicMethods":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testSalesGridReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testLastOrdersReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testMostViewedProductsReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testBestSalesProductsReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageReturnsFallbackWhenEmpty":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsList":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyArrayWhenNone":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsDefaultsToPl":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsForDifferentLanguage":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testUnsubscribeReturnsFalseForInvalidHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testUnsubscribeDeletesSubscriber":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConfirmSubscriptionReturnsFalseForInvalidHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConfirmSubscriptionUpdatesStatus":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testGetHashByEmailReturnsHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testGetHashByEmailReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testRemoveByEmailDeletesSubscriber":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testRemoveByEmailReturnsFalseForMissing":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSignupReturnsFalseForExistingEmail":0.001,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConstructorAcceptsOptionalDependencies":0.003,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusesReturnsMappedArray":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testNextAndPrevOrderIdReturnNullForInvalidInput":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindReturnsDefaultProducerForInvalidId":0.001,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindNormalizesProducerData":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testSaveInsertsNewProducer":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testSaveUpdatesExistingProducer":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testListForAdminWhitelistsSortAndPagination":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllProducersReturnsFormattedList":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testProducerProductsReturnsPaginatedResults":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testFindReturnsDefaultSetForInvalidId":0.001,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testFindNormalizesSetData":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testSaveInsertsNewSetAndSyncsProducts":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testSaveUpdatesExistingSet":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testListForAdminWhitelistsSortAndPagination":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testAllSetsReturnsFormattedList":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testAllProductsForMassEditReturnsMap":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testAllProductsForMassEditEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetProductsByCategoryReturnsList":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetProductsByCategoryReturnsEmptyArray":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentReturnsNullForInvalidProduct":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentReturnsCorrectPrices":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentZeroPercentNullsPromo":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsAssociativeArray":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenNoSettings":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsHandlesNullFromDb":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueReturnsCorrectParam":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueUsesParamNotHardcoded":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueReturnsEmptyStringWhenNotFound":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testConstructorAcceptsDb":0.002,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasUpdateMethod":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testUpdateReturnsArray":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasRunPendingMigrationsMethod":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testRunPendingMigrationsWithNoResults":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasPrivateHelperMethods":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testHasMainViewMethod":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testMainViewReturnsString":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testConstructorRequiresRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testConstructorAcceptsRepositories":0.003,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testValidateValuesRowsReturnsErrorsForMissingDefaultLanguageAndDefaultSelection":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testValidateValuesRowsReturnsEmptyArrayForValidRows":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testHasExpectedActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testViewActionsReturnString":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testMutationActionsReturnVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testConstructorRequiresCategoryAndLanguagesRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testConstructorRequiresClientRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testConstructorAcceptsService":0.003,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testHasExpectedActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testViewActionsReturnString":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testMutationActionsReturnVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testConstructorRequiresOrderAdminService":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testConstructorAcceptsRepositories":0.003,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasMassEditActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasViewListMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasEditAndSaveMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasOperationMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasCombinationMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasImageAndFileMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testMassEditReturnsString":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testMassEditSaveReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testGetProductsByCategoryReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testConstructorRequiresRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasFormBuildingHelpers":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testSaveMethodReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testConstructorRequiresProductSetRepository":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasMainViewMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testMainViewReturnsString":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateAllMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsArticleWithRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendCopyFromFallback":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticlesIdsReturnsSortedIds":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticlesIdsReturnsNullForEmpty":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesCountReturnsInt":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesCountReturnsZeroForEmpty":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesPagination":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleNoindexReturnsBool":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleNoindexReturnsFalseForNonNoindex":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsReturnsArticlesArray":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testTopArticlesOrderByViews":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsListArticlesOrderByDateDesc":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testBannersReturnsActiveBannersWithFlatLanguages":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testBannersReturnsNullWhenNoBanners":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testMainBannerReturnsActiveBannerWithFlatLanguages":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testMainBannerReturnsNullWhenNoBanner":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testCategoryDefaultLayoutIdReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testCategoryDefaultLayoutIdReturnsNullWhenNone":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetDefaultLayoutReturnsLayoutFromDb":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetDefaultLayoutReturnsNullWhenNoLayout":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetArticleLayoutReturnsLayoutFromDb":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetArticleLayoutReturnsNullWhenNoLayout":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetActiveLayoutFallsBackToDefault":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetActiveLayoutReturnsNullWhenNothingFound":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageDetailsReturnsPageWithLanguage":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMainPageIdReturnsStartPage":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMainPageIdFallsBackToFirstActive":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageSortReturnsValue":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuDetailsReturnsMenuWithPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuDetailsReturnsNullForInvalidMenu":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuPagesReturnsEmptyForNoPages":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsContainerWithLanguage":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontAttributeDetailsReturnsAttributeWithLanguage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontAttributeDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontValueDetailsReturnsValueWithLanguage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontValueDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testSummaryWpCalculatesTotal":0.001,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testSummaryWpReturnsZeroForEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsSumsQuantities":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsReturnsZeroForEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextSingular":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextPlural2to4":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextPlural5Plus":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextCastsToInt":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testGetCategorySortReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testGetCategorySortReturnsSortType":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsTitle":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsEmptyWhenNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsCategoryWithLanguage":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsEmptyWhenCategoryNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoriesTreeReturnsEmptyWhenNoCategories":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryProductsCountReturnsZeroForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryProductsCountReturnsCount":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testProductsIdReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testProductsIdReturnsProductIds":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testBlogCategoryProductsReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testBlogCategoryProductsReturnsIds":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testPaginatedCategoryProductsReturnsEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testPaginatedCategoryProductsClampsPage":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsRowOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsStringOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesReturnsRows":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesHandlesFalseFromDb":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDetailsReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDetailsReturnsRow":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveReturnsFalseForInvalidClientId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveInsertsNewAddress":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveUpdatesExistingAddress":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testMarkAddressAsCurrentReturnsFalseForInvalidIds":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testMarkAddressAsCurrentResetsAndSets":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorOnEmptyInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorWhenClientNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsInactiveForUnconfirmedAccount":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorOnWrongPassword":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsOkOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsNullOnEmptyInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsNullWhenEmailTaken":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsIdAndHashOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationReturnsNullOnEmptyHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationActivatesAndReturnsEmail":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsNullOnEmptyHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsEmailAndPassword":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoveryReturnsNullOnEmptyEmail":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoveryReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoverySetsRecoveryFlagAndReturnsHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientOrdersReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsObjectWhenFound":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsNullForEmptyName":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsTrueForActiveCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForUsedCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForInactiveCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForNullCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableWorksWithArray":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testMarkAsUsedCallsUpdate":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testMarkAsUsedSkipsInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIncrementUsedCountCallsUpdate":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIncrementUsedCountSkipsInvalidId":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsIdWhenFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsNullForEmptyHash":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindHashByIdReturnsHashWhenFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindHashByIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendByIdReturnsArrayWithProducts":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendByHashReturnsArrayWithProducts":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGenerateOrderNumberFormatsCorrectly":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGenerateOrderNumberStartsAt001":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllActiveProducersReturnsFullData":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllActiveProducersReturnsEmptyOnNull":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsProducerWithLanguage":0,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testConstructorRequiresOrderRepository":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testConstructorRequiresProducerRepository":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveReturnsOneForActivePayment":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNormalizedData":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllActiveReturnsEmptyOnNull":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testForTransportReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackReturnsSku":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackFromParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetEanWithFallbackReturnsEan":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetEanWithFallbackFromParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsOneForActive":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsZeroForInactive":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontReturnsCategories":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontUsesParentId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageZeroReturnsMessage":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageZeroReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageNonzeroReturnsMessage":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageNonzeroReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testTopProductIdsReturnsActiveProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testNewProductIdsReturnsProductIds":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testNewProductIdsReturnsEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testPromotedProductIdsCachedReturnsIds":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testPromotedProductIdsCachedReturnsEmptyWhenNone":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeWholeBasketAppliesDiscountToAll":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoriesOrAppliesDiscountToMatchingCategories":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoryConditionAppliesWhenConditionMet":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoryConditionNoDiscountWhenConditionNotMet":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoriesAndAppliesWhenBothConditionsMet":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportCostCachedReturnsCost":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindActiveByIdCachedReturnsTransport":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindActiveByIdCachedReturnsNullForInvalid":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testForPaymentMethodReturnsTransports":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testForPaymentMethodReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCheckProductQuantityInStockReturnsFalseOnNullBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCheckProductQuantityInStockReturnsFalseOnEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testValidateBasketReturnsEmptyArrayOnNull":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testValidateBasketReturnsBasketArrayAsIs":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsOnlyOrderRepository":0.006,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsAllDependencies":0.014,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsEmptyForEmptyQuery":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsEmptyWithoutProductRepo":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsFormattedResults":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsReturnsFalseForInvalidOrderId":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsDeletesRemovedProducts":0.001,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsUpdatesQuantityAndAdjustsStock":0.001,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsAddsNewProductAndDecreasesStock":0.001,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testGetFreeDeliveryThresholdReturnsZeroWithoutSettingsRepo":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testGetFreeDeliveryThresholdReturnsValue":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGetOrderProductReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGetOrderProductReturnsArray":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testAddOrderProductReturnsNullForInvalidOrderId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testAddOrderProductInsertsAndReturnsId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductUpdatesFields":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductReturnsFalseForEmptyData":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testDeleteOrderProductReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testDeleteOrderProductCallsDelete":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateTransportCostDoesNothingForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateTransportCostUpdatesOrder":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenNoApiKey":0.001,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenWrongApiKey":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenStoredKeyEmpty":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingEndpoint":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingAction":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns404ForUnknownEndpoint":0,"Tests\\Unit\\api\\ApiRouterTest::testSendSuccessOutputsCorrectJson":0,"Tests\\Unit\\api\\ApiRouterTest::testSendErrorOutputsCorrectJson":0,"Tests\\Unit\\api\\ApiRouterTest::testRequireMethodReturnsTrueForMatchingMethod":0,"Tests\\Unit\\api\\ApiRouterTest::testRequireMethodReturnsFalseAndSendsErrorForMismatch":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesReturnsFormattedList":0.001,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListReturnsOrders":0.001,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListPassesFiltersToRepository":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturnsOrder":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusUpdatesOrder":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidCallsServiceWhenOrderExists":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetUnpaidReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetUnpaidCallsServiceWhenOrderExists":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListReturnsProducts":0.002,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListPassesFiltersToRepository":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListDefaultPagination":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListClampsPerPageTo100":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturnsProduct":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataConvertsStatusToCheckbox":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsLanguages":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsNumericFields":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsCategories":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataPartialUpdatePreservesExisting":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsForeignKeys":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumValidFormat":0.005,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumInvalidHash":0.004,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumInvalidFormat":0.001,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testCreateBackupWithEmptyManifest":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testDownloadManifestReturnsNullForInvalidUrl":0.016,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForApiReturnsActiveAttributesWithValues":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForApiReturnsEmptyWhenNoAttributes":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantsForApiReturnsVariants":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantsForApiReturnsEmptyWhenNoVariants":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsNullForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsNullForNonexistent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForArchivedParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullWhenParentIsVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForEmptyAttributes":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForDuplicateHash":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiReturnsFalseForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiReturnsFalseForNonexistent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiFiltersUnallowedFields":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiReturnsFalseForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiReturnsFalseForNonexistent":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturnsVariantsList":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns400ForVariantProduct":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantReturns404WhenNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantSuccess":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListPassesAttributeFilters":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiCastsTypes":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSavePersistsMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveConvertsEmptyMinMaxToNull":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesNullMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWhenAllPaymentsExceedMaxAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWhenAllPaymentsBelowMinAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWithNoPaymentMethods":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontKeepsBothTransportsWhenPaymentsAvailable":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontKeepsTransportIfAtLeastOnePaymentAvailable":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataReturnsBothNamesAndColors":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataFiltersInvalidHexColors":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataReturnsEmptyOnDbFailure":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorReturnsBlackForLightColor":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorReturnsWhiteForDarkColor":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorHandlesShortHex":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorDefaultsToWhiteForInvalidHex":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlStripsDisallowedTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlStripsAttributesFromAllowedTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlPreservesCleanTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlHandlesPlainText":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsReturnsEmptyWhenNoResults":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsHandlesNullFromSelect":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testDeleteLogCallsDelete":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testClearLogsDeletesAll":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasLogsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsClearReturnsVoid":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull":0.023,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts":0.014,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataPreservesZeroBasePriceForSaveProduct":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueReturns400WhenNoBody":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRegisterHandlerAndProcessJob":0.003,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueReturnsEmptyStatsWhenNoJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerReturnsFalse":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerThrowsException":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueNoHandlerRegistered":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerReturnsArray":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueuePassesPayloadToHandler":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueMultipleJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsFromDueSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsSkipsDuplicates":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsWithPayload":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsReturnsZeroWhenNoSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRunExecutesFullPipeline":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRunReturnsScheduledCount":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueInsertsJobAndReturnsId":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithPayloadEncodesJson":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithoutPayloadDoesNotSetPayloadKey":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithScheduledAt":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueReturnsNullOnFailure":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextReturnsEmptyArrayWhenNoJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextUpdatesStatusToProcessing":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextDecodesPayloadJson":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkCompletedUpdatesStatus":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkCompletedWithResult":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedWithRetriesLeft":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedWhenMaxAttemptsReached":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedTruncatesErrorTo500Chars":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobReturnsFalseWhenNone":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobWithPayloadMatch":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testCleanupDeletesOldCompletedJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testRecoverStuckResetsProcessingJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testGetDueSchedulesReturnsEnabledSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testTouchScheduleUpdatesTimestamps":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testAllTypesReturnsAllJobTypes":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testAllStatusesReturnsAllStatuses":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testPriorityConstants":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testCalculateBackoffExponential":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testCalculateBackoffCapsAtMax":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testJobTypeConstantsMatchStrings":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testStatusConstantsMatchStrings":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsCronJobRepo":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasBulkDeletePermanentMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testBulkDeletePermanentMethodReturnType":0,"Tests\\Unit\\front\\Controllers\\ShopBasketControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\front\\Controllers\\ShopBasketControllerTest::testHasCheckoutMethods":0,"Tests\\Unit\\front\\Controllers\\ShopBasketControllerTest::testConstructorRequiresDependencies":0,"Tests\\Unit\\Shared\\Security\\CsrfTokenTest::testGetTokenReturns64CharHexString":0.001,"Tests\\Unit\\Shared\\Security\\CsrfTokenTest::testGetTokenIsIdempotent":0,"Tests\\Unit\\Shared\\Security\\CsrfTokenTest::testValidateReturnsTrueForCorrectToken":0,"Tests\\Unit\\Shared\\Security\\CsrfTokenTest::testValidateReturnsFalseForEmptyString":0,"Tests\\Unit\\Shared\\Security\\CsrfTokenTest::testValidateReturnsFalseForWrongToken":0,"Tests\\Unit\\Shared\\Security\\CsrfTokenTest::testValidateReturnsFalseWhenNoSessionToken":0,"Tests\\Unit\\Shared\\Security\\CsrfTokenTest::testRegenerateChangesToken":0,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testApiloGetAccessTokenReturnsNullWithoutSettings":0.002,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testShouldRefreshAccessTokenReturnsFalseForFarFutureDate":0,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testShouldRefreshAccessTokenReturnsTrueForNearExpiryDate":0,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testApiloFetchListThrowsForInvalidType":0,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testApiloFetchListResultReturnsDetailedErrorWhenConfigMissing":0,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testApiloIntegrationStatusReturnsMissingConfigMessage":0,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testNormalizeApiloMapListRejectsErrorPayload":0,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testNormalizeApiloMapListAcceptsIdNameList":0,"Tests\\Unit\\Domain\\Integrations\\ApiloRepositoryTest::testAllPublicMethodsExist":0}} \ No newline at end of file diff --git a/.vscode/ftp-kr.diff.ver_0.338.2.zip b/.vscode/ftp-kr.diff.ver_0.338.2.zip new file mode 100644 index 0000000..b459f43 --- /dev/null +++ b/.vscode/ftp-kr.diff.ver_0.338.2.zip @@ -0,0 +1 @@ +c:\visual studio code\projekty\shopPRO\updates\0.30\ver_0.338.zip \ No newline at end of file diff --git a/.vscode/ftp-kr.diff.ver_0.338.zip b/.vscode/ftp-kr.diff.ver_0.338.zip new file mode 100644 index 0000000..b459f43 --- /dev/null +++ b/.vscode/ftp-kr.diff.ver_0.338.zip @@ -0,0 +1 @@ +c:\visual studio code\projekty\shopPRO\updates\0.30\ver_0.338.zip \ No newline at end of file diff --git a/.vscode/ftp-kr.json b/.vscode/ftp-kr.json index 80959a7..4df2b00 100644 --- a/.vscode/ftp-kr.json +++ b/.vscode/ftp-kr.json @@ -18,6 +18,7 @@ "/.serena", "/.claude", "/docs", - "/tests" + "/tests", + "/.paul" ] } diff --git a/docs/PAUL_WORKFLOW.md b/docs/PAUL_WORKFLOW.md new file mode 100644 index 0000000..bacf360 --- /dev/null +++ b/docs/PAUL_WORKFLOW.md @@ -0,0 +1,150 @@ +# PAUL — Workflow dla shopPRO + +[PAUL](https://github.com/ChristopherKahler/paul) to zestaw komend dla Claude Code, który strukturyzuje pracę nad projektem: planowanie → implementacja → weryfikacja. Działa przez slash-komendy w czacie z Claude. + +--- + +## Jednorazowa inicjalizacja + +``` +/paul:init +``` + +Uruchom raz — tworzy plik `.paul/` z konfiguracją projektu (milestones, fazy). Jeśli już zainicjalizowany, pomijasz ten krok. + +--- + +## Nowa funkcja (feature) + +### 1. Omów wizję + +``` +/paul:discuss +``` + +Claude zadaje pytania, żeby doprecyzować, co dokładnie chcesz zbudować — wynik to jasna definicja przed planowaniem. + +### 2. Zbadaj opcje techniczne (opcjonalnie) + +``` +/paul:discover +``` + +Przydatne gdy nie jesteś pewny jak zaimplementować coś technicznie — Claude przegląda kod i proponuje podejścia. + +### 3. Zaplanuj implementację + +``` +/paul:plan +``` + +Tworzy szczegółowy plan z krokami, plikami do zmiany, kolejnością. Zatwierdź plan zanim ruszysz dalej. + +### 4. Wykonaj plan + +``` +/paul:apply +``` + +Claude implementuje plan krok po kroku, z commitami na każdą fazę. + +### 5. Zweryfikuj (UAT) + +``` +/paul:verify +``` + +Claude generuje checklistę testów manualnych — punkt po punkcie sprawdzasz czy funkcja działa. + +--- + +## Poprawka błędu (bug fix) + +``` +/paul:plan-fix +``` + +Dedykowany flow dla bugów: opisujesz problem → Claude analizuje kod → tworzy plan naprawy → `/paul:apply` wykonuje. + +Krótszy wariant dla prostych bugów — możesz też po prostu opisać błąd i użyć `/paul:plan` → `/paul:apply`. + +--- + +## Zarządzanie sesjami + +| Komenda | Kiedy | +|---------|-------| +| `/paul:progress` | Sprawdź gdzie jesteś, co dalej | +| `/paul:pause` | Kończysz sesję — tworzy plik handoff | +| `/paul:resume` | Zaczynasz nową sesję — wczytuje kontekst | +| `/paul:handoff` | Generuje dokument przekazania pracy | + +--- + +## Milestones (większe wdrożenia) + +Gdy planujesz większą wersję (np. nowy moduł, refactor): + +``` +/paul:milestone # Tworzysz nowy milestone +/paul:add-phase # Dodajesz fazę do milestone +/paul:complete-milestone # Zamykasz po wdrożeniu +``` + +--- + +## Czy PAUL może przejrzeć projekt pod kątem błędów? + +Tak, częściowo. Najlepsze podejście: + +### Mapowanie kodu + +``` +/paul:map-codebase +``` + +Generuje analizę architektury — wyłapuje niespójności, martwy kod, problematyczne zależności. + +### Research konkretnego obszaru + +``` +/paul:research +``` + +Np. _"Sprawdź czy walidacja zamówień w OrderRepository jest kompletna"_ — Claude przegląda kod i raportuje problemy. + +### Ograniczenia + +PAUL nie jest narzędziem do statycznej analizy kodu (jak PHPStan czy psalm). Do systematycznego przeglądu błędów lepiej połączyć: + +- **PAUL** (`/paul:map-codebase` + `/paul:research`) — problemy architektoniczne, logika biznesowa +- **Testy** (`./test.ps1`) — regresje, złe zachowanie metod +- **PHPStan** (jeśli dodany do projektu) — typy, niezdefiniowane zmienne + +--- + +## Typowy dzień pracy w shopPRO + +``` +# Rano — wznów kontekst +/paul:resume + +# W trakcie — sprawdź co dalej +/paul:progress + +# Nowa funkcja lub bug +/paul:discuss → /paul:plan → /paul:apply → /paul:verify + +# Na koniec dnia +/paul:pause +``` + +--- + +## Dobre praktyki + +- Zawsze zatwierdzaj plan (`/paul:plan`) zanim uruchomisz `/paul:apply` — łatwiej zmienić plan niż cofnąć kod +- Po każdym `/paul:apply` odpal testy: `./test.ps1` +- Używaj `/paul:verify` przed mergem — generuje checklistę zamiast zgadywać co przetestować +- Przy bugach najpierw opisz symptom dokładnie, Claude lepiej planuje naprawę mając konkretny przypadek +