UPDATE
This commit is contained in:
115
.paul/PROJECT.md
Normal file
115
.paul/PROJECT.md
Normal file
@@ -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*
|
||||
62
.paul/ROADMAP.md
Normal file
62
.paul/ROADMAP.md
Normal file
@@ -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*
|
||||
37
.paul/SPECIAL-FLOWS.md
Normal file
37
.paul/SPECIAL-FLOWS.md
Normal file
@@ -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*
|
||||
58
.paul/STATE.md
Normal file
58
.paul/STATE.md
Normal file
@@ -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*
|
||||
24
.paul/codebase/README.md
Normal file
24
.paul/codebase/README.md
Normal file
@@ -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)
|
||||
235
.paul/codebase/architecture.md
Normal file
235
.paul/codebase/architecture.md
Normal file
@@ -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)
|
||||
<h1><?= $this->category['name'] ?></h1>
|
||||
```
|
||||
|
||||
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`)
|
||||
127
.paul/codebase/concerns.md
Normal file
127
.paul/codebase/concerns.md
Normal file
@@ -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 |
|
||||
198
.paul/codebase/conventions.md
Normal file
198
.paul/codebase/conventions.md
Normal file
@@ -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
|
||||
<h1><?= $this->varName ?></h1>
|
||||
|
||||
// XSS escape
|
||||
<span><?= $tpl->secureHTML($this->userInput) ?></span>
|
||||
```
|
||||
|
||||
## 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
|
||||
<?= $tpl->secureHTML($this->categoryName) ?>
|
||||
|
||||
// Or use htmlspecialchars directly
|
||||
<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
65
.paul/codebase/dependencies.md
Normal file
65
.paul/codebase/dependencies.md
Normal file
@@ -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
|
||||
72
.paul/codebase/overview.md
Normal file
72
.paul/codebase/overview.md
Normal file
@@ -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`
|
||||
141
.paul/codebase/stack.md
Normal file
141
.paul/codebase/stack.md
Normal file
@@ -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
|
||||
245
.paul/codebase/testing.md
Normal file
245
.paul/codebase/testing.md
Normal file
@@ -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)
|
||||
246
.paul/phases/04-csrf-protection/04-01-PLAN.md
Normal file
246
.paul/phases/04-csrf-protection/04-01-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## 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)
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Utwórz klasę CsrfToken + test jednostkowy</name>
|
||||
<files>autoload/Shared/Security/CsrfToken.php, tests/Unit/Shared/Security/CsrfTokenTest.php</files>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>./test.ps1 tests/Unit/Shared/Security/CsrfTokenTest.php</verify>
|
||||
<done>AC-5 satisfied: token unikalny, 64 znaki, idempotentny</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Integracja CSRF w formularzach edycji (form-edit.php + FormRequestHandler)</name>
|
||||
<files>admin/templates/components/form-edit.php, autoload/admin/Support/Forms/FormRequestHandler.php</files>
|
||||
<action>
|
||||
**1. form-edit.php** — dodaj token CSRF jako hidden field zaraz po `_form_id`:
|
||||
```php
|
||||
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
|
||||
```
|
||||
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ą.
|
||||
</action>
|
||||
<verify>
|
||||
Ręcznie: sprawdź źródło strony formularza edycji — musi zawierać input[name="_csrf_token"].
|
||||
Testy: ./test.ps1 (suite nie powinna się zepsuć).
|
||||
</verify>
|
||||
<done>AC-1 i AC-2 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: CSRF w formularzach logowania i special_actions</name>
|
||||
<files>admin/templates/site/unlogged-layout.php, admin/templates/users/user-2fa.php, autoload/admin/App.php</files>
|
||||
<action>
|
||||
**1. unlogged-layout.php** — dodaj hidden field CSRF do formularza logowania (zaraz po `s-action`):
|
||||
```php
|
||||
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
|
||||
```
|
||||
|
||||
**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).
|
||||
</action>
|
||||
<verify>
|
||||
Ręcznie: sprawdź źródło strony logowania — musi zawierać input[name="_csrf_token"].
|
||||
./test.ps1 (suite nie powinna się zepsuć).
|
||||
</verify>
|
||||
<done>AC-3 i AC-4 satisfied</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Po zakończeniu utwórz `.paul/phases/04-csrf-protection/04-01-SUMMARY.md`
|
||||
</output>
|
||||
119
.paul/phases/04-csrf-protection/04-01-SUMMARY.md
Normal file
119
.paul/phases/04-csrf-protection/04-01-SUMMARY.md
Normal file
@@ -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*
|
||||
46
.paul/phases/05-order-bugs-fix/05-01-FIX-SUMMARY.md
Normal file
46
.paul/phases/05-order-bugs-fix/05-01-FIX-SUMMARY.md
Normal file
@@ -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 |
|
||||
313
.paul/phases/05-order-bugs-fix/05-01-FIX.md
Normal file
313
.paul/phases/05-order-bugs-fix/05-01-FIX.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
## 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.
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Fix BUG-1: Guard summaryView() against re-submission after successful order</name>
|
||||
<files>autoload/front/Controllers/ShopBasketController.php</files>
|
||||
<action>
|
||||
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();
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
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
|
||||
</verify>
|
||||
<done>AC-1 satisfied: navigating back to summary after successful order redirects, no form shown</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Fix BUG-1: Wrap createFromBasket in try-catch in basketSave()</name>
|
||||
<files>autoload/front/Controllers/ShopBasketController.php</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
Confirm no PHP syntax errors: `php -l autoload/front/Controllers/ShopBasketController.php`
|
||||
</verify>
|
||||
<done>AC-2 satisfied: exceptions from createFromBasket are caught and handled gracefully</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Fix BUG-2: Add is_cod column migration</name>
|
||||
<files>migrations/0.338.sql, docs/DATABASE_STRUCTURE.md</files>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
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.
|
||||
</verify>
|
||||
<done>AC-3 precondition: column definition prepared for migration</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Fix BUG-2: Add is_cod to PaymentMethodRepository normalization and queries</name>
|
||||
<files>autoload/Domain/PaymentMethod/PaymentMethodRepository.php</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
`php -l autoload/Domain/PaymentMethod/PaymentMethodRepository.php` — no syntax errors.
|
||||
All explicit SQL SELECTs in this file now include `is_cod`.
|
||||
</verify>
|
||||
<done>AC-3 + AC-4 precondition: repository returns is_cod field</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Fix BUG-2: Add is_cod switch to admin payment method form</name>
|
||||
<files>autoload/admin/Controllers/ShopPaymentMethodController.php</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
`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.
|
||||
</verify>
|
||||
<done>AC-3 satisfied: admin can set is_cod flag on any payment method</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Fix BUG-2: Use is_cod flag instead of hardcoded payment_id == 3 in OrderRepository</name>
|
||||
<files>autoload/Domain/Order/OrderRepository.php</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
`php -l autoload/Domain/Order/OrderRepository.php` — no syntax errors.
|
||||
Confirm the old `$payment_id == 3` no longer exists in createFromBasket().
|
||||
</verify>
|
||||
<done>AC-4 satisfied: COD status promotion is driven by is_cod flag, not hardcoded ID</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-action" gate="blocking">
|
||||
<action>Run the database migration on production server</action>
|
||||
<instructions>
|
||||
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).
|
||||
</instructions>
|
||||
<verification>
|
||||
Claude will verify the code changes are in place. The DB migration must be confirmed by you.
|
||||
</verification>
|
||||
<resume-signal>Type "done" when migration and admin flag set</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
## 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
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
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`
|
||||
</output>
|
||||
188
.paul/phases/06-integrations-refactoring/06-01-PLAN.md
Normal file
188
.paul/phases/06-integrations-refactoring/06-01-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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)
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@autoload/Domain/Integrations/IntegrationsRepository.php
|
||||
@tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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ą
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Utwórz ApiloRepository — ekstrakcja metod Apilo</name>
|
||||
<files>autoload/Domain/Integrations/ApiloRepository.php</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
php -l autoload/Domain/Integrations/ApiloRepository.php zwraca "No syntax errors"
|
||||
Klasa ma dokładnie 8 publicznych metod apilo* + 6 prywatnych helperów.
|
||||
</verify>
|
||||
<done>AC-1 i AC-2 spełnione</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Utwórz ApiloRepositoryTest</name>
|
||||
<files>tests/Unit/Domain/Integrations/ApiloRepositoryTest.php</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
./test.ps1 tests/Unit/Domain/Integrations/ApiloRepositoryTest.php — wszystkie testy green
|
||||
./test.ps1 — pełna suite green (817+ testów, brak regresji)
|
||||
</verify>
|
||||
<done>AC-3 i AC-4 spełnione</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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.
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/06-integrations-refactoring/06-01-SUMMARY.md`
|
||||
</output>
|
||||
104
.paul/phases/06-integrations-refactoring/06-01-SUMMARY.md
Normal file
104
.paul/phases/06-integrations-refactoring/06-01-SUMMARY.md
Normal file
@@ -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*
|
||||
296
.paul/phases/06-integrations-refactoring/06-02-PLAN.md
Normal file
296
.paul/phases/06-integrations-refactoring/06-02-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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*
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Zaktualizuj IntegrationsController i App.php</name>
|
||||
<files>autoload/admin/Controllers/IntegrationsController.php, autoload/admin/App.php</files>
|
||||
<action>
|
||||
**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 )
|
||||
);
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
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ń
|
||||
</verify>
|
||||
<done>AC-1 spełnione</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Zaktualizuj OrderAdminService i cron.php</name>
|
||||
<files>autoload/Domain/Order/OrderAdminService.php, cron.php</files>
|
||||
<action>
|
||||
**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)
|
||||
</action>
|
||||
<verify>
|
||||
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
|
||||
</verify>
|
||||
<done>AC-2 spełnione</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Usuń metody apilo* z IntegrationsRepository + cleanup testów</name>
|
||||
<files>autoload/Domain/Integrations/IntegrationsRepository.php, tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php</files>
|
||||
<action>
|
||||
**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).
|
||||
</action>
|
||||
<verify>
|
||||
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
|
||||
</verify>
|
||||
<done>AC-3 i AC-4 spełnione</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/06-integrations-refactoring/06-02-SUMMARY.md`
|
||||
</output>
|
||||
99
.paul/phases/06-integrations-refactoring/06-02-SUMMARY.md
Normal file
99
.paul/phases/06-integrations-refactoring/06-02-SUMMARY.md
Normal file
@@ -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*
|
||||
File diff suppressed because one or more lines are too long
1
.vscode/ftp-kr.diff.ver_0.338.2.zip
vendored
Normal file
1
.vscode/ftp-kr.diff.ver_0.338.2.zip
vendored
Normal file
@@ -0,0 +1 @@
|
||||
c:\visual studio code\projekty\shopPRO\updates\0.30\ver_0.338.zip
|
||||
1
.vscode/ftp-kr.diff.ver_0.338.zip
vendored
Normal file
1
.vscode/ftp-kr.diff.ver_0.338.zip
vendored
Normal file
@@ -0,0 +1 @@
|
||||
c:\visual studio code\projekty\shopPRO\updates\0.30\ver_0.338.zip
|
||||
3
.vscode/ftp-kr.json
vendored
3
.vscode/ftp-kr.json
vendored
@@ -18,6 +18,7 @@
|
||||
"/.serena",
|
||||
"/.claude",
|
||||
"/docs",
|
||||
"/tests"
|
||||
"/tests",
|
||||
"/.paul"
|
||||
]
|
||||
}
|
||||
|
||||
150
docs/PAUL_WORKFLOW.md
Normal file
150
docs/PAUL_WORKFLOW.md
Normal file
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user