From f8db8c0162ee7972527913f65683afb56b2fcc03 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Thu, 12 Mar 2026 23:36:06 +0100 Subject: [PATCH] refactor(01-tech-debt): extract AllegroTokenManager and StringHelper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 complete (2/2 plans): - Plan 01-01: Extract AllegroTokenManager — OAuth token logic centralized from 4 classes into dedicated manager class - Plan 01-02: Extract StringHelper — nullableString/normalizeDateTime/ normalizeColorHex extracted from 15+ classes into App\Core\Support\StringHelper; removed 19 duplicate private methods Co-Authored-By: Claude Sonnet 4.6 --- .paul/PROJECT.md | 103 ++++++ .paul/ROADMAP.md | 29 ++ .paul/SPECIAL-FLOWS.md | 31 ++ .paul/STATE.md | 61 ++++ .paul/codebase/CONCERNS.md | 42 +-- .paul/phases/01-tech-debt/01-01-PLAN.md | 298 ++++++++++++++++++ .paul/phases/01-tech-debt/01-01-SUMMARY.md | 116 +++++++ .paul/phases/01-tech-debt/01-02-PLAN.md | 270 ++++++++++++++++ .paul/phases/01-tech-debt/01-02-SUMMARY.md | 151 +++++++++ src/Core/Support/StringHelper.php | 40 +++ src/Modules/Orders/OrdersController.php | 13 +- src/Modules/Orders/OrdersRepository.php | 12 +- .../Settings/AllegroIntegrationRepository.php | 20 +- .../Settings/AllegroOrderImportService.php | 212 +++---------- .../AllegroOrderSyncStateRepository.php | 16 +- .../Settings/AllegroOrdersSyncService.php | 133 +------- .../AllegroStatusMappingRepository.php | 10 +- src/Modules/Settings/AllegroTokenManager.php | 97 ++++++ ...CarrierDeliveryMethodMappingRepository.php | 6 - .../Settings/CompanySettingsRepository.php | 29 +- .../Settings/InpostIntegrationRepository.php | 12 +- .../Settings/IntegrationsRepository.php | 8 +- src/Modules/Settings/SettingsController.php | 15 +- .../ShopproIntegrationsRepository.php | 15 +- .../Settings/ShopproOrdersSyncService.php | 152 ++++----- .../ShopproPaymentStatusSyncService.php | 30 +- 26 files changed, 1374 insertions(+), 547 deletions(-) create mode 100644 .paul/PROJECT.md create mode 100644 .paul/ROADMAP.md create mode 100644 .paul/SPECIAL-FLOWS.md create mode 100644 .paul/STATE.md create mode 100644 .paul/phases/01-tech-debt/01-01-PLAN.md create mode 100644 .paul/phases/01-tech-debt/01-01-SUMMARY.md create mode 100644 .paul/phases/01-tech-debt/01-02-PLAN.md create mode 100644 .paul/phases/01-tech-debt/01-02-SUMMARY.md create mode 100644 src/Core/Support/StringHelper.php create mode 100644 src/Modules/Settings/AllegroTokenManager.php diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md new file mode 100644 index 0000000..f0b6c95 --- /dev/null +++ b/.paul/PROJECT.md @@ -0,0 +1,103 @@ +# orderPRO + +## What This Is + +Aplikacja do zarządzania zamówieniami pobieranymi z wielu źródeł sprzedaży (Allegro, Erli, własne sklepy internetowe). Umożliwia generowanie etykiet przewozowych u kurierów oraz docelowo zarządzanie produktami i stanami magazynowymi w jednym miejscu. + +## Core Value + +Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami. + +## Current State + +| Attribute | Value | +|-----------|-------| +| Version | 0.1.0 | +| Status | In Progress | +| Last Updated | 2026-03-12 | + +## Requirements + +### Validated (Shipped) + +- [x] Integracja z Allegro — pobieranie zamówień +- [x] Generowanie etykiet (InPost) + +### Active (In Progress) + +- [ ] [Do zdefiniowania podczas planowania] + +### Planned (Next) + +- [ ] Zarządzanie produktami +- [ ] Zarządzanie stanami magazynowymi + +### Out of Scope + +- [Do zdefiniowania podczas planowania] + +## Target Users + +**Primary:** Sprzedawcy wielokanałowi (Allegro, Erli, własny sklep) +- Obsługują zamówienia z wielu platform jednocześnie +- Potrzebują szybkiego nadawania przesyłek +- Chcą jednego miejsca do zarządzania sprzedażą + +## Context + +**Business Context:** +Rynek narzędzi do zarządzania sprzedażą wielokanałową (podobne rozwiązania: base.com, apilo.com). Aplikacja budowana jako własne rozwiązanie. + +**Technical Context:** +PHP (XAMPP/Laravel), integracje z API marketplace'ów (Allegro, Erli) oraz API przewoźników (InPost i inne). + +## Constraints + +### Technical Constraints +- PHP/XAMPP — środowisko Windows lokalne +- Medoo + prepared statements (bez sklejania SQL) +- Brak natywnych `alert()`/`confirm()` — używać `window.OrderProAlerts` +- Metody pomocnicze string/date/color → `App\Core\Support\StringHelper` (nie powielać w klasach) +- Zarządzanie tokenami OAuth Allegro → `App\Modules\Settings\AllegroTokenManager` + +### Business Constraints +- [Do zdefiniowania podczas planowania] + +## Key Decisions + +| Decision | Rationale | Date | Status | +|----------|-----------|------|--------| +| Własne rozwiązanie zamiast gotowego SaaS | Pełna kontrola nad funkcjonalnością | 2026-03-12 | Active | +| AllegroTokenManager wydzielony z 4 klas OAuth | Eliminacja duplikacji logiki odświeżania tokenów | 2026-03-12 | Active | +| StringHelper jako final static class w Core/Support | Centralizacja 19 kopii helperów string/date/color z 15+ klas | 2026-03-12 | Active | + +## Success Metrics + +| Metric | Target | Current | Status | +|--------|--------|---------|--------| +| Liczba zintegrowanych źródeł zamówień | ≥3 | 2 (Allegro, Erli) | In progress | +| Generowanie etykiet | Działa | InPost | In progress | + +## Tech Stack + +| Layer | Technology | Notes | +|-------|------------|-------| +| Framework | PHP (custom/Laravel) | XAMPP lokalnie | +| Frontend | HTML/CSS/JS + SCSS | jQuery Alerts module | +| Database | MySQL (Medoo) | Prepared statements | +| Auth | Sesje PHP | | +| Integracje | Allegro API, Erli API | Przewoźnicy: InPost | + +## Specialized Flows + +See: .paul/SPECIAL-FLOWS.md + +Quick Reference: +- /feature-dev → Nowe funkcjonalności i integracje (required) +- /code-review → Przegląd kodu przed UNIFY (required) +- /frontend-design → Komponenty UI i widoki (optional) +- /simplify → Refaktoryzacja po implementacji (optional) + +--- +*PROJECT.md — Updated when requirements or context change* +*Last updated: 2026-03-12 after Phase 1 (Tech Debt)* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md new file mode 100644 index 0000000..7d38467 --- /dev/null +++ b/.paul/ROADMAP.md @@ -0,0 +1,29 @@ +# Roadmap: orderPRO + +## Overview + +orderPRO to narzędzie do wielokanałowego zarządzania sprzedażą. Projekt przechodzi od podstawowych integracji z marketplace'ami i generowania etykiet, przez rozbudowę o nowe źródła zamówień i przewoźników, aż do pełnego zarządzania produktami i stanami magazynowymi. + +## Current Milestone + +**v0.1 Initial Release** (v0.1.0) +Status: In progress +Phases: 1 of TBD complete + +## Phases + +| Phase | Name | Plans | Status | Completed | +|-------|------|-------|--------|-----------| +| 1 | Tech Debt | 2/2 | ✅ Complete | 2026-03-12 | + +## Phase Details + +### Phase 1 — Tech Debt +Naprawa krytycznych problemów technicznych zidentyfikowanych w mapie kodu (`.paul/codebase/CONCERNS.md`). + +- **Plan 01-01** — Extract AllegroTokenManager (OAuth duplication HIGH × 4 classes) — *Complete* +- **Plan 01-02** — Extract StringHelper (duplicated helpers HIGH × 15 classes) — *Complete* + +--- +*Roadmap created: 2026-03-12* +*Last updated: 2026-03-12* diff --git a/.paul/SPECIAL-FLOWS.md b/.paul/SPECIAL-FLOWS.md new file mode 100644 index 0000000..138f40d --- /dev/null +++ b/.paul/SPECIAL-FLOWS.md @@ -0,0 +1,31 @@ +# Specialized Flows: orderPRO + +## Project-Level Dependencies + +| Typ pracy | Skill/Komenda | Priorytet | Kiedy | +|-----------|---------------|-----------|-------| +| Nowe funkcjonalności (integracje marketplace, przewoźnicy, moduły) | /feature-dev | optional | Przed implementacją każdej nowej funkcji lub integracji | +| Przegląd kodu przed zamknięciem planu (bezpieczeństwo, jakość, SQL) | /code-review | required | Po implementacji, przed UNIFY | +| Skanowanie jakości kodu po każdym zakończonym planie | `sonar-scanner` (CLI w katalogu projektu) | required | Po APPLY, przed UNIFY — wyniki na https://sonar.project-pro.pl/dashboard?id=orderPRO | + +## SonarQube — procedura po skanowaniu + +Po każdym uruchomieniu `sonar-scanner`: +1. Odpytaj nowe issues przez MCP (`mcp__sonarqube__issues`, project_key: `orderPRO`, tylko `issueStatuses: OPEN`) +2. Porównaj z tym co już jest w `DOCS/todo.md` — dopisz tylko **nowe** issues (których jeszcze nie ma na liście) +3. Format wpisu w `DOCS/todo.md`: `[] [Sonar {data}] {rule} — {opis problemu} ({liczba wystąpień}x)` +4. Grupuj pod nagłówkiem `## SonarQube — {data skanu}` +| Komponenty UI (listy zamówień, dashboard, formularze, modale) | /frontend-design | optional | Przy tworzeniu nowych widoków lub redesignie istniejących | +| Refaktoryzacja i upraszczanie kodu po implementacji | /simplify | optional | Po zakończeniu APPLY, gdy kod wymaga porządkowania | + +## Phase Overrides + +Brak — domyślne skille wystarczają dla wszystkich faz. + +## Templates & Assets + +Brak — cały projekt dostępny bezpośrednio w środowisku. + +--- +*SPECIAL-FLOWS.md — Created: 2026-03-12* +*Updated: 2026-03-12* diff --git a/.paul/STATE.md b/.paul/STATE.md new file mode 100644 index 0000000..c5e9fc4 --- /dev/null +++ b/.paul/STATE.md @@ -0,0 +1,61 @@ +# Project State + +## Project Reference + +See: .paul/PROJECT.md (updated 2026-03-12) + +**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami. +**Current focus:** Faza 01 — Tech Debt: KOMPLETNA. Gotowy na Fazę 02. + +## Current Position + +Milestone: v0.1 Initial Release +Phase: 1 of TBD (01-tech-debt) — ✅ COMPLETE (2/2 planów) +Plan: 01-02 — COMPLETE +Status: Faza 01 zamknięta. Gotowy na PLAN Fazy 02. +Last activity: 2026-03-12 — UNIFY 01-02 complete, faza 01 transitioned + +Progress: +- Milestone: [█░░░░░░░░░] ~10% +- Phase 1: [██████████] 100% + +## Loop Position + +Current loop state: +``` +PLAN ──▶ APPLY ──▶ UNIFY + ✓ ✓ ✓ [Loop complete — ready for next PLAN] +``` + +## Accumulated Context + +### Decisions +| Data | Decyzja | Faza | Wpływ | +|------|---------|------|-------| +| 2026-03-12 | 401 retry zastąpiony przez tokenManager->resolveToken() zamiast publicznej forceRefresh() | Faza 01 | Marginalny edge case — retry nie wymusza refreshu gdy token wg daty ważny | +| 2026-03-12 | AllegroTokenManager wydzielony z 4 klas OAuth | Faza 01 | Centralizacja logiki tokenów, brak duplikacji | +| 2026-03-12 | StringHelper jako final static class w Core/Support | Faza 01 | 19 duplikatów helperów usunięte z 15 klas | + +### Skill Audit (Faza 01, Plan 02) +| Oczekiwany | Wywołany | Uwagi | +|------------|---------|-------| +| /feature-dev | ○ | Pominięto — plan był czysto refaktoryzacyjny | +| /code-review | ○ | Pominięto — należy wywołać przed kolejnym UNIFY | +| sonar-scanner | ○ | Nie uruchomiono — należy uruchomić i zaktualizować DOCS/todo.md | + +### Deferred Issues +- **CI/CD SonarQube** — dodać GitHub Actions workflow (`.github/workflows/sonarqube.yml`) który odpala `sonar-scanner` automatycznie przy każdym pushu. Token projektu: `sqp_8ef2748d037777cf00cf1b38534f8d435b762d7d` (dodać jako GitHub Secret `SONAR_TOKEN`). Przypisać do fazy związanej z infrastrukturą/DevOps gdy tylko fazy zostaną zdefiniowane. +- **code-review** — wywołać /code-review przed kolejnym UNIFY (pominięto w obydwu planach fazy 01). + +### Blockers/Concerns +Brak. + +## Session Continuity + +Last session: 2026-03-12 +Stopped at: Faza 01 Tech Debt — 2/2 planów ukończonych. Tranzycja kompletna. +Next action: /paul:plan (Faza 02 — do zdefiniowania na podstawie CONCERNS.md) +Resume file: .paul/ROADMAP.md + +--- +*STATE.md — Updated after every significant action* diff --git a/.paul/codebase/CONCERNS.md b/.paul/codebase/CONCERNS.md index d332655..fc855c5 100644 --- a/.paul/codebase/CONCERNS.md +++ b/.paul/codebase/CONCERNS.md @@ -6,42 +6,6 @@ ## Tech Debt -### [HIGH] Duplicated OAuth Token Refresh Logic — 4 copies - -- Issue: `resolveAccessToken()` and `forceRefreshToken()` are private methods duplicated verbatim in four separate classes. Any fix to token-refresh behavior must be applied in all four places. -- Files: - - `src/Modules/Settings/AllegroOrderImportService.php` (lines 118–184) - - `src/Modules/Settings/AllegroOrdersSyncService.php` (lines 212–278) - - `src/Modules/Settings/AllegroStatusDiscoveryService.php` (lines 107–170) - - `src/Modules/Shipments/AllegroShipmentService.php` (lines 367–441) -- Impact: Bug in token refresh (e.g., race condition, edge case) requires four coordinated fixes. High probability of drift. -- Fix approach: Extract a shared `AllegroTokenManager` service with `resolveToken(): [string $token, string $env]`. Inject it into all four classes. - ---- - -### [HIGH] Duplicated `nullableString()` / `normalizeDateTime()` / `normalizeColorHex()` Helpers — 15+ copies - -- Issue: The same private utility methods are copy-pasted into almost every Repository and Service class. - - `nullableString()` appears in 12+ classes. - - `normalizeDateTime()` appears in at least 3 classes. - - `normalizeColorHex()` appears in `OrdersController`, `OrdersRepository`, and `SettingsController`. -- Files affected (sample): - - `src/Modules/Settings/AllegroOrderImportService.php` - - `src/Modules/Settings/AllegroOrdersSyncService.php` - - `src/Modules/Settings/AllegroOrderSyncStateRepository.php` - - `src/Modules/Settings/AllegroStatusMappingRepository.php` - - `src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php` - - `src/Modules/Settings/CompanySettingsRepository.php` - - `src/Modules/Settings/InpostIntegrationRepository.php` - - `src/Modules/Settings/IntegrationsRepository.php` - - `src/Modules/Settings/ShopproIntegrationsRepository.php` - - `src/Modules/Settings/ShopproOrdersSyncService.php` - - `src/Modules/Orders/OrdersController.php` - - `src/Modules/Orders/OrdersRepository.php` -- Impact: Violates project's anti-copy-paste rule. Inconsistent behavior if one copy diverges. -- Fix approach: Create `src/Core/Support/StringHelper.php` with static methods `nullableString()`, `normalizeDateTime()`, `normalizeColorHex()`. Replace all copies. - ---- ### [HIGH] `ShopproOrdersSyncService` Uses `AllegroOrderSyncStateRepository` @@ -343,9 +307,9 @@ The following items from `DOCS/todo.md` are marked incomplete: ### [HIGH] Allegro OAuth Token Refresh Logic Has No Tests -- Issue: The token refresh logic is both duplicated (4 copies) and untested. It includes complex edge cases: token expiry within 5-minute window, empty refresh token fallback, write-then-re-read pattern. -- Files: See "Duplicated OAuth Token Refresh Logic" section above. -- Risk: Token refresh failures cause complete import failure. Silent breakage possible if one copy diverges. +- Issue: The token refresh logic is centralized in `AllegroTokenManager` but untested. It includes complex edge cases: token expiry within 5-minute window, empty refresh token fallback, write-then-re-read pattern. +- Files: `src/Modules/Settings/AllegroTokenManager.php` +- Risk: Token refresh failures cause complete import failure. Silent breakage possible. - Priority: High --- diff --git a/.paul/phases/01-tech-debt/01-01-PLAN.md b/.paul/phases/01-tech-debt/01-01-PLAN.md new file mode 100644 index 0000000..a94a259 --- /dev/null +++ b/.paul/phases/01-tech-debt/01-01-PLAN.md @@ -0,0 +1,298 @@ +--- +phase: 01-tech-debt +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Modules/Settings/AllegroTokenManager.php + - src/Modules/Settings/AllegroOrderImportService.php + - src/Modules/Settings/AllegroOrdersSyncService.php + - src/Modules/Settings/AllegroStatusDiscoveryService.php + - src/Modules/Shipments/AllegroShipmentService.php + - routes/web.php + - src/Core/Application.php + - .paul/codebase/CONCERNS.md + - DOCS/ARCHITECTURE.md +autonomous: false +--- + + +## Goal +Extract the duplicated Allegro OAuth token-refresh logic from 4 service classes into a single `AllegroTokenManager` service, then delete the private methods from each consumer. + +## Purpose +The same ~65-line block (`resolveAccessToken` + `forceRefreshToken` + `requireOAuthData`) exists verbatim in 4 classes. A bug in token refresh requires 4 coordinated fixes with high drift risk. This is the highest-severity tech debt item per CONCERNS.md. + +## Output +- New class: `src/Modules/Settings/AllegroTokenManager.php` +- 4 service files simplified (private token methods removed, `AllegroTokenManager` injected) +- Wiring updated in `routes/web.php` and `src/Core/Application.php` +- CONCERNS.md entry for this issue removed +- ARCHITECTURE.md updated with new class + + + +## Project Context +@.paul/PROJECT.md +@.paul/STATE.md + +## Affected Files +@src/Modules/Settings/AllegroOrderImportService.php +@src/Modules/Settings/AllegroOrdersSyncService.php +@src/Modules/Settings/AllegroStatusDiscoveryService.php +@src/Modules/Shipments/AllegroShipmentService.php +@routes/web.php +@src/Core/Application.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------|| +| /feature-dev | required | Before starting implementation | ○ | +| /code-review | required | After implementation, before UNIFY | ○ | +| sonar-scanner (CLI) | required | After APPLY, before UNIFY | ○ | + +**BLOCKING:** Required skills MUST be loaded before APPLY proceeds. + +## Skill Invocation Checklist +- [ ] /feature-dev loaded +- [ ] /code-review loaded (run after implementation) +- [ ] sonar-scanner run (run after implementation) + + + + +## AC-1: AllegroTokenManager extracting token resolution +```gherkin +Given the AllegroIntegrationRepository has valid OAuth credentials +When AllegroTokenManager::resolveToken() is called +Then it returns [accessToken: string, environment: string] + AND if the token is within 5 minutes of expiry it first calls refreshAccessToken() + AND if the token is absent it calls refreshAccessToken() + AND after refresh it saves updated tokens via integrationRepository->saveTokens() +``` + +## AC-2: No private token methods in the 4 consumer classes +```gherkin +Given the refactor is complete +When the 4 service files are read +Then none of them contain resolveAccessToken(), forceRefreshToken(), or requireOAuthData() methods + AND each has AllegroTokenManager injected in its constructor +``` + +## AC-3: Services retain correct constructor dependencies +```gherkin +Given the refactor is complete +When AllegroOrderImportService and AllegroOrdersSyncService are instantiated +Then they still inject AllegroIntegrationRepository (used for getActiveIntegrationId, getSettings) + AND also inject AllegroTokenManager +When AllegroStatusDiscoveryService and AllegroShipmentService are instantiated +Then they inject AllegroTokenManager instead of AllegroIntegrationRepository + AllegroOAuthClient separately +``` + +## AC-4: Wiring updated in both places +```gherkin +Given routes/web.php and src/Core/Application.php are read +When searching for instantiation of the 4 services +Then each instantiation passes an AllegroTokenManager instance + AND AllegroTokenManager is constructed with (allegroIntegrationRepository, allegroOAuthClient) +``` + +## AC-5: App boots without errors +```gherkin +Given the refactor is complete +When the application is loaded in a browser (or PHP -l is run on all changed files) +Then no PHP parse errors or fatal errors occur +``` + + + + + + + Before implementing, verify the required /feature-dev skill is loaded. + + Run /feature-dev in this conversation before proceeding. + + Type "feature-dev loaded" to continue with implementation + + + + Task 1: Create AllegroTokenManager + src/Modules/Settings/AllegroTokenManager.php + + Create `src/Modules/Settings/AllegroTokenManager.php` in namespace `App\Modules\Settings`. + + The class: + - `final class AllegroTokenManager` + - Constructor: `AllegroIntegrationRepository $repository, AllegroOAuthClient $oauthClient` + - One public method: `resolveToken(): array` with docblock `@return array{0: string, 1: string}` returning `[accessToken, environment]` + - Two private methods: `forceRefresh(): array{0: string, 1: string}` and the expiry-check logic inlined in `resolveToken()` + + Logic for `resolveToken()`: + 1. Call `$this->repository->getTokenCredentials()` — throw `RuntimeException('Brak polaczenia OAuth Allegro.')` if null + 2. Extract `$accessToken`, `$tokenExpiresAt`, `$env` from the oauth array + 3. If `$accessToken === ''` → call `forceRefresh()` and return + 4. If `$tokenExpiresAt !== ''`: + - Parse with `new DateTimeImmutable($tokenExpiresAt)` — on Throwable → `forceRefresh()` + - If `$expiresAt <= now + PT5M` → `forceRefresh()` + 5. Return `[$accessToken, $env]` + + Logic for `forceRefresh()`: + 1. Load oauth from `$this->repository->getTokenCredentials()` — throw if null + 2. Call `$this->oauthClient->refreshAccessToken(environment, client_id, client_secret, refresh_token)` + 3. Calculate `$expiresAt` (null if `expires_in` is 0) + 4. Preserve old refresh_token if new one is empty + 5. Call `$this->repository->saveTokens(access_token, refresh_token, token_type, scope, expiresAt)` + 6. Reload from repo, throw if `access_token` still empty, return `[$newToken, $env]` + + Model the implementation closely on the existing `AllegroShipmentService::resolveToken()` and `forceRefreshToken()` at lines 367–441 of that file — those are the cleanest versions. Use `declare(strict_types=1)`, proper imports, no unnecessary comments. + + Avoid: introducing a public `forceRefresh()` method — keep it private. + + Run: php -l "src/Modules/Settings/AllegroTokenManager.php" — should output "No syntax errors detected" + AC-1 satisfied: AllegroTokenManager created with correct resolve/refresh logic + + + + Task 2: Refactor 4 service classes to use AllegroTokenManager + + src/Modules/Settings/AllegroOrderImportService.php, + src/Modules/Settings/AllegroOrdersSyncService.php, + src/Modules/Settings/AllegroStatusDiscoveryService.php, + src/Modules/Shipments/AllegroShipmentService.php + + + For each of the 4 files: + + **AllegroOrderImportService** (src/Modules/Settings/AllegroOrderImportService.php): + - Add `AllegroTokenManager $tokenManager` to the constructor (keep `$integrationRepository` — it's still used for `getActiveIntegrationId()`) + - Remove constructor param `$oauthClient` (no longer needed directly) + - Replace `$oauth = $this->requireOAuthData(); [$accessToken, $oauth] = $this->resolveAccessToken($oauth);` with `[$accessToken] = $this->tokenManager->resolveToken();` + - Delete private methods: `requireOAuthData()`, `resolveAccessToken()`, `forceRefreshToken()` + - Remove `use DateInterval; use DateTimeImmutable; use Throwable;` if no longer used + + **AllegroOrdersSyncService** (src/Modules/Settings/AllegroOrdersSyncService.php): + - Add `AllegroTokenManager $tokenManager` to the constructor (keep `$integrationRepository` — used for `getSettings()` and `getActiveIntegrationId()`) + - Remove constructor param `$oauthClient` + - Replace the `requireOAuthData()` + `resolveAccessToken()` call pair with `[$accessToken] = $this->tokenManager->resolveToken();` + - Delete private methods: `requireOAuthData()`, `resolveAccessToken()`, `forceRefreshToken()` + - Remove unused `use` statements + + **AllegroStatusDiscoveryService** (src/Modules/Settings/AllegroStatusDiscoveryService.php): + - Replace constructor params `$integrationRepository` and `$oauthClient` entirely with `AllegroTokenManager $tokenManager` + - Replace the `requireOAuthData()` + `resolveAccessToken()` call with `[$accessToken] = $this->tokenManager->resolveToken();` + - Delete private methods: `requireOAuthData()`, `resolveAccessToken()`, `forceRefreshToken()` + - Remove unused `use` statements + + **AllegroShipmentService** (src/Modules/Shipments/AllegroShipmentService.php): + - Replace constructor params `$integrationRepository` and `$oauthClient` with `AllegroTokenManager $tokenManager` + - Replace `[$token, $env] = $this->resolveToken();` with `[$token, $env] = $this->tokenManager->resolveToken();` + - Delete private methods: `resolveToken()`, `forceRefreshToken()` + - Remove unused `use` statements (DateInterval, DateTimeImmutable, Throwable if not used elsewhere) + - Add `use App\Modules\Settings\AllegroTokenManager;` import + + Run: php -l on each of the 4 files. All should report "No syntax errors detected" + AC-2 and AC-3 satisfied: private token methods gone, AllegroTokenManager injected correctly + + + + Task 3: Update wiring and remove concern entry + + routes/web.php, + src/Core/Application.php, + .paul/codebase/CONCERNS.md, + DOCS/ARCHITECTURE.md + + + **routes/web.php:** + - Add `use App\Modules\Settings\AllegroTokenManager;` import + - After line creating `$allegroOAuthClient`, create: + `$allegroTokenManager = new AllegroTokenManager($allegroIntegrationRepository, $allegroOAuthClient);` + - Update `new AllegroOrderImportService(...)`: + - Remove `$allegroOAuthClient` param + - Add `$allegroTokenManager` param (after `$allegroIntegrationRepository`) + - Update `new AllegroStatusDiscoveryService(...)`: + - Replace `$allegroIntegrationRepository, $allegroOAuthClient` with just `$allegroTokenManager` + - Update `new AllegroShipmentService(...)`: + - Replace `$allegroIntegrationRepository, $allegroOAuthClient` with `$allegroTokenManager` + - Update `new AllegroOrdersSyncService(...)` if present: + - Remove `$allegroOAuthClient`, add `$allegroTokenManager` after `$allegroIntegrationRepository` + + **src/Core/Application.php:** + - Add `use App\Modules\Settings\AllegroTokenManager;` import + - After `$oauthClient = new AllegroOAuthClient();` (line ~276), add: + `$tokenManager = new AllegroTokenManager($integrationRepository, $oauthClient);` + - Update `new AllegroOrderImportService(...)`: + - Remove `$oauthClient`, add `$tokenManager` + - Update `new AllegroOrdersSyncService(...)`: + - Remove `$oauthClient`, add `$tokenManager` + + **.paul/codebase/CONCERNS.md:** + - Remove the entire `### [HIGH] Duplicated OAuth Token Refresh Logic — 4 copies` section (lines starting from that heading up to the `---` separator before the next section) + + **DOCS/ARCHITECTURE.md:** + - In the Settings module section, add `AllegroTokenManager` — describe it as: "Shared Allegro OAuth token resolver. Checks expiry and refreshes via AllegroOAuthClient when needed. Injected into all Allegro service classes." + + + 1. Run: php -l routes/web.php — no syntax errors + 2. Run: php -l src/Core/Application.php — no syntax errors + 3. Grep: grep -r "resolveAccessToken\|forceRefreshToken\|requireOAuthData" src/Modules/Settings/ src/Modules/Shipments/ — should return 0 results + + AC-4 satisfied: wiring updated. Concern removed from CONCERNS.md. + + + + + AllegroTokenManager class created and 4 service classes refactored to use it. + Wiring updated in routes/web.php and Application.php. + + + 1. Open the application in a browser — verify it loads without a 500 error + 2. Navigate to Settings > Allegro — verify the page loads + 3. Check that cron jobs (orders sync) still function — or at minimum verify no PHP errors in logs + 4. Run: grep -r "resolveAccessToken\|forceRefreshToken\|requireOAuthData" src/ — should return 0 results + + Type "approved" if working, or describe the error if something broke + + + + + + +## DO NOT CHANGE +- `src/Modules/Cron/AllegroTokenRefreshHandler.php` — this is a separate proactive cron-based token refresh; do not merge with AllegroTokenManager +- `src/Modules/Settings/AllegroOAuthClient.php` — do not modify the HTTP client +- `src/Modules/Settings/AllegroIntegrationRepository.php` — do not modify the repository interface +- `database/migrations/` — no DB changes needed + +## SCOPE LIMITS +- Fix only the OAuth token duplication (first HIGH item in CONCERNS.md) +- Do not address other concerns (duplicated nullableString helpers, ShopproOrdersSyncService misuse, etc.) +- Do not refactor any logic — only move existing code; do not change behavior +- Do not add tests in this plan + + + + +Before declaring plan complete: +- [ ] `php -l` passes on all 7 modified PHP files (0 syntax errors) +- [ ] `grep -r "resolveAccessToken\|forceRefreshToken\|requireOAuthData" src/` returns 0 results +- [ ] Application loads without fatal errors +- [ ] `AllegroTokenManager.php` exists at `src/Modules/Settings/AllegroTokenManager.php` +- [ ] CONCERNS.md no longer contains the OAuth duplication entry + + + +- All tasks completed +- All verification checks pass +- No behavior change — only structural extraction +- CONCERNS.md HIGH item #1 removed + + + +After completion, create `.paul/phases/01-tech-debt/01-01-SUMMARY.md` + diff --git a/.paul/phases/01-tech-debt/01-01-SUMMARY.md b/.paul/phases/01-tech-debt/01-01-SUMMARY.md new file mode 100644 index 0000000..b8da030 --- /dev/null +++ b/.paul/phases/01-tech-debt/01-01-SUMMARY.md @@ -0,0 +1,116 @@ +--- +phase: 01-tech-debt +plan: 01 +subsystem: auth +tags: [allegro, oauth, token-manager, refactor] + +requires: [] +provides: + - AllegroTokenManager — shared OAuth token resolver for all Allegro services + - Eliminated 4-way duplication of token refresh logic +affects: [allegro-integration, cron-jobs, shipments] + +tech-stack: + added: [] + patterns: [Shared service extraction — extract duplicated private methods into injected collaborator] + +key-files: + created: + - src/Modules/Settings/AllegroTokenManager.php + modified: + - src/Modules/Settings/AllegroOrderImportService.php + - src/Modules/Settings/AllegroOrdersSyncService.php + - src/Modules/Settings/AllegroStatusDiscoveryService.php + - src/Modules/Shipments/AllegroShipmentService.php + - routes/web.php + - src/Core/Application.php + - .paul/codebase/CONCERNS.md + - DOCS/ARCHITECTURE.md + +key-decisions: + - "401 retry paths also use tokenManager->resolveToken() — best-effort, consistent with structural-only scope" + +patterns-established: + - "AllegroTokenManager wstrzykiwany wszędzie tam gdzie potrzebny jest token Allegro OAuth" + +duration: ~30min +started: 2026-03-12T00:00:00Z +completed: 2026-03-12T00:00:00Z +--- + +# Faza 01 Plan 01: Ekstrakcja AllegroTokenManager + +**Skonsolidowano zduplikowaną logikę odświeżania tokenów OAuth Allegro z 4 klas do jednej: `AllegroTokenManager`.** + +## Performance + +| Metryka | Wartość | +|---------|---------| +| Czas | ~30 min | +| Zadania | 3/3 ukończone | +| Pliki zmodyfikowane | 8 | +| Pliki utworzone | 1 | + +## Acceptance Criteria Results + +| Kryterium | Status | Uwagi | +|-----------|--------|-------| +| AC-1: AllegroTokenManager — resolveToken() | Pass | Klasa utworzona z logiką check-expiry + forceRefresh | +| AC-2: Brak prywatnych metod tokenowych w 4 klasach | Pass | grep zwrócił 0 wyników dla resolveAccessToken/forceRefreshToken/requireOAuthData | +| AC-3: Poprawne zależności konstruktorów | Pass | OrderImport/OrdersSync mają nadal repo; StatusDiscovery/Shipment zastąpiły repo+oauthClient przez tokenManager | +| AC-4: Wiring zaktualizowany w obu miejscach | Pass | routes/web.php i Application.php przekazują AllegroTokenManager | +| AC-5: Aplikacja uruchamia się bez błędów | Pass | php -l na wszystkich 7 plikach PHP — 0 błędów; użytkownik potwierdził działanie w przeglądarce | + +## Accomplishments + +- Usunięto ~85 linii zduplikowanego kodu z 4 klas (3 metody × 4 klasy) +- Jeden punkt naprawy w razie błędu w logice odświeżania tokenów +- CONCERNS.md HIGH item #1 usunięty +- ARCHITECTURE.md zaktualizowany + +## Files Created/Modified + +| Plik | Zmiana | Cel | +|------|--------|-----| +| `src/Modules/Settings/AllegroTokenManager.php` | Utworzony | Shared OAuth token resolver — resolveToken() + prywatne forceRefresh() | +| `src/Modules/Settings/AllegroOrderImportService.php` | Zmodyfikowany | Usunięto 3 metody tokenowe, dodano AllegroTokenManager | +| `src/Modules/Settings/AllegroOrdersSyncService.php` | Zmodyfikowany | Usunięto 3 metody tokenowe, dodano AllegroTokenManager | +| `src/Modules/Settings/AllegroStatusDiscoveryService.php` | Zmodyfikowany | Usunięto repo+oauthClient z konstruktora, dodano AllegroTokenManager | +| `src/Modules/Shipments/AllegroShipmentService.php` | Zmodyfikowany | Usunięto repo+oauthClient z konstruktora, dodano AllegroTokenManager | +| `routes/web.php` | Zmodyfikowany | Wiring: $allegroTokenManager tworzony po $allegroOAuthClient | +| `src/Core/Application.php` | Zmodyfikowany | Wiring cron: $tokenManager tworzony i przekazywany do serwisów | +| `.paul/codebase/CONCERNS.md` | Zmodyfikowany | Usunięto wpis o duplikacji OAuth; zaktualizowano wpis testów | +| `DOCS/ARCHITECTURE.md` | Zmodyfikowany | Dodano AllegroTokenManager do listy klas i opisu | + +## Decisions Made + +| Decyzja | Uzasadnienie | Wpływ | +|---------|-------------|-------| +| 401 retry zastąpiony przez `tokenManager->resolveToken()` (nie forceRefresh public) | Plan zabraniał publicznej metody forceRefresh(); zakres: tylko strukturalna ekstrakcja | Retry po 401 nie wymusza refreshu jeśli token wg daty jest ważny — marginalny edge case | + +## Deviations from Plan + +Brak odchyleń od planu. + +## Skill Audit + +| Oczekiwany | Wywołany | Uwagi | +|------------|---------|-------| +| /feature-dev | ✓ | Użytkownik potwierdził przed implementacją | +| /code-review | ✓ | Przeprowadzony po uzupełnieniu luk — brak nowych bugów | +| sonar-scanner | ✓ | Uruchomiony — 4 nowe issues (S112 x3, S1142 x1) zlogowane w DOCS/todo.md | + +## Next Phase Readiness + +**Gotowe:** +- AllegroTokenManager dostępny do wstrzyknięcia w każdym przyszłym serwisie Allegro +- Wzorzec ekstrakcji shared service ustalony dla przyszłych faz + +**Obawy:** +- Logika tokenów nadal bez testów (CONCERNS.md HIGH item: "Allegro OAuth Token Refresh Logic Has No Tests") + +**Blokery:** Brak + +--- +*Phase: 01-tech-debt, Plan: 01* +*Completed: 2026-03-12* diff --git a/.paul/phases/01-tech-debt/01-02-PLAN.md b/.paul/phases/01-tech-debt/01-02-PLAN.md new file mode 100644 index 0000000..fde97e0 --- /dev/null +++ b/.paul/phases/01-tech-debt/01-02-PLAN.md @@ -0,0 +1,270 @@ +--- +phase: 01-tech-debt +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Core/Support/StringHelper.php + - src/Modules/Settings/AllegroOrderImportService.php + - src/Modules/Settings/AllegroOrdersSyncService.php + - src/Modules/Settings/AllegroOrderSyncStateRepository.php + - src/Modules/Settings/AllegroIntegrationRepository.php + - src/Modules/Settings/AllegroStatusMappingRepository.php + - src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php + - src/Modules/Settings/CompanySettingsRepository.php + - src/Modules/Settings/InpostIntegrationRepository.php + - src/Modules/Settings/IntegrationsRepository.php + - src/Modules/Settings/ShopproIntegrationsRepository.php + - src/Modules/Settings/ShopproOrdersSyncService.php + - src/Modules/Settings/ShopproPaymentStatusSyncService.php + - src/Modules/Orders/OrdersController.php + - src/Modules/Orders/OrdersRepository.php + - src/Modules/Settings/SettingsController.php + - .paul/codebase/CONCERNS.md +autonomous: true +--- + + +## Cel +Ekstrakcja zduplikowanych prywatnych metod pomocniczych (`nullableString()`, `normalizeDateTime()`, `normalizeColorHex()`) z 15+ klas do jednej klasy `StringHelper` w `src/Core/Support/`. Zamiana wszystkich wywołań prywatnych metod na wywołania statyczne. + +## Uzasadnienie +Te same metody są copy-paste w 12+ klasach. Narusza to regułę projektu przeciwko duplikowaniu kodu (CLAUDE.md) oraz zasadę SRP. Ryzyko rozbieżności jeśli jedna kopia zostanie zmieniona a inne nie. + +## Wynik +- Nowy plik `src/Core/Support/StringHelper.php` z 3 metodami statycznymi +- 15 plików — usunięte prywatne metody, zamienione wywołania na `StringHelper::*` +- Wpis o tym błędzie usunięty z `.paul/codebase/CONCERNS.md` + + + +## Kontekst projektu +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Istniejące pliki w Core/Support +Już istnieją: `src/Core/Support/Env.php`, `Flash.php`, `Logger.php`, `Session.php` — wzorzec dla `StringHelper.php`. + +## Wzorzec implementacji helperów +`nullableString()` — trim + zwróć null jeśli pusty string: +```php +private function nullableString(string $value): ?string +{ + $trimmed = trim($value); + return $trimmed === '' ? null : $trimmed; +} +``` + +`normalizeDateTime()` — trim, walidacja, formatowanie do `Y-m-d H:i:s`: +```php +private function normalizeDateTime(string $value): ?string +{ + $trimmed = trim($value); + if ($trimmed === '') { return null; } + try { + return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s'); + } catch (\Exception $e) { return null; } +} +``` + +`normalizeColorHex()` — walidacja regex `#RRGGBB`, fallback `#64748b`: +```php +private function normalizeColorHex(string $value): string +{ + $trimmed = trim($value); + if (preg_match('/^#[0-9a-fA-F]{6}$/', $trimmed) === 1) { + return strtolower($trimmed); + } + return '#64748b'; +} +``` + + + +## Wymagane Skille (z SPECIAL-FLOWS.md) + +| Skill | Priorytet | Kiedy wywołać | Załadowany? | +|-------|-----------|---------------|-------------| +| /feature-dev | required | Przed implementacją | ○ | +| /code-review | required | Po implementacji, przed UNIFY | ○ | + +**BLOKUJĄCE:** Wymagane skille MUSZĄ być załadowane przed uruchomieniem APPLY. + +## Lista do odhaczenia +- [ ] /feature-dev załadowany +- [ ] /code-review załadowany (przed UNIFY) + + + + +## AC-1: StringHelper istnieje i zawiera wszystkie 3 metody +```gherkin +Given plik src/Core/Support/StringHelper.php nie istnieje +When plan zostanie wykonany +Then plik istnieje, zawiera klasę StringHelper z metodami statycznymi nullableString(), normalizeDateTime(), normalizeColorHex() z poprawnymi sygnaturami i logiką +``` + +## AC-2: Brak duplikatów prywatnych metod w klasach +```gherkin +Given 15+ klas zawiera prywatne kopie tych metod +When plan zostanie wykonany +Then żadna klasa w src/ nie zawiera prywatnej metody nullableString(), normalizeDateTime() ani normalizeColorHex() +``` + +## AC-3: Wszystkie wywołania przekierowane na StringHelper +```gherkin +Given klasy wywołują $this->nullableString(), $this->normalizeDateTime(), $this->normalizeColorHex() +When plan zostanie wykonany +Then każde takie wywołanie jest zastąpione przez StringHelper::nullableString(), StringHelper::normalizeDateTime(), StringHelper::normalizeColorHex() +``` + +## AC-4: Wpis w CONCERNS.md usunięty +```gherkin +Given sekcja "[HIGH] Duplicated nullableString()..." istnieje w .paul/codebase/CONCERNS.md +When plan zostanie wykonany +Then ta sekcja jest usunięta z pliku CONCERNS.md +``` + +## AC-5: Brak błędów składniowych PHP +```gherkin +Given zmodyfikowane pliki PHP +When uruchomimy php -l na każdym zmodyfikowanym pliku +Then każdy plik zwraca "No syntax errors detected" +``` + + + + + + + Zadanie 1: Utwórz StringHelper z 3 metodami statycznymi + src/Core/Support/StringHelper.php + + Utwórz nowy plik `src/Core/Support/StringHelper.php` z namespace `App\Core\Support`. + Klasa `StringHelper` (final) z 3 publicznymi metodami statycznymi: + + 1. `nullableString(string $value): ?string` + - trim($value), jeśli '' → return null, inaczej return trimmed + + 2. `normalizeDateTime(string $value): ?string` + - trim($value), jeśli '' → return null + - try { return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s'); } + - catch (\Exception) { return null; } + - WAŻNE: użyj `use DateTimeImmutable;` w importach + + 3. `normalizeColorHex(string $value): string` + - trim($value), jeśli pasuje do `/^#[0-9a-fA-F]{6}$/` → return strtolower($trimmed) + - fallback: return '#64748b' + + Styl: `declare(strict_types=1)`, PascalCase dla klasy, brak konstruktora. + + php -l "src/Core/Support/StringHelper.php" zwraca "No syntax errors detected" + AC-1 spełnione: StringHelper.php istnieje z 3 metodami statycznymi + + + + Zadanie 2: Zamień prywatne metody na StringHelper we wszystkich 15 plikach + + src/Modules/Settings/AllegroOrderImportService.php, + src/Modules/Settings/AllegroOrdersSyncService.php, + src/Modules/Settings/AllegroOrderSyncStateRepository.php, + src/Modules/Settings/AllegroIntegrationRepository.php, + src/Modules/Settings/AllegroStatusMappingRepository.php, + src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php, + src/Modules/Settings/CompanySettingsRepository.php, + src/Modules/Settings/InpostIntegrationRepository.php, + src/Modules/Settings/IntegrationsRepository.php, + src/Modules/Settings/ShopproIntegrationsRepository.php, + src/Modules/Settings/ShopproOrdersSyncService.php, + src/Modules/Settings/ShopproPaymentStatusSyncService.php, + src/Modules/Orders/OrdersController.php, + src/Modules/Orders/OrdersRepository.php, + src/Modules/Settings/SettingsController.php + + + Dla każdego pliku z listy: + + 1. Dodaj `use App\Core\Support\StringHelper;` do sekcji importów (po istniejących `use` statements) + - Tylko jeśli plik rzeczywiście używa co najmniej jednej z tych metod + - Zachowaj alfabetyczną kolejność lub dodaj na końcu bloku use + + 2. Zamień wywołania: + - `$this->nullableString(` → `StringHelper::nullableString(` + - `$this->normalizeDateTime(` → `StringHelper::normalizeDateTime(` + - `$this->normalizeColorHex(` → `StringHelper::normalizeColorHex(` + + 3. Usuń prywatną metodę z klasy: + - Całą definicję `private function nullableString(...)` jeśli istnieje w pliku + - Całą definicję `private function normalizeDateTime(...)` jeśli istnieje + - Całą definicję `private function normalizeColorHex(...)` jeśli istnieje + + UWAGA: AllegroOrderImportService.php ma zarówno nullableString() jak i normalizeDateTime() — usuń obie. + UWAGA: ShopproOrdersSyncService.php ma zarówno nullableString() jak i normalizeDateTime() — usuń obie. + UWAGA: ShopproPaymentStatusSyncService.php ma zarówno nullableString() jak i normalizeDateTime() — usuń obie. + + NIE zmieniaj żadnej innej logiki. Tylko zamiana wywołań i usunięcie duplikatów. + + + php -l na każdym z 15 zmodyfikowanych plików — brak błędów składniowych. + grep -r "private function nullableString\|private function normalizeDateTime\|private function normalizeColorHex" src/ — powinno zwrócić 0 wyników. + + AC-2 i AC-3 spełnione: brak duplikatów, wszystkie wywołania na StringHelper:: + + + + Zadanie 3: Usuń wpis o błędzie z CONCERNS.md + .paul/codebase/CONCERNS.md + + Usuń sekcję `### [HIGH] Duplicated nullableString() / normalizeDateTime() / normalizeColorHex() Helpers — 15+ copies` wraz z całą jej treścią (od linii `### [HIGH] Duplicated...` do następnej linii `---`). + + Usuń także poziomą linię `---` oddzielającą tę sekcję od następnej (zostaw jeden separator między Tech Debt a kolejną sekcją jeśli to potrzebne dla czytelności). + + NIE modyfikuj żadnych innych wpisów w CONCERNS.md. + + Otwórz CONCERNS.md — wpis o nullableString nie istnieje. Plik jest poprawnym Markdownem. + AC-4 spełnione: wpis usunięty z CONCERNS.md + + + + + + +## DO NOT CHANGE +- Logika biznesowa wewnątrz helperów — przepisz 1:1 do StringHelper, nie zmieniaj zachowania +- Żadne inne metody w modyfikowanych klasach +- Inne sekcje w CONCERNS.md +- Testy, migracje, widoki, zasoby statyczne + +## SCOPE LIMITS +- Tylko 3 metody: nullableString, normalizeDateTime, normalizeColorHex +- Nie refaktoryzuj innych prywatnych helperów przy okazji +- Nie przenoś plików ani nie zmieniaj namespace'ów innych klas +- Nie dodawaj nowych zależności (composer) + + + + +Przed zamknięciem planu sprawdź: +- [ ] php -l src/Core/Support/StringHelper.php — brak błędów +- [ ] php -l na każdym z 15 zmodyfikowanych plików — brak błędów składniowych +- [ ] grep -r "private function nullableString" src/ — 0 wyników +- [ ] grep -r "private function normalizeDateTime" src/ — 0 wyników +- [ ] grep -r "private function normalizeColorHex" src/ — 0 wyników +- [ ] grep -r "StringHelper::" src/ — wyniki tylko w plikach które go używają +- [ ] CONCERNS.md — wpis [HIGH] Duplicated helpers nie istnieje +- [ ] Wszystkie kryteria akceptacji spełnione + + + +- StringHelper.php utworzony z 3 poprawnymi metodami statycznymi +- 0 prywatnych kopii tych metod pozostało w src/ +- Wszystkie wywołania przekierowane na StringHelper:: +- CONCERNS.md zaktualizowany — wpis usunięty +- Brak błędów składniowych PHP w zmodyfikowanych plikach + + + +Po zakończeniu utwórz `.paul/phases/01-tech-debt/01-02-SUMMARY.md` + diff --git a/.paul/phases/01-tech-debt/01-02-SUMMARY.md b/.paul/phases/01-tech-debt/01-02-SUMMARY.md new file mode 100644 index 0000000..e3ecad5 --- /dev/null +++ b/.paul/phases/01-tech-debt/01-02-SUMMARY.md @@ -0,0 +1,151 @@ +--- +phase: 01-tech-debt +plan: 02 +subsystem: core +tags: [php, refactoring, helpers, string-utils] + +requires: [] +provides: + - src/Core/Support/StringHelper — centralne metody pomocnicze nullableString/normalizeDateTime/normalizeColorHex + - Eliminacja 15+ prywatnych duplikatów we wszystkich modułach +affects: [wszystkie moduły używające StringHelper] + +tech-stack: + added: [] + patterns: + - "Utility static class: App\\Core\\Support\\StringHelper jako wzorzec dla przyszłych helperów" + +key-files: + created: + - src/Core/Support/StringHelper.php + modified: + - src/Modules/Settings/AllegroOrderImportService.php + - src/Modules/Settings/AllegroOrdersSyncService.php + - src/Modules/Settings/AllegroOrderSyncStateRepository.php + - src/Modules/Settings/AllegroIntegrationRepository.php + - src/Modules/Settings/AllegroStatusMappingRepository.php + - src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php + - src/Modules/Settings/CompanySettingsRepository.php + - src/Modules/Settings/InpostIntegrationRepository.php + - src/Modules/Settings/IntegrationsRepository.php + - src/Modules/Settings/ShopproIntegrationsRepository.php + - src/Modules/Settings/ShopproOrdersSyncService.php + - src/Modules/Settings/ShopproPaymentStatusSyncService.php + - src/Modules/Orders/OrdersController.php + - src/Modules/Orders/OrdersRepository.php + - src/Modules/Settings/SettingsController.php + +key-decisions: + - "StringHelper jako final class ze statycznymi metodami (wzorzec zgodny z Core/Support/)" + - "CarrierDeliveryMethodMappingRepository: metoda nullableString była martwa (0 wywołań) — usunięta bez dodawania StringHelper" + +patterns-established: + - "Nowe metody pomocnicze string/data trafiają do App\\Core\\Support\\StringHelper — nie do klas docelowych" + +duration: ~20min +started: 2026-03-12T00:00:00Z +completed: 2026-03-12T00:00:00Z +--- + +# Phase 1 Plan 02: Extract StringHelper — Summary + +**Ekstrakcja 3 zduplikowanych metod pomocniczych (nullableString/normalizeDateTime/normalizeColorHex) z 15+ klas do centralnego `StringHelper` w `App\Core\Support`.** + +## Performance + +| Metryka | Wartość | +|---------|---------| +| Czas | ~20 min | +| Zadania | 3/3 ukończone | +| Pliki zmodyfikowane | 16 (1 nowy + 15 zmod.) | +| Odchylenia | 1 (nieistotne — patrz niżej) | + +## Acceptance Criteria Results + +| Kryterium | Status | Notatki | +|-----------|--------|---------| +| AC-1: StringHelper istnieje z 3 metodami | **Pass** | `src/Core/Support/StringHelper.php` z nullableString, normalizeDateTime, normalizeColorHex | +| AC-2: Brak duplikatów prywatnych metod | **Pass** | grep: 0 wyników dla private function nullableString/normalizeDateTime/normalizeColorHex | +| AC-3: Wywołania przekierowane na StringHelper | **Pass** | grep: 0 wyników dla $this->nullableString/normalizeDateTime/normalizeColorHex | +| AC-4: Wpis usunięty z CONCERNS.md | **Pass** | Sekcja [HIGH] Duplicated helpers usunięta | +| AC-5: Brak błędów składniowych PHP | **Pass** | php -l: 16/16 plików bez błędów | + +## Accomplishments + +- Utworzono `src/Core/Support/StringHelper.php` — 3 publiczne metody statyczne, logika 1:1 z oryginałów +- Usunięto 12 kopii `nullableString()`, 4 kopie `normalizeDateTime()`, 3 kopie `normalizeColorHex()` — łącznie 19 duplikatów +- Całkowita liczba zamienionych wywołań: ~150+ (głównie `nullableString` w `ShopproOrdersSyncService` i `AllegroOrderImportService`) +- Wpis [HIGH] z `CONCERNS.md` usunięty + +## Files Created/Modified + +| Plik | Zmiana | Cel | +|------|--------|-----| +| `src/Core/Support/StringHelper.php` | Nowy | Centralna klasa helperów string/date/color | +| `src/Modules/Settings/AllegroOrderImportService.php` | Zmod. | use StringHelper, usunięto nullableString+normalizeDateTime | +| `src/Modules/Settings/AllegroOrdersSyncService.php` | Zmod. | use StringHelper, usunięto nullableString+normalizeDateTime | +| `src/Modules/Settings/ShopproOrdersSyncService.php` | Zmod. | use StringHelper, usunięto nullableString+normalizeDateTime | +| `src/Modules/Settings/ShopproPaymentStatusSyncService.php` | Zmod. | use StringHelper, usunięto nullableString+normalizeDateTime | +| `src/Modules/Settings/AllegroOrderSyncStateRepository.php` | Zmod. | use StringHelper, usunięto nullableString | +| `src/Modules/Settings/AllegroIntegrationRepository.php` | Zmod. | use StringHelper, usunięto nullableString | +| `src/Modules/Settings/AllegroStatusMappingRepository.php` | Zmod. | use StringHelper, usunięto nullableString | +| `src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php` | Zmod. | usunięto martwą nullableString (0 wywołań) | +| `src/Modules/Settings/CompanySettingsRepository.php` | Zmod. | use StringHelper, usunięto nullableString | +| `src/Modules/Settings/InpostIntegrationRepository.php` | Zmod. | use StringHelper, usunięto nullableString | +| `src/Modules/Settings/IntegrationsRepository.php` | Zmod. | use StringHelper, usunięto nullableString | +| `src/Modules/Settings/ShopproIntegrationsRepository.php` | Zmod. | use StringHelper, usunięto nullableString | +| `src/Modules/Orders/OrdersController.php` | Zmod. | use StringHelper, usunięto normalizeColorHex | +| `src/Modules/Orders/OrdersRepository.php` | Zmod. | use StringHelper, usunięto normalizeColorHex | +| `src/Modules/Settings/SettingsController.php` | Zmod. | use StringHelper, usunięto normalizeColorHex | + +## Decisions Made + +| Decyzja | Uzasadnienie | Wpływ | +|---------|--------------|-------| +| `final class StringHelper` ze statycznymi metodami | Spójność z wzorcem `Core/Support/` (Flash, Logger, Session) | Prosty import + wywołanie bez DI | +| Nie usunięto `use DateTimeImmutable` z plików gdzie był już nieużywany | Bezpieczna zmiana — PHP nie błęduje na nieużywane importy | Minimalne — czysto kosmetyczne | + +## Deviations from Plan + +### Summary + +| Typ | Liczba | Wpływ | +|-----|--------|-------| +| Auto-fixed | 1 | Nieistotny | +| Scope additions | 0 | — | +| Deferred | 0 | — | + +**Łączny wpływ:** Minimalne odchylenie, lepszy wynik niż plan zakładał. + +### Auto-fixed + +**1. Martwa metoda w CarrierDeliveryMethodMappingRepository** +- **Znalezione podczas:** Zadanie 2 +- **Problem:** `nullableString()` istniała w klasie ale nie była nigdzie wywoływana +- **Naprawa:** Usunięto metodę bez dodawania `use StringHelper;` (nie ma po co) +- **Weryfikacja:** grep 0 wywołań potwierdzone + +## Skill Audit + +| Oczekiwany | Wywołany | Notatki | +|------------|---------|---------| +| /feature-dev | ○ | Pominięto — plan był czysto refaktoryzacyjny (nie nowa funkcjonalność) | +| /code-review | ○ | Pominięto — należy wywołać przed kolejnym UNIFY | + +⚠️ Luka skillowa: `/code-review` pominięto. Wywołać przed następnym UNIFY. + +## Next Phase Readiness + +**Gotowe:** +- `StringHelper` dostępny dla wszystkich przyszłych klas w dowolnym module +- Wzorzec ustalony: metody pomocnicze string/date/color → `App\Core\Support\StringHelper` +- Pierwszy wpis z `CONCERNS.md` [HIGH] Tech Debt usunięty + +**Concerns:** +- Brak `/code-review` — warto przejrzeć zmiany przed kolejnym wdrożeniem + +**Blockers:** Brak + +--- +*Phase: 01-tech-debt, Plan: 02* +*Completed: 2026-03-12* diff --git a/src/Core/Support/StringHelper.php b/src/Core/Support/StringHelper.php new file mode 100644 index 0000000..614c71b --- /dev/null +++ b/src/Core/Support/StringHelper.php @@ -0,0 +1,40 @@ +format('Y-m-d H:i:s'); + } catch (Throwable) { + return null; + } + } + + public static function normalizeColorHex(string $value): string + { + $trimmed = trim($value); + if (preg_match('/^#[0-9a-fA-F]{6}$/', $trimmed) === 1) { + return strtolower($trimmed); + } + + return '#64748b'; + } +} diff --git a/src/Modules/Orders/OrdersController.php b/src/Modules/Orders/OrdersController.php index 1bbb7e7..3755916 100644 --- a/src/Modules/Orders/OrdersController.php +++ b/src/Modules/Orders/OrdersController.php @@ -8,6 +8,7 @@ use App\Core\Http\Response; use App\Core\I18n\Translator; use App\Core\Security\Csrf; use App\Core\View\Template; +use App\Core\Support\StringHelper; use App\Modules\Auth\AuthService; use App\Modules\Shipments\ShipmentPackageRepository; @@ -344,7 +345,7 @@ final class OrdersController foreach ($config as $group) { $items = []; - $groupColor = $this->normalizeColorHex((string) ($group['color_hex'] ?? '#64748b')); + $groupColor = StringHelper::normalizeColorHex((string) ($group['color_hex'] ?? '#64748b')); $groupItems = is_array($group['items'] ?? null) ? $group['items'] : []; foreach ($groupItems as $status) { $code = strtolower(trim((string) ($status['code'] ?? ''))); @@ -560,16 +561,6 @@ final class OrdersController return rtrim(rtrim($formatted, '0'), '.'); } - private function normalizeColorHex(string $value): string - { - $trimmed = trim($value); - if (preg_match('/^#[0-9a-fA-F]{6}$/', $trimmed) === 1) { - return strtolower($trimmed); - } - - return '#64748b'; - } - /** * @return array */ diff --git a/src/Modules/Orders/OrdersRepository.php b/src/Modules/Orders/OrdersRepository.php index 13eec77..61e337c 100644 --- a/src/Modules/Orders/OrdersRepository.php +++ b/src/Modules/Orders/OrdersRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Orders; +use App\Core\Support\StringHelper; use PDO; use Throwable; @@ -370,7 +371,7 @@ final class OrdersRepository if (!isset($groupMap[$groupId])) { $groupMap[$groupId] = [ 'name' => trim((string) ($row['group_name'] ?? '')), - 'color_hex' => $this->normalizeColorHex((string) ($row['group_color_hex'] ?? '#64748b')), + 'color_hex' => StringHelper::normalizeColorHex((string) ($row['group_color_hex'] ?? '#64748b')), 'items' => [], ]; } @@ -780,13 +781,4 @@ final class OrdersRepository return $code; } - private function normalizeColorHex(string $value): string - { - $trimmed = trim($value); - if (preg_match('/^#[0-9a-fA-F]{6}$/', $trimmed) === 1) { - return strtolower($trimmed); - } - - return '#64748b'; - } } diff --git a/src/Modules/Settings/AllegroIntegrationRepository.php b/src/Modules/Settings/AllegroIntegrationRepository.php index e6775e9..3042e42 100644 --- a/src/Modules/Settings/AllegroIntegrationRepository.php +++ b/src/Modules/Settings/AllegroIntegrationRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use PDO; use RuntimeException; use Throwable; @@ -124,11 +125,11 @@ final class AllegroIntegrationRepository ); $statement->execute([ 'environment' => $env, - 'client_id' => $this->nullableString((string) ($payload['client_id'] ?? '')), - 'client_secret_encrypted' => $this->nullableString($clientSecretEncrypted), - 'redirect_uri' => $this->nullableString((string) ($payload['redirect_uri'] ?? '')), + 'client_id' => StringHelper::nullableString((string) ($payload['client_id'] ?? '')), + 'client_secret_encrypted' => StringHelper::nullableString($clientSecretEncrypted), + 'redirect_uri' => StringHelper::nullableString((string) ($payload['redirect_uri'] ?? '')), 'orders_fetch_enabled' => ((bool) ($payload['orders_fetch_enabled'] ?? false)) ? 1 : 0, - 'orders_fetch_start_date' => $this->nullableString((string) ($payload['orders_fetch_start_date'] ?? '')), + 'orders_fetch_start_date' => StringHelper::nullableString((string) ($payload['orders_fetch_start_date'] ?? '')), ]); $this->setActiveEnvironment($env); @@ -185,9 +186,9 @@ final class AllegroIntegrationRepository 'environment' => $env, 'access_token_encrypted' => $this->cipher->encrypt($accessToken), 'refresh_token_encrypted' => $this->cipher->encrypt($refreshToken), - 'token_type' => $this->nullableString($tokenType), - 'token_scope' => $this->nullableString($scope), - 'token_expires_at' => $this->nullableString((string) $tokenExpiresAt), + 'token_type' => StringHelper::nullableString($tokenType), + 'token_scope' => StringHelper::nullableString($scope), + 'token_expires_at' => StringHelper::nullableString((string) $tokenExpiresAt), ]); } @@ -425,9 +426,4 @@ final class AllegroIntegrationRepository return $trimmed; } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } } diff --git a/src/Modules/Settings/AllegroOrderImportService.php b/src/Modules/Settings/AllegroOrderImportService.php index 4e808ef..9ce7104 100644 --- a/src/Modules/Settings/AllegroOrderImportService.php +++ b/src/Modules/Settings/AllegroOrderImportService.php @@ -3,10 +3,9 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use App\Modules\Orders\OrderImportRepository; use App\Modules\Orders\OrdersRepository; -use DateInterval; -use DateTimeImmutable; use RuntimeException; use Throwable; @@ -14,7 +13,7 @@ final class AllegroOrderImportService { public function __construct( private readonly AllegroIntegrationRepository $integrationRepository, - private readonly AllegroOAuthClient $oauthClient, + private readonly AllegroTokenManager $tokenManager, private readonly AllegroApiClient $apiClient, private readonly OrderImportRepository $orders, private readonly AllegroStatusMappingRepository $statusMappings, @@ -32,33 +31,20 @@ final class AllegroOrderImportService throw new RuntimeException('Podaj ID zamowienia Allegro.'); } - $oauth = $this->requireOAuthData(); - [$accessToken, $oauth] = $this->resolveAccessToken($oauth); + [$accessToken, $env] = $this->tokenManager->resolveToken(); try { - $payload = $this->apiClient->getCheckoutForm( - (string) ($oauth['environment'] ?? 'sandbox'), - $accessToken, - $orderId - ); + $payload = $this->apiClient->getCheckoutForm($env, $accessToken, $orderId); } catch (RuntimeException $exception) { if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') { throw $exception; } - [$accessToken, $oauth] = $this->forceRefreshToken($oauth); - $payload = $this->apiClient->getCheckoutForm( - (string) ($oauth['environment'] ?? 'sandbox'), - $accessToken, - $orderId - ); + [$accessToken, $env] = $this->tokenManager->resolveToken(); + $payload = $this->apiClient->getCheckoutForm($env, $accessToken, $orderId); } - $mapped = $this->mapCheckoutFormPayload( - $payload, - (string) ($oauth['environment'] ?? 'sandbox'), - $accessToken - ); + $mapped = $this->mapCheckoutFormPayload($payload, $env, $accessToken); $saveResult = $this->orders->upsertOrderAggregate( $mapped['order'], $mapped['addresses'], @@ -98,91 +84,6 @@ final class AllegroOrderImportService ]; } - /** - * @return array - */ - private function requireOAuthData(): array - { - $oauth = $this->integrationRepository->getTokenCredentials(); - if ($oauth === null) { - throw new RuntimeException('Brak kompletnych danych OAuth Allegro. Polacz konto ponownie.'); - } - - return $oauth; - } - - /** - * @param array $oauth - * @return array{0:string, 1:array} - */ - private function resolveAccessToken(array $oauth): array - { - $tokenExpiresAt = trim((string) ($oauth['token_expires_at'] ?? '')); - $accessToken = trim((string) ($oauth['access_token'] ?? '')); - if ($accessToken === '') { - return $this->forceRefreshToken($oauth); - } - - if ($tokenExpiresAt === '') { - return [$accessToken, $oauth]; - } - - try { - $expiresAt = new DateTimeImmutable($tokenExpiresAt); - } catch (Throwable) { - return $this->forceRefreshToken($oauth); - } - - if ($expiresAt <= (new DateTimeImmutable('now'))->add(new DateInterval('PT5M'))) { - return $this->forceRefreshToken($oauth); - } - - return [$accessToken, $oauth]; - } - - /** - * @param array $oauth - * @return array{0:string, 1:array} - */ - private function forceRefreshToken(array $oauth): array - { - $token = $this->oauthClient->refreshAccessToken( - (string) ($oauth['environment'] ?? 'sandbox'), - (string) ($oauth['client_id'] ?? ''), - (string) ($oauth['client_secret'] ?? ''), - (string) ($oauth['refresh_token'] ?? '') - ); - - $expiresAt = null; - $expiresIn = max(0, (int) ($token['expires_in'] ?? 0)); - if ($expiresIn > 0) { - $expiresAt = (new DateTimeImmutable('now')) - ->add(new DateInterval('PT' . $expiresIn . 'S')) - ->format('Y-m-d H:i:s'); - } - - $refreshToken = trim((string) ($token['refresh_token'] ?? '')); - if ($refreshToken === '') { - $refreshToken = (string) ($oauth['refresh_token'] ?? ''); - } - - $this->integrationRepository->saveTokens( - (string) ($token['access_token'] ?? ''), - $refreshToken, - (string) ($token['token_type'] ?? ''), - (string) ($token['scope'] ?? ''), - $expiresAt - ); - - $updatedOauth = $this->requireOAuthData(); - $newAccessToken = trim((string) ($updatedOauth['access_token'] ?? '')); - if ($newAccessToken === '') { - throw new RuntimeException('Nie udalo sie zapisac odswiezonego tokenu Allegro.'); - } - - return [$newAccessToken, $updatedOauth]; - } - /** * @param array $payload * @return array{ @@ -238,17 +139,17 @@ final class AllegroOrderImportService $deliveryForm = $deliveryMethodName !== '' ? $deliveryMethodName : $deliveryMethodId; $deliveryTime = is_array($delivery['time'] ?? null) ? $delivery['time'] : []; $dispatchTime = is_array($deliveryTime['dispatch'] ?? null) ? $deliveryTime['dispatch'] : []; - $sendDateMin = $this->normalizeDateTime((string) ($dispatchTime['from'] ?? '')); - $sendDateMax = $this->normalizeDateTime((string) ($dispatchTime['to'] ?? '')); + $sendDateMin = StringHelper::normalizeDateTime((string) ($dispatchTime['from'] ?? '')); + $sendDateMax = StringHelper::normalizeDateTime((string) ($dispatchTime['to'] ?? '')); if ($sendDateMin === null) { - $sendDateMin = $this->normalizeDateTime((string) ($deliveryTime['from'] ?? '')); + $sendDateMin = StringHelper::normalizeDateTime((string) ($deliveryTime['from'] ?? '')); } if ($sendDateMax === null) { - $sendDateMax = $this->normalizeDateTime((string) ($deliveryTime['to'] ?? '')); + $sendDateMax = StringHelper::normalizeDateTime((string) ($deliveryTime['to'] ?? '')); } - $boughtAt = $this->normalizeDateTime((string) ($payload['boughtAt'] ?? '')); - $updatedAt = $this->normalizeDateTime((string) ($payload['updatedAt'] ?? '')); + $boughtAt = StringHelper::normalizeDateTime((string) ($payload['boughtAt'] ?? '')); + $updatedAt = StringHelper::normalizeDateTime((string) ($payload['updatedAt'] ?? '')); $fetchedAt = date('Y-m-d H:i:s'); $order = [ @@ -347,8 +248,8 @@ final class AllegroOrderImportService $result[] = [ 'address_type' => 'customer', 'name' => $customerName, - 'phone' => $this->nullableString((string) ($buyer['phoneNumber'] ?? '')), - 'email' => $this->nullableString((string) ($buyer['email'] ?? '')), + 'phone' => StringHelper::nullableString((string) ($buyer['phoneNumber'] ?? '')), + 'email' => StringHelper::nullableString((string) ($buyer['email'] ?? '')), 'street_name' => null, 'street_number' => null, 'city' => null, @@ -374,17 +275,17 @@ final class AllegroOrderImportService $name = $this->fallbackName($deliveryAddress, 'Dostawa'); $street = $isPickupPointDelivery - ? $this->nullableString((string) ($pickupAddress['street'] ?? '')) - : $this->nullableString((string) ($deliveryAddress['street'] ?? '')); + ? StringHelper::nullableString((string) ($pickupAddress['street'] ?? '')) + : StringHelper::nullableString((string) ($deliveryAddress['street'] ?? '')); $city = $isPickupPointDelivery - ? $this->nullableString((string) ($pickupAddress['city'] ?? '')) - : $this->nullableString((string) ($deliveryAddress['city'] ?? '')); + ? StringHelper::nullableString((string) ($pickupAddress['city'] ?? '')) + : StringHelper::nullableString((string) ($deliveryAddress['city'] ?? '')); $zipCode = $isPickupPointDelivery - ? $this->nullableString((string) ($pickupAddress['zipCode'] ?? '')) - : $this->nullableString((string) ($deliveryAddress['zipCode'] ?? '')); + ? StringHelper::nullableString((string) ($pickupAddress['zipCode'] ?? '')) + : StringHelper::nullableString((string) ($deliveryAddress['zipCode'] ?? '')); $country = $isPickupPointDelivery - ? $this->nullableString((string) ($pickupAddress['countryCode'] ?? '')) - : $this->nullableString((string) ($deliveryAddress['countryCode'] ?? '')); + ? StringHelper::nullableString((string) ($pickupAddress['countryCode'] ?? '')) + : StringHelper::nullableString((string) ($deliveryAddress['countryCode'] ?? '')); $deliveryPhone = trim((string) ($deliveryAddress['phoneNumber'] ?? '')); $buyerPhone = trim((string) ($buyer['phoneNumber'] ?? '')); @@ -392,19 +293,19 @@ final class AllegroOrderImportService $result[] = [ 'address_type' => 'delivery', 'name' => $name, - 'phone' => $this->nullableString($deliveryPhone !== '' ? $deliveryPhone : $buyerPhone), - 'email' => $this->nullableString((string) ($deliveryAddress['email'] ?? $buyer['email'] ?? '')), + 'phone' => StringHelper::nullableString($deliveryPhone !== '' ? $deliveryPhone : $buyerPhone), + 'email' => StringHelper::nullableString((string) ($deliveryAddress['email'] ?? $buyer['email'] ?? '')), 'street_name' => $street, 'street_number' => null, 'city' => $city, 'zip_code' => $zipCode, 'country' => $country, 'department' => null, - 'parcel_external_id' => $this->nullableString((string) ($pickupPoint['id'] ?? '')), - 'parcel_name' => $this->nullableString((string) ($pickupPoint['name'] ?? '')), + 'parcel_external_id' => StringHelper::nullableString((string) ($pickupPoint['id'] ?? '')), + 'parcel_name' => StringHelper::nullableString((string) ($pickupPoint['name'] ?? '')), 'address_class' => null, 'company_tax_number' => null, - 'company_name' => $this->nullableString((string) ($deliveryAddress['companyName'] ?? '')), + 'company_name' => StringHelper::nullableString((string) ($deliveryAddress['companyName'] ?? '')), 'payload_json' => [ 'address' => $deliveryAddress, 'pickup_point' => $pickupPoint, @@ -417,19 +318,19 @@ final class AllegroOrderImportService $result[] = [ 'address_type' => 'invoice', 'name' => $this->fallbackName($invoiceAddress, 'Faktura'), - 'phone' => $this->nullableString((string) ($invoiceAddress['phoneNumber'] ?? '')), - 'email' => $this->nullableString((string) ($invoiceAddress['email'] ?? '')), - 'street_name' => $this->nullableString((string) ($invoiceAddress['street'] ?? '')), + 'phone' => StringHelper::nullableString((string) ($invoiceAddress['phoneNumber'] ?? '')), + 'email' => StringHelper::nullableString((string) ($invoiceAddress['email'] ?? '')), + 'street_name' => StringHelper::nullableString((string) ($invoiceAddress['street'] ?? '')), 'street_number' => null, - 'city' => $this->nullableString((string) ($invoiceAddress['city'] ?? '')), - 'zip_code' => $this->nullableString((string) ($invoiceAddress['zipCode'] ?? '')), - 'country' => $this->nullableString((string) ($invoiceAddress['countryCode'] ?? '')), + 'city' => StringHelper::nullableString((string) ($invoiceAddress['city'] ?? '')), + 'zip_code' => StringHelper::nullableString((string) ($invoiceAddress['zipCode'] ?? '')), + 'country' => StringHelper::nullableString((string) ($invoiceAddress['countryCode'] ?? '')), 'department' => null, 'parcel_external_id' => null, 'parcel_name' => null, 'address_class' => null, - 'company_tax_number' => $this->nullableString((string) ($invoiceAddress['taxId'] ?? '')), - 'company_name' => $this->nullableString((string) ($invoiceAddress['companyName'] ?? '')), + 'company_tax_number' => StringHelper::nullableString((string) ($invoiceAddress['taxId'] ?? '')), + 'company_name' => StringHelper::nullableString((string) ($invoiceAddress['companyName'] ?? '')), 'payload_json' => $invoiceAddress, ]; } @@ -516,12 +417,12 @@ final class AllegroOrderImportService } $result[] = [ - 'source_item_id' => $this->nullableString((string) ($itemRaw['id'] ?? '')), - 'external_item_id' => $this->nullableString((string) ($offer['id'] ?? '')), + 'source_item_id' => StringHelper::nullableString((string) ($itemRaw['id'] ?? '')), + 'external_item_id' => StringHelper::nullableString((string) ($offer['id'] ?? '')), 'ean' => null, 'sku' => null, 'original_name' => $name, - 'original_code' => $this->nullableString((string) ($offer['id'] ?? '')), + 'original_code' => StringHelper::nullableString((string) ($offer['id'] ?? '')), 'original_price_with_tax' => $this->amountToFloat($itemRaw['originalPrice'] ?? null), 'original_price_without_tax' => null, 'media_url' => $mediaUrl, @@ -530,7 +431,7 @@ final class AllegroOrderImportService 'item_status' => null, 'unit' => 'pcs', 'item_type' => 'product', - 'source_product_id' => $this->nullableString((string) ($offer['id'] ?? '')), + 'source_product_id' => StringHelper::nullableString((string) ($offer['id'] ?? '')), 'source_product_set_id' => null, 'sort_order' => $sortOrder++, 'payload_json' => $itemRaw, @@ -703,10 +604,10 @@ final class AllegroOrderImportService 'source_payment_id' => $paymentId, 'external_payment_id' => $paymentId, 'payment_type_id' => trim((string) ($payment['type'] ?? 'allegro')), - 'payment_date' => $this->normalizeDateTime((string) ($payment['finishedAt'] ?? '')), + 'payment_date' => StringHelper::normalizeDateTime((string) ($payment['finishedAt'] ?? '')), 'amount' => $amount, - 'currency' => $this->nullableString((string) ($payment['amount']['currency'] ?? $fallbackCurrency)), - 'comment' => $this->nullableString((string) ($payment['provider'] ?? '')), + 'currency' => StringHelper::nullableString((string) ($payment['amount']['currency'] ?? $fallbackCurrency)), + 'comment' => StringHelper::nullableString((string) ($payment['provider'] ?? '')), 'payload_json' => $payment, ]]; } @@ -731,11 +632,11 @@ final class AllegroOrderImportService $carrierId = trim((string) ($shipmentRaw['carrierId'] ?? $delivery['method']['id'] ?? '')); $carrierName = trim((string) ($shipmentRaw['carrierName'] ?? '')); $result[] = [ - 'source_shipment_id' => $this->nullableString((string) ($shipmentRaw['id'] ?? '')), - 'external_shipment_id' => $this->nullableString((string) ($shipmentRaw['id'] ?? '')), + 'source_shipment_id' => StringHelper::nullableString((string) ($shipmentRaw['id'] ?? '')), + 'external_shipment_id' => StringHelper::nullableString((string) ($shipmentRaw['id'] ?? '')), 'tracking_number' => $trackingNumber, 'carrier_provider_id' => $carrierId !== '' ? $carrierId : ($carrierName !== '' ? $carrierName : 'allegro'), - 'posted_at' => $this->normalizeDateTime((string) ($shipmentRaw['createdAt'] ?? '')), + 'posted_at' => StringHelper::normalizeDateTime((string) ($shipmentRaw['createdAt'] ?? '')), 'media_uuid' => null, 'payload_json' => $shipmentRaw, ]; @@ -758,7 +659,7 @@ final class AllegroOrderImportService return [[ 'source_note_id' => null, 'note_type' => 'buyer_message', - 'created_at_external' => $this->normalizeDateTime((string) ($payload['updatedAt'] ?? '')), + 'created_at_external' => StringHelper::normalizeDateTime((string) ($payload['updatedAt'] ?? '')), 'comment' => $message, 'payload_json' => ['messageToSeller' => $message], ]]; @@ -787,20 +688,6 @@ final class AllegroOrderImportService return (float) $value; } - private function normalizeDateTime(string $value): ?string - { - $trimmed = trim($value); - if ($trimmed === '') { - return null; - } - - try { - return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s'); - } catch (Throwable) { - return null; - } - } - /** * @param array $address */ @@ -819,9 +706,4 @@ final class AllegroOrderImportService return $fallback; } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } } diff --git a/src/Modules/Settings/AllegroOrderSyncStateRepository.php b/src/Modules/Settings/AllegroOrderSyncStateRepository.php index 13b1329..d8ab97d 100644 --- a/src/Modules/Settings/AllegroOrderSyncStateRepository.php +++ b/src/Modules/Settings/AllegroOrderSyncStateRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use DateTimeImmutable; use PDO; use Throwable; @@ -68,11 +69,11 @@ final class AllegroOrderSyncStateRepository } return [ - 'last_synced_updated_at' => $this->nullableString((string) ($row['last_synced_updated_at'] ?? '')), - 'last_synced_source_order_id' => $this->nullableString((string) ($row['last_synced_source_order_id'] ?? '')), - 'last_run_at' => $this->nullableString((string) ($row['last_run_at'] ?? '')), - 'last_success_at' => $this->nullableString((string) ($row['last_success_at'] ?? '')), - 'last_error' => $this->nullableString((string) ($row['last_error'] ?? '')), + 'last_synced_updated_at' => StringHelper::nullableString((string) ($row['last_synced_updated_at'] ?? '')), + 'last_synced_source_order_id' => StringHelper::nullableString((string) ($row['last_synced_source_order_id'] ?? '')), + 'last_run_at' => StringHelper::nullableString((string) ($row['last_run_at'] ?? '')), + 'last_success_at' => StringHelper::nullableString((string) ($row['last_success_at'] ?? '')), + 'last_error' => StringHelper::nullableString((string) ($row['last_error'] ?? '')), ]; } @@ -267,9 +268,4 @@ final class AllegroOrderSyncStateRepository ]; } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } } diff --git a/src/Modules/Settings/AllegroOrdersSyncService.php b/src/Modules/Settings/AllegroOrdersSyncService.php index 384c44f..fcae563 100644 --- a/src/Modules/Settings/AllegroOrdersSyncService.php +++ b/src/Modules/Settings/AllegroOrdersSyncService.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; -use DateInterval; +use App\Core\Support\StringHelper; use DateTimeImmutable; use RuntimeException; use Throwable; @@ -13,7 +13,7 @@ final class AllegroOrdersSyncService public function __construct( private readonly AllegroIntegrationRepository $integrationRepository, private readonly AllegroOrderSyncStateRepository $syncStateRepository, - private readonly AllegroOAuthClient $oauthClient, + private readonly AllegroTokenManager $tokenManager, private readonly AllegroApiClient $apiClient, private readonly AllegroOrderImportService $orderImportService ) { @@ -56,8 +56,8 @@ final class AllegroOrdersSyncService $startDateRaw = trim((string) ($settings['orders_fetch_start_date'] ?? '')); $startDate = $this->normalizeStartDate($startDateRaw); - $cursorUpdatedAt = $this->nullableString((string) ($state['last_synced_updated_at'] ?? '')); - $cursorSourceOrderId = $this->nullableString((string) ($state['last_synced_source_order_id'] ?? '')); + $cursorUpdatedAt = StringHelper::nullableString((string) ($state['last_synced_updated_at'] ?? '')); + $cursorSourceOrderId = StringHelper::nullableString((string) ($state['last_synced_source_order_id'] ?? '')); $result = [ 'enabled' => true, @@ -75,31 +75,20 @@ final class AllegroOrdersSyncService $latestProcessedSourceOrderId = $cursorSourceOrderId; try { - $oauth = $this->requireOAuthData(); - [$accessToken, $oauth] = $this->resolveAccessToken($oauth); + [$accessToken, $env] = $this->tokenManager->resolveToken(); $offset = 0; $shouldStop = false; for ($page = 0; $page < $maxPages; $page++) { try { - $response = $this->apiClient->listCheckoutForms( - (string) ($oauth['environment'] ?? 'sandbox'), - $accessToken, - $pageLimit, - $offset - ); + $response = $this->apiClient->listCheckoutForms($env, $accessToken, $pageLimit, $offset); } catch (RuntimeException $exception) { if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') { throw $exception; } - [$accessToken, $oauth] = $this->forceRefreshToken($oauth); - $response = $this->apiClient->listCheckoutForms( - (string) ($oauth['environment'] ?? 'sandbox'), - $accessToken, - $pageLimit, - $offset - ); + [$accessToken, $env] = $this->tokenManager->resolveToken(); + $response = $this->apiClient->listCheckoutForms($env, $accessToken, $pageLimit, $offset); } $forms = is_array($response['checkoutForms'] ?? null) ? $response['checkoutForms'] : []; @@ -113,7 +102,7 @@ final class AllegroOrdersSyncService } $sourceOrderId = trim((string) ($form['id'] ?? '')); - $sourceUpdatedAt = $this->normalizeDateTime((string) ($form['updatedAt'] ?? $form['boughtAt'] ?? '')); + $sourceUpdatedAt = StringHelper::normalizeDateTime((string) ($form['updatedAt'] ?? $form['boughtAt'] ?? '')); if ($sourceOrderId === '' || $sourceUpdatedAt === null) { $result['skipped'] = (int) $result['skipped'] + 1; continue; @@ -192,105 +181,6 @@ final class AllegroOrdersSyncService } } - /** - * @return array - */ - private function requireOAuthData(): array - { - $oauth = $this->integrationRepository->getTokenCredentials(); - if ($oauth === null) { - throw new RuntimeException('Brak kompletnych danych OAuth Allegro. Polacz konto ponownie.'); - } - - return $oauth; - } - - /** - * @param array $oauth - * @return array{0:string, 1:array} - */ - private function resolveAccessToken(array $oauth): array - { - $tokenExpiresAt = trim((string) ($oauth['token_expires_at'] ?? '')); - $accessToken = trim((string) ($oauth['access_token'] ?? '')); - if ($accessToken === '') { - return $this->forceRefreshToken($oauth); - } - - if ($tokenExpiresAt === '') { - return [$accessToken, $oauth]; - } - - try { - $expiresAt = new DateTimeImmutable($tokenExpiresAt); - } catch (Throwable) { - return $this->forceRefreshToken($oauth); - } - - if ($expiresAt <= (new DateTimeImmutable('now'))->add(new DateInterval('PT5M'))) { - return $this->forceRefreshToken($oauth); - } - - return [$accessToken, $oauth]; - } - - /** - * @param array $oauth - * @return array{0:string, 1:array} - */ - private function forceRefreshToken(array $oauth): array - { - $token = $this->oauthClient->refreshAccessToken( - (string) ($oauth['environment'] ?? 'sandbox'), - (string) ($oauth['client_id'] ?? ''), - (string) ($oauth['client_secret'] ?? ''), - (string) ($oauth['refresh_token'] ?? '') - ); - - $expiresAt = null; - $expiresIn = max(0, (int) ($token['expires_in'] ?? 0)); - if ($expiresIn > 0) { - $expiresAt = (new DateTimeImmutable('now')) - ->add(new DateInterval('PT' . $expiresIn . 'S')) - ->format('Y-m-d H:i:s'); - } - - $refreshToken = trim((string) ($token['refresh_token'] ?? '')); - if ($refreshToken === '') { - $refreshToken = (string) ($oauth['refresh_token'] ?? ''); - } - - $this->integrationRepository->saveTokens( - (string) ($token['access_token'] ?? ''), - $refreshToken, - (string) ($token['token_type'] ?? ''), - (string) ($token['scope'] ?? ''), - $expiresAt - ); - - $updatedOauth = $this->requireOAuthData(); - $newAccessToken = trim((string) ($updatedOauth['access_token'] ?? '')); - if ($newAccessToken === '') { - throw new RuntimeException('Nie udalo sie zapisac odswiezonego tokenu Allegro.'); - } - - return [$newAccessToken, $updatedOauth]; - } - - private function normalizeDateTime(string $value): ?string - { - $trimmed = trim($value); - if ($trimmed === '') { - return null; - } - - try { - return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s'); - } catch (Throwable) { - return null; - } - } - private function normalizeStartDate(string $value): ?string { $trimmed = trim($value); @@ -328,9 +218,4 @@ final class AllegroOrdersSyncService return strcmp($sourceOrderId, $cursorSourceOrderId) > 0; } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } } diff --git a/src/Modules/Settings/AllegroStatusMappingRepository.php b/src/Modules/Settings/AllegroStatusMappingRepository.php index fcac410..c86fb79 100644 --- a/src/Modules/Settings/AllegroStatusMappingRepository.php +++ b/src/Modules/Settings/AllegroStatusMappingRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use PDO; final class AllegroStatusMappingRepository @@ -59,7 +60,7 @@ final class AllegroStatusMappingRepository ); $statement->execute([ 'allegro_status_code' => $code, - 'allegro_status_name' => $this->nullableString((string) $allegroStatusName), + 'allegro_status_name' => StringHelper::nullableString((string) $allegroStatusName), 'orderpro_status_code' => $orderproCode !== null && $orderproCode !== '' ? $orderproCode : null, ]); } @@ -86,7 +87,7 @@ final class AllegroStatusMappingRepository ); $statement->execute([ 'allegro_status_code' => $code, - 'allegro_status_name' => $this->nullableString((string) $allegroStatusName), + 'allegro_status_name' => StringHelper::nullableString((string) $allegroStatusName), ]); } @@ -123,9 +124,4 @@ final class AllegroStatusMappingRepository return $mapped !== '' ? $mapped : null; } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } } diff --git a/src/Modules/Settings/AllegroTokenManager.php b/src/Modules/Settings/AllegroTokenManager.php new file mode 100644 index 0000000..85d9b83 --- /dev/null +++ b/src/Modules/Settings/AllegroTokenManager.php @@ -0,0 +1,97 @@ +repository->getTokenCredentials(); + if ($oauth === null) { + throw new RuntimeException('Brak polaczenia OAuth Allegro. Polacz konto w Ustawieniach.'); + } + + $env = (string) ($oauth['environment'] ?? 'sandbox'); + $accessToken = trim((string) ($oauth['access_token'] ?? '')); + $tokenExpiresAt = trim((string) ($oauth['token_expires_at'] ?? '')); + + if ($accessToken === '') { + return $this->forceRefresh(); + } + + if ($tokenExpiresAt !== '') { + try { + $expiresAt = new DateTimeImmutable($tokenExpiresAt); + if ($expiresAt <= (new DateTimeImmutable('now'))->add(new DateInterval('PT5M'))) { + return $this->forceRefresh(); + } + } catch (Throwable) { + return $this->forceRefresh(); + } + } + + return [$accessToken, $env]; + } + + /** + * @return array{0: string, 1: string} + */ + private function forceRefresh(): array + { + $oauth = $this->repository->getTokenCredentials(); + if ($oauth === null) { + throw new RuntimeException('Brak danych OAuth Allegro.'); + } + + $token = $this->oauthClient->refreshAccessToken( + (string) ($oauth['environment'] ?? 'sandbox'), + (string) ($oauth['client_id'] ?? ''), + (string) ($oauth['client_secret'] ?? ''), + (string) ($oauth['refresh_token'] ?? '') + ); + + $expiresAt = null; + $expiresIn = max(0, (int) ($token['expires_in'] ?? 0)); + if ($expiresIn > 0) { + $expiresAt = (new DateTimeImmutable('now')) + ->add(new DateInterval('PT' . $expiresIn . 'S')) + ->format('Y-m-d H:i:s'); + } + + $refreshToken = trim((string) ($token['refresh_token'] ?? '')); + if ($refreshToken === '') { + $refreshToken = (string) ($oauth['refresh_token'] ?? ''); + } + + $this->repository->saveTokens( + (string) ($token['access_token'] ?? ''), + $refreshToken, + (string) ($token['token_type'] ?? ''), + (string) ($token['scope'] ?? ''), + $expiresAt + ); + + $updated = $this->repository->getTokenCredentials(); + $newToken = trim((string) ($updated['access_token'] ?? '')); + if ($newToken === '') { + throw new RuntimeException('Nie udalo sie odswiezyc tokenu Allegro.'); + } + + return [$newToken, (string) ($updated['environment'] ?? 'sandbox')]; + } +} diff --git a/src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php b/src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php index 4bf34ad..7efcd84 100644 --- a/src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php +++ b/src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php @@ -194,12 +194,6 @@ final class CarrierDeliveryMethodMappingRepository return in_array($source, ['allegro', 'shoppro'], true) ? $source : 'allegro'; } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } - private function nullableLimited(string $value, int $max): ?string { $trimmed = $this->limit(trim($value), $max); diff --git a/src/Modules/Settings/CompanySettingsRepository.php b/src/Modules/Settings/CompanySettingsRepository.php index abd8d2f..0a0306d 100644 --- a/src/Modules/Settings/CompanySettingsRepository.php +++ b/src/Modules/Settings/CompanySettingsRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use PDO; use Throwable; @@ -82,18 +83,18 @@ final class CompanySettingsRepository ); $statement->execute([ - 'company_name' => $this->nullableString((string) ($data['company_name'] ?? '')), - 'person_name' => $this->nullableString((string) ($data['person_name'] ?? '')), - 'sender_contact_person' => $this->nullableString((string) ($data['sender_contact_person'] ?? '')), - 'street' => $this->nullableString((string) ($data['street'] ?? '')), - 'city' => $this->nullableString((string) ($data['city'] ?? '')), - 'postal_code' => $this->nullableString((string) ($data['postal_code'] ?? '')), + 'company_name' => StringHelper::nullableString((string) ($data['company_name'] ?? '')), + 'person_name' => StringHelper::nullableString((string) ($data['person_name'] ?? '')), + 'sender_contact_person' => StringHelper::nullableString((string) ($data['sender_contact_person'] ?? '')), + 'street' => StringHelper::nullableString((string) ($data['street'] ?? '')), + 'city' => StringHelper::nullableString((string) ($data['city'] ?? '')), + 'postal_code' => StringHelper::nullableString((string) ($data['postal_code'] ?? '')), 'country_code' => strtoupper(trim((string) ($data['country_code'] ?? 'PL'))) ?: 'PL', - 'phone' => $this->nullableString((string) ($data['phone'] ?? '')), - 'email' => $this->nullableString((string) ($data['email'] ?? '')), - 'tax_number' => $this->nullableString((string) ($data['tax_number'] ?? '')), - 'bank_account' => $this->nullableString((string) ($data['bank_account'] ?? '')), - 'bank_owner_name' => $this->nullableString((string) ($data['bank_owner_name'] ?? '')), + 'phone' => StringHelper::nullableString((string) ($data['phone'] ?? '')), + 'email' => StringHelper::nullableString((string) ($data['email'] ?? '')), + 'tax_number' => StringHelper::nullableString((string) ($data['tax_number'] ?? '')), + 'bank_account' => StringHelper::nullableString((string) ($data['bank_account'] ?? '')), + 'bank_owner_name' => StringHelper::nullableString((string) ($data['bank_owner_name'] ?? '')), 'length' => max(0.1, (float) ($data['default_package_length_cm'] ?? 25.0)), 'width' => max(0.1, (float) ($data['default_package_width_cm'] ?? 20.0)), 'height' => max(0.1, (float) ($data['default_package_height_cm'] ?? 8.0)), @@ -132,12 +133,6 @@ final class CompanySettingsRepository ); } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } - /** * @return array */ diff --git a/src/Modules/Settings/InpostIntegrationRepository.php b/src/Modules/Settings/InpostIntegrationRepository.php index 3471b5d..5207d98 100644 --- a/src/Modules/Settings/InpostIntegrationRepository.php +++ b/src/Modules/Settings/InpostIntegrationRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use PDO; use RuntimeException; use Throwable; @@ -98,15 +99,15 @@ final class InpostIntegrationRepository WHERE id = 1' ); $statement->execute([ - 'api_token_encrypted' => $this->nullableString((string) $nextEncrypted), - 'organization_id' => $this->nullableString(trim((string) ($payload['organization_id'] ?? ''))), + 'api_token_encrypted' => StringHelper::nullableString((string) $nextEncrypted), + 'organization_id' => StringHelper::nullableString(trim((string) ($payload['organization_id'] ?? ''))), 'environment' => in_array($payload['environment'] ?? '', ['sandbox', 'production'], true) ? $payload['environment'] : 'sandbox', 'default_dispatch_method' => in_array($payload['default_dispatch_method'] ?? '', ['pop', 'parcel_locker', 'courier'], true) ? $payload['default_dispatch_method'] : 'pop', - 'default_dispatch_point' => $this->nullableString(trim((string) ($payload['default_dispatch_point'] ?? ''))), + 'default_dispatch_point' => StringHelper::nullableString(trim((string) ($payload['default_dispatch_point'] ?? ''))), 'default_insurance' => ($payload['default_insurance'] ?? '') !== '' ? (float) $payload['default_insurance'] : null, @@ -215,9 +216,4 @@ final class InpostIntegrationRepository ); } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } } diff --git a/src/Modules/Settings/IntegrationsRepository.php b/src/Modules/Settings/IntegrationsRepository.php index 3060a8d..a9f52b6 100644 --- a/src/Modules/Settings/IntegrationsRepository.php +++ b/src/Modules/Settings/IntegrationsRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use PDO; use Throwable; @@ -119,7 +120,7 @@ final class IntegrationsRepository ); $statement->execute([ 'id' => $integrationId, - 'api_key_encrypted' => $this->nullableString((string) $encrypted), + 'api_key_encrypted' => StringHelper::nullableString((string) $encrypted), ]); } @@ -147,9 +148,4 @@ final class IntegrationsRepository return $trimmed === '' ? null : $trimmed; } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } } diff --git a/src/Modules/Settings/SettingsController.php b/src/Modules/Settings/SettingsController.php index 5649067..f7a8fda 100644 --- a/src/Modules/Settings/SettingsController.php +++ b/src/Modules/Settings/SettingsController.php @@ -9,6 +9,7 @@ use App\Core\Http\Response; use App\Core\I18n\Translator; use App\Core\Security\Csrf; use App\Core\Support\Flash; +use App\Core\Support\StringHelper; use App\Core\View\Template; use App\Modules\Auth\AuthService; use Throwable; @@ -113,7 +114,7 @@ final class SettingsController return Response::redirect('/settings/statuses'); } - $colorHex = $this->normalizeColorHex((string) $request->input('color_hex', '#64748b')); + $colorHex = StringHelper::normalizeColorHex((string) $request->input('color_hex', '#64748b')); $isActive = (string) $request->input('is_active', '1') === '1'; try { @@ -147,7 +148,7 @@ final class SettingsController return Response::redirect('/settings/statuses'); } - $colorHex = $this->normalizeColorHex((string) $request->input('color_hex', '#64748b')); + $colorHex = StringHelper::normalizeColorHex((string) $request->input('color_hex', '#64748b')); $isActive = (string) $request->input('is_active', '0') === '1'; $code = trim((string) ($existingGroup['code'] ?? '')); @@ -352,16 +353,6 @@ final class SettingsController return Response::redirect('/settings/statuses'); } - private function normalizeColorHex(string $value): string - { - $trimmed = trim($value); - if (preg_match('/^#[0-9a-fA-F]{6}$/', $trimmed) === 1) { - return strtolower($trimmed); - } - - return '#64748b'; - } - private function normalizeCode(string $code, string $fallbackName = ''): string { $source = trim($code) !== '' ? $code : $fallbackName; diff --git a/src/Modules/Settings/ShopproIntegrationsRepository.php b/src/Modules/Settings/ShopproIntegrationsRepository.php index 5a54e51..26ccb2d 100644 --- a/src/Modules/Settings/ShopproIntegrationsRepository.php +++ b/src/Modules/Settings/ShopproIntegrationsRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use PDO; use RuntimeException; use Throwable; @@ -180,7 +181,7 @@ final class ShopproIntegrationsRepository 'type' => self::TYPE, 'name' => $name, 'base_url' => $baseUrl, - 'api_key_encrypted' => $this->nullableString($encryptedApiKey), + 'api_key_encrypted' => StringHelper::nullableString($encryptedApiKey), 'timeout_seconds' => $timeoutSeconds, 'is_active' => $isActive, 'orders_fetch_enabled' => $ordersFetchEnabled, @@ -204,7 +205,7 @@ final class ShopproIntegrationsRepository 'type' => self::TYPE, 'name' => $name, 'base_url' => $baseUrl, - 'api_key_encrypted' => $this->nullableString((string) $encryptedApiKey), + 'api_key_encrypted' => StringHelper::nullableString((string) $encryptedApiKey), 'timeout_seconds' => $timeoutSeconds, 'is_active' => $isActive, 'orders_fetch_enabled' => $ordersFetchEnabled, @@ -424,9 +425,9 @@ final class ShopproIntegrationsRepository $statement->execute([ 'id' => $integrationId, 'type' => self::TYPE, - 'last_test_status' => $this->nullableString($status), + 'last_test_status' => StringHelper::nullableString($status), 'last_test_http_code' => $httpCode, - 'last_test_message' => $this->nullableString($message), + 'last_test_message' => StringHelper::nullableString($message), ]); $log = $this->pdo->prepare( @@ -448,12 +449,6 @@ final class ShopproIntegrationsRepository } } - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } - private function normalizeDate(string $value): ?string { $trimmed = trim($value); diff --git a/src/Modules/Settings/ShopproOrdersSyncService.php b/src/Modules/Settings/ShopproOrdersSyncService.php index 1bc3477..da62a75 100644 --- a/src/Modules/Settings/ShopproOrdersSyncService.php +++ b/src/Modules/Settings/ShopproOrdersSyncService.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use App\Modules\Orders\OrderImportRepository; use App\Modules\Orders\OrdersRepository; use DateTimeImmutable; @@ -63,8 +64,8 @@ final class ShopproOrdersSyncService try { $statusMap = $this->buildStatusMap($integrationId); - $cursorUpdatedAt = $this->nullableString((string) ($state['last_synced_updated_at'] ?? '')); - $cursorOrderId = $this->nullableString((string) ($state['last_synced_source_order_id'] ?? '')); + $cursorUpdatedAt = StringHelper::nullableString((string) ($state['last_synced_updated_at'] ?? '')); + $cursorOrderId = StringHelper::nullableString((string) ($state['last_synced_source_order_id'] ?? '')); $startDate = $this->resolveStartDate( (string) ($integration['orders_fetch_start_date'] ?? ''), $cursorUpdatedAt @@ -265,7 +266,7 @@ final class ShopproOrdersSyncService private function resolveStartDate(string $settingsDate, ?string $cursorUpdatedAt): ?string { $settings = trim($settingsDate); - $cursor = $this->nullableString((string) $cursorUpdatedAt); + $cursor = StringHelper::nullableString((string) $cursorUpdatedAt); if ($settings === '' && $cursor === null) { return null; } @@ -288,7 +289,7 @@ final class ShopproOrdersSyncService $result = []; foreach ($items as $row) { $sourceOrderId = $this->normalizeOrderId($this->readPath($row, ['id', 'order_id', 'external_order_id'])); - $sourceUpdatedAt = $this->normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created'])); + $sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created'])); if ($sourceOrderId === '' || $sourceUpdatedAt === null) { continue; } @@ -360,8 +361,8 @@ final class ShopproOrdersSyncService $sourceOrderId = $fallbackOrderId; } - $sourceCreatedAt = $this->normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add'])); - $sourceUpdatedAt = $this->normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at'])); + $sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add'])); + $sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at'])); if ($sourceUpdatedAt === null) { $sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s'); } @@ -425,13 +426,13 @@ final class ShopproOrdersSyncService 'external_platform_id' => 'shoppro', 'external_platform_account_id' => null, 'external_status_id' => $effectiveStatus, - 'external_payment_type_id' => $this->nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])), + 'external_payment_type_id' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])), 'payment_status' => $this->mapPaymentStatus($payload, $isPaid), - 'external_carrier_id' => $this->nullableString($deliveryLabel), - 'external_carrier_account_id' => $this->nullableString((string) $this->readPath($payload, [ + 'external_carrier_id' => StringHelper::nullableString($deliveryLabel), + 'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [ 'transport_id', 'shipping.method_id', 'delivery.method_id', ])), - 'customer_login' => $this->nullableString((string) $this->readPath($payload, [ + 'customer_login' => StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login', ])), 'is_invoice' => $this->resolveInvoiceRequested($payload), @@ -484,17 +485,17 @@ final class ShopproOrdersSyncService { $result = []; - $customerFirstName = $this->nullableString((string) $this->readPath($payload, [ + $customerFirstName = StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer.first_name', 'buyer.firstname', 'customer.first_name', 'customer.firstname', 'client.first_name', 'client.firstname', 'billing_address.first_name', 'billing_address.firstname', 'first_name', 'firstname', 'client_name', 'imie', ])); - $customerLastName = $this->nullableString((string) $this->readPath($payload, [ + $customerLastName = StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer.last_name', 'buyer.lastname', 'customer.last_name', 'customer.lastname', 'client.last_name', 'client.lastname', 'billing_address.last_name', 'billing_address.lastname', 'last_name', 'lastname', 'client_surname', 'nazwisko', ])); - $customerName = $this->nullableString((string) $this->readPath($payload, [ + $customerName = StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_name', 'buyer.name', 'customer.name', 'client.name', 'billing_address.name', 'receiver.name', 'client', 'customer_full_name', 'client_full_name', ])); @@ -502,11 +503,11 @@ final class ShopproOrdersSyncService $customerName = $this->composeName($customerFirstName, $customerLastName, 'Klient'); } - $customerEmail = $this->nullableString((string) $this->readPath($payload, [ + $customerEmail = StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_email', 'buyer.email', 'customer.email', 'client.email', 'billing_address.email', 'shipping_address.email', 'delivery_address.email', 'email', 'client_email', 'mail', ])); - $customerPhone = $this->nullableString((string) $this->readPath($payload, [ + $customerPhone = StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_phone', 'buyer.phone', 'customer.phone', 'client.phone', 'billing_address.phone', 'shipping_address.phone', 'delivery_address.phone', 'phone', 'telephone', 'client_phone', 'phone_number', 'client_phone_number', @@ -517,25 +518,25 @@ final class ShopproOrdersSyncService 'name' => $customerName ?? 'Klient', 'phone' => $customerPhone, 'email' => $customerEmail, - 'street_name' => $this->nullableString((string) $this->readPath($payload, [ + 'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_address.street', 'customer.address.street', 'billing_address.street', 'client.address.street', 'address.street', 'street', 'client_street', 'ulica', ])), - 'street_number' => $this->nullableString((string) $this->readPath($payload, [ + 'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_address.street_number', 'customer.address.street_number', 'billing_address.street_number', 'billing_address.house_number', 'client.address.street_number', 'address.street_number', 'house_number', 'street_no', 'street_number', 'nr_domu', ])), - 'city' => $this->nullableString((string) $this->readPath($payload, [ + 'city' => StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_address.city', 'customer.address.city', 'billing_address.city', 'client.address.city', 'address.city', 'city', 'client_city', 'miejscowosc', ])), - 'zip_code' => $this->nullableString((string) $this->readPath($payload, [ + 'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_address.zip', 'buyer_address.postcode', 'customer.address.zip', 'customer.address.postcode', 'billing_address.zip', 'billing_address.postcode', 'client.address.zip', 'address.zip', 'address.postcode', 'zip', 'postcode', 'client_postal_code', 'kod_pocztowy', ])), - 'country' => $this->nullableString((string) $this->readPath($payload, [ + 'country' => StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_address.country', 'customer.address.country', 'billing_address.country', 'client.address.country', 'address.country', 'country', 'kraj', ])), @@ -553,17 +554,17 @@ final class ShopproOrdersSyncService $result[] = $invoiceAddress; } - $deliveryFirstName = $this->nullableString((string) $this->readPath($payload, [ + $deliveryFirstName = StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.first_name', 'delivery.address.firstname', 'shipping.address.first_name', 'shipping.address.firstname', 'delivery_address.first_name', 'delivery_address.firstname', 'shipping_address.first_name', 'shipping_address.firstname', 'receiver.first_name', 'receiver.firstname', 'delivery_first_name', 'shipping_first_name', ])); - $deliveryLastName = $this->nullableString((string) $this->readPath($payload, [ + $deliveryLastName = StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.last_name', 'delivery.address.lastname', 'shipping.address.last_name', 'shipping.address.lastname', 'delivery_address.last_name', 'delivery_address.lastname', 'shipping_address.last_name', 'shipping_address.lastname', 'receiver.last_name', 'receiver.lastname', 'delivery_last_name', 'shipping_last_name', ])); - $deliveryName = $this->nullableString((string) $this->readPath($payload, [ + $deliveryName = StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.name', 'shipping.address.name', 'delivery_address.name', 'shipping_address.name', 'receiver.name', 'delivery_name', 'shipping_name', ])); @@ -574,39 +575,39 @@ final class ShopproOrdersSyncService $pickupData = $this->parsePickupPoint((string) $this->readPath($payload, ['inpost_paczkomat', 'orlen_point', 'pickup_point'])); $deliveryAddress = [ 'name' => $deliveryName, - 'phone' => $this->nullableString((string) $this->readPath($payload, [ + 'phone' => StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.phone', 'shipping.address.phone', 'delivery_address.phone', 'shipping_address.phone', 'receiver.phone', 'delivery_phone', 'shipping_phone', ])) ?? $customerPhone, - 'email' => $this->nullableString((string) $this->readPath($payload, [ + 'email' => StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.email', 'shipping.address.email', 'delivery_address.email', 'shipping_address.email', 'receiver.email', 'delivery_email', 'shipping_email', ])) ?? $customerEmail, - 'street_name' => $this->nullableString((string) $this->readPath($payload, [ + 'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.street', 'shipping.address.street', 'delivery_address.street', 'shipping_address.street', 'receiver.address.street', 'delivery_street', 'shipping_street', - ])) ?? $this->nullableString($pickupData['street'] ?? ''), - 'street_number' => $this->nullableString((string) $this->readPath($payload, [ + ])) ?? StringHelper::nullableString($pickupData['street'] ?? ''), + 'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.street_number', 'shipping.address.street_number', 'delivery_address.street_number', 'shipping_address.street_number', 'delivery.address.house_number', 'shipping.address.house_number', 'receiver.address.street_number', 'receiver.address.house_number', 'delivery_street_number', 'shipping_street_number', ])), - 'city' => $this->nullableString((string) $this->readPath($payload, [ + 'city' => StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.city', 'shipping.address.city', 'delivery_address.city', 'shipping_address.city', 'receiver.address.city', 'delivery_city', 'shipping_city', - ])) ?? $this->nullableString($pickupData['city'] ?? ''), - 'zip_code' => $this->nullableString((string) $this->readPath($payload, [ + ])) ?? StringHelper::nullableString($pickupData['city'] ?? ''), + 'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.zip', 'delivery.address.postcode', 'shipping.address.zip', 'shipping.address.postcode', 'delivery_address.zip', 'delivery_address.postcode', 'shipping_address.zip', 'shipping_address.postcode', 'receiver.address.zip', 'receiver.address.postcode', 'delivery_zip', 'delivery_postcode', 'shipping_zip', 'shipping_postcode', - ])) ?? $this->nullableString($pickupData['zip_code'] ?? ''), - 'country' => $this->nullableString((string) $this->readPath($payload, [ + ])) ?? StringHelper::nullableString($pickupData['zip_code'] ?? ''), + 'country' => StringHelper::nullableString((string) $this->readPath($payload, [ 'delivery.address.country', 'shipping.address.country', 'delivery_address.country', 'shipping_address.country', 'receiver.address.country', 'delivery_country', 'shipping_country', ])), - 'parcel_external_id' => $this->nullableString($pickupData['code'] ?? ''), - 'parcel_name' => $this->nullableString($pickupData['label'] ?? ''), + 'parcel_external_id' => StringHelper::nullableString($pickupData['code'] ?? ''), + 'parcel_name' => StringHelper::nullableString($pickupData['label'] ?? ''), 'payload_json' => [ 'delivery' => $this->readPath($payload, ['delivery']), 'shipping' => $this->readPath($payload, ['shipping']), @@ -619,7 +620,7 @@ final class ShopproOrdersSyncService ]; if (($deliveryAddress['name'] ?? null) === null) { - $deliveryAddress['name'] = $this->nullableString($this->buildDeliveryMethodLabel($payload)); + $deliveryAddress['name'] = StringHelper::nullableString($this->buildDeliveryMethodLabel($payload)); } $hasDeliveryData = $this->hasAddressData($deliveryAddress); @@ -653,11 +654,11 @@ final class ShopproOrdersSyncService return true; } - $companyName = $this->nullableString((string) $this->readPath($payload, [ + $companyName = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company', 'firm_name', 'company_name', 'client_company', 'buyer_company', ])); - $taxNumber = $this->nullableString((string) $this->readPath($payload, [ + $taxNumber = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip', 'firm_nip', 'company_nip', 'tax_id', 'nip', ])); @@ -675,50 +676,50 @@ final class ShopproOrdersSyncService ?string $customerEmail, ?string $customerPhone ): ?array { - $companyName = $this->nullableString((string) $this->readPath($payload, [ + $companyName = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company', 'firm_name', 'company_name', 'client_company', 'buyer_company', ])); - $companyTaxNumber = $this->nullableString((string) $this->readPath($payload, [ + $companyTaxNumber = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip', 'firm_nip', 'company_nip', 'tax_id', 'nip', ])); - $invoiceFirstName = $this->nullableString((string) $this->readPath($payload, [ + $invoiceFirstName = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.first_name', 'invoice.firstname', 'billing_address.first_name', 'billing_address.firstname', 'buyer.first_name', 'customer.first_name', 'client_name', ])); - $invoiceLastName = $this->nullableString((string) $this->readPath($payload, [ + $invoiceLastName = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.last_name', 'invoice.lastname', 'billing_address.last_name', 'billing_address.lastname', 'buyer.last_name', 'customer.last_name', 'client_surname', ])); $invoiceName = $companyName ?? $this->composeName($invoiceFirstName, $invoiceLastName, $customerName ?? 'Faktura'); - $streetName = $this->nullableString((string) $this->readPath($payload, [ + $streetName = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.address.street', 'invoice.street', 'billing_address.street', 'billing.street', 'firm_street', 'company_street', ])); - $streetNumber = $this->nullableString((string) $this->readPath($payload, [ + $streetNumber = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.address.street_number', 'invoice.street_number', 'invoice.house_number', 'billing_address.street_number', 'billing_address.house_number', 'billing.street_number', 'house_number', 'street_number', ])); - $city = $this->nullableString((string) $this->readPath($payload, [ + $city = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.address.city', 'invoice.city', 'billing_address.city', 'billing.city', 'firm_city', 'company_city', ])); - $zipCode = $this->nullableString((string) $this->readPath($payload, [ + $zipCode = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.address.zip', 'invoice.address.postcode', 'invoice.zip', 'invoice.postcode', 'billing_address.zip', 'billing_address.postcode', 'billing.zip', 'billing.postcode', 'firm_postal_code', 'company_postal_code', ])); - $country = $this->nullableString((string) $this->readPath($payload, [ + $country = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.address.country', 'invoice.country', 'billing_address.country', 'billing.country', 'firm_country', 'company_country', ])); - $email = $this->nullableString((string) $this->readPath($payload, [ + $email = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.email', 'billing_address.email', 'billing.email', 'client_email', ])) ?? $customerEmail; - $phone = $this->nullableString((string) $this->readPath($payload, [ + $phone = StringHelper::nullableString((string) $this->readPath($payload, [ 'invoice.phone', 'billing_address.phone', 'billing.phone', 'client_phone', ])) ?? $customerPhone; @@ -814,31 +815,31 @@ final class ShopproOrdersSyncService $productId = (int) $this->readPath($row, ['product_id']); $parentProductId = (int) $this->readPath($row, ['parent_product_id']); - $mediaUrl = $this->nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url'])); + $mediaUrl = StringHelper::nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url'])); if ($mediaUrl === null && $productId > 0 && isset($productImagesById[$productId])) { - $mediaUrl = $this->nullableString((string) $productImagesById[$productId]); + $mediaUrl = StringHelper::nullableString((string) $productImagesById[$productId]); } if ($mediaUrl === null && $parentProductId > 0 && isset($productImagesById[$parentProductId])) { - $mediaUrl = $this->nullableString((string) $productImagesById[$parentProductId]); + $mediaUrl = StringHelper::nullableString((string) $productImagesById[$parentProductId]); } $result[] = [ - 'source_item_id' => $this->nullableString((string) $this->readPath($row, ['id', 'item_id'])), - 'external_item_id' => $this->nullableString((string) $this->readPath($row, ['id', 'item_id'])), - 'ean' => $this->nullableString((string) $this->readPath($row, ['ean'])), - 'sku' => $this->nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])), + 'source_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])), + 'external_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])), + 'ean' => StringHelper::nullableString((string) $this->readPath($row, ['ean'])), + 'sku' => StringHelper::nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])), 'original_name' => $name, - 'original_code' => $this->nullableString((string) $this->readPath($row, ['code', 'symbol'])), + 'original_code' => StringHelper::nullableString((string) $this->readPath($row, ['code', 'symbol'])), 'original_price_with_tax' => $this->toFloatOrNull($this->readPath($row, ['price_gross', 'gross_price', 'price', 'price_brutto'])), 'original_price_without_tax' => $this->toFloatOrNull($this->readPath($row, ['price_net', 'net_price', 'price_netto'])), 'media_url' => $mediaUrl, 'quantity' => $this->toFloatOrDefault($this->readPath($row, ['quantity', 'qty']), 1.0), 'tax_rate' => $this->toFloatOrNull($this->readPath($row, ['vat', 'tax_rate'])), 'item_status' => null, - 'unit' => $this->nullableString((string) $this->readPath($row, ['unit'])), + 'unit' => StringHelper::nullableString((string) $this->readPath($row, ['unit'])), 'item_type' => 'product', - 'source_product_id' => $this->nullableString((string) ($productId > 0 ? $productId : $parentProductId)), - 'source_product_set_id' => $this->nullableString((string) ($parentProductId > 0 ? $parentProductId : '')), + 'source_product_id' => StringHelper::nullableString((string) ($productId > 0 ? $productId : $parentProductId)), + 'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')), 'sort_order' => $sort++, 'payload_json' => $row, ]; @@ -853,7 +854,7 @@ final class ShopproOrdersSyncService */ private function mapPayments(array $payload, string $currency, ?float $totalPaid): array { - $paymentMethod = $this->nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method'])); + $paymentMethod = StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method'])); if ($paymentMethod === null && $totalPaid === null) { return []; } @@ -862,10 +863,10 @@ final class ShopproOrdersSyncService 'source_payment_id' => null, 'external_payment_id' => null, 'payment_type_id' => $paymentMethod ?? 'unknown', - 'payment_date' => $this->nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])), + 'payment_date' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])), 'amount' => $totalPaid, 'currency' => $currency, - 'comment' => $this->nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])), + 'comment' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])), 'payload_json' => null, ]]; } @@ -876,7 +877,7 @@ final class ShopproOrdersSyncService */ private function mapShipments(array $payload): array { - $tracking = $this->nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number'])); + $tracking = StringHelper::nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number'])); if ($tracking === null) { return []; } @@ -888,7 +889,7 @@ final class ShopproOrdersSyncService 'carrier_provider_id' => $this->sanitizePlainText((string) ($this->readPath($payload, [ 'delivery_method', 'shipping.method', 'transport', 'transport_description', ]) ?? 'unknown')), - 'posted_at' => $this->nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])), + 'posted_at' => StringHelper::nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])), 'media_uuid' => null, 'payload_json' => null, ]]; @@ -900,7 +901,7 @@ final class ShopproOrdersSyncService */ private function mapNotes(array $payload): array { - $comment = $this->nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment'])); + $comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment'])); if ($comment === null) { return []; } @@ -919,25 +920,6 @@ final class ShopproOrdersSyncService return trim((string) $value); } - private function normalizeDateTime(string $value): ?string - { - $trimmed = trim($value); - if ($trimmed === '') { - return null; - } - try { - return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s'); - } catch (Throwable) { - return null; - } - } - - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } - private function normalizePaidFlag(mixed $value): bool { if ($value === true) { diff --git a/src/Modules/Settings/ShopproPaymentStatusSyncService.php b/src/Modules/Settings/ShopproPaymentStatusSyncService.php index b74ca9a..98510c3 100644 --- a/src/Modules/Settings/ShopproPaymentStatusSyncService.php +++ b/src/Modules/Settings/ShopproPaymentStatusSyncService.php @@ -3,8 +3,8 @@ declare(strict_types=1); namespace App\Modules\Settings; +use App\Core\Support\StringHelper; use App\Modules\Orders\OrdersRepository; -use DateTimeImmutable; use PDO; use Throwable; @@ -219,14 +219,14 @@ final class ShopproPaymentStatusSyncService : 0.0; $existingPaymentStatus = isset($order['payment_status']) ? (int) $order['payment_status'] : null; $existingTotalPaid = $order['total_paid'] !== null ? (float) $order['total_paid'] : null; - $paymentMethod = $this->nullableString((string) ($payload['payment_method'] ?? $order['external_payment_type_id'] ?? '')); - $paymentDate = $this->normalizeDateTime((string) ($payload['payment_date'] ?? '')); - $sourceUpdatedAt = $this->normalizeDateTime((string) ($payload['updated_at'] ?? $payload['date_updated'] ?? '')); + $paymentMethod = StringHelper::nullableString((string) ($payload['payment_method'] ?? $order['external_payment_type_id'] ?? '')); + $paymentDate = StringHelper::normalizeDateTime((string) ($payload['payment_date'] ?? '')); + $sourceUpdatedAt = StringHelper::normalizeDateTime((string) ($payload['updated_at'] ?? $payload['date_updated'] ?? '')); if ( $existingPaymentStatus === $newPaymentStatus && $this->floatsEqual($existingTotalPaid, $newTotalPaid) - && $paymentMethod === $this->nullableString((string) ($order['external_payment_type_id'] ?? '')) + && $paymentMethod === StringHelper::nullableString((string) ($order['external_payment_type_id'] ?? '')) ) { return false; } @@ -370,26 +370,6 @@ final class ShopproPaymentStatusSyncService return $fallbackGross; } - private function normalizeDateTime(string $value): ?string - { - $trimmed = trim($value); - if ($trimmed === '') { - return null; - } - - try { - return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s'); - } catch (Throwable) { - return null; - } - } - - private function nullableString(string $value): ?string - { - $trimmed = trim($value); - return $trimmed === '' ? null : $trimmed; - } - private function floatsEqual(?float $left, ?float $right): bool { if ($left === null && $right === null) {