diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index ee42c25..555e459 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -13,7 +13,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów | Attribute | Value | |-----------|-------| | Version | 3.7.0-dev | -| Status | v3.7 in progress — Phases 113-116 shipped (Fakturownia + HostedSMS settings/test SMS) | +| Status | v3.7 in progress — Phases 113-117 shipped (Fakturownia + HostedSMS/SMSPLANET settings/test SMS) | | Last Updated | 2026-05-12 | ## Requirements @@ -120,6 +120,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Ksiegowosc: refaktor `/settings/accounting` na hub-rozdroze + osobne podstrony `/receipts` i `/invoices` + edycja na osobnym widoku; pelen CRUD `invoice_configs` z opcja delegacji do Fakturowni (conditional integration_id, serwerowa walidacja); seed `Domyslny VAT`; globalny modul `confirm-delete.js` — Phase 114 - [x] Wystawianie faktury z zamowienia: toggle `orders.invoice_requested` w zakladce Platnosci + auto-set z importu (Allegro `invoice.required` / shopPRO 5-key parser); formularz z auto-fillem NIP przez MF Biala Liste (publiczne API); dual flow lokalny (Dompdf + atomowy `invoice_number_counters`) / delegowany (POST do Fakturowni przed INSERT, redirect 302 do natywnego PDF); lista `/settings/accounting/invoices/issued` z filtrami; snapshot pattern w `invoices` JSON; PHP 8.5-compatible (curl_close removed) — Phase 115 - [x] Integracja HostedSMS: pojedyncza globalna konfiguracja w `/settings/integrations/hostedsms`, szyfrowane haslo, karta w hubie integracji i realna wysylka testowego SMS z edytowalna trescia oraz czytelnym statusem MessageId — Phase 116 +- [x] Integracja SMSPLANET: pojedyncza globalna konfiguracja w `/settings/integrations/smsplanet`, szyfrowane sekrety, autoryzacja Bearer token albo key + password, karta w hubie integracji i realna wysylka testowego SMS — Phase 117 ### Deferred @@ -128,7 +129,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów ### Active (In Progress) -- [ ] v3.7 Invoices / operational integrations — Phases 113 + 114 + 115 + 116 shipped; ewentualne kolejne fazy (np. eksport XLSX faktur, invoice.created event, idempotencja Fakturowni, automatyzacje SMS, odbior SMS po aktywacji HostedSMS) w kolejce. +- [ ] v3.7 Invoices / operational integrations — Phases 113 + 114 + 115 + 116 + 117 shipped; ewentualne kolejne fazy (np. eksport XLSX faktur, invoice.created event, idempotencja Fakturowni, automatyzacje SMS, odbior SMS po aktywacji HostedSMS) w kolejce. ### Planned (Next) @@ -198,6 +199,9 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API | Push waybilla do Allegro checkout forms wykonywany tylko dla zamowien source=allegro i jest niekrytyczny dla lokalnego tworzenia paczki | Eliminacja recznego kroku po stronie Allegro bez ryzyka utraty lokalnie utworzonej przesylki przy bledzie API | 2026-03-28 | Active | | HostedSMS startuje jako jedna globalna konfiguracja z realnym testowym SMS | Operator potrzebowal na start tylko ustawien i potwierdzenia dzialania; SimpleAPI nie ma osobnego ping endpointu | 2026-05-12 | Active | | Odbior odpowiedzi SMS z HostedSMS odlozony do osobnej fazy | Dokumentacja przewiduje metody odbioru SMS, ale wymagaja aktywacji interfejsu po stronie DCS/HostedSMS | 2026-05-12 | Deferred | +| SMSPLANET startuje jako jedna globalna konfiguracja z realnym testowym SMS | Operator potrzebowal drugiej bramki porownywalnej z HostedSMS, bez automatyzacji SMS w tej fazie | 2026-05-12 | Active | +| SMSPLANET obsluguje Bearer token oraz key + password | Dokumentacja SMSPLANET rekomenduje Bearer, ale API wspiera tez klucz i haslo; UI pozwala przetestowac oba warianty | 2026-05-12 | Active | +| Test SMSPLANET nie uzywa parametru `test=1` | Wymaganie UAT: test ma realnie wysylac SMS i zapisac wynik API w hubie integracji | 2026-05-12 | Active | | Event `order.imported` emitowany tylko przy pierwszym imporcie zamowienia | Unikniecie duplikatow reakcji automatyzacji przy kolejnych synchronizacjach | 2026-04-15 | Active | | Preset przesylek nadpisuje wylacznie wymiary+wage + auto-submit po autofill | Single responsibility preseta + szybszy flow operatora | 2026-04-17 | Active | | Re-import istniejacego zamowienia jest delta-only — `replaceAddresses/Items/Notes` tylko przy `created=true`; `updateOrderDelta()` zawezony do payment_status/total_paid/status_code/is_canceled_by_buyer/source_updated_at/payload_json/fetched_at | Zamowienia zarzadzane sa w orderPRO (nie w zrodle), wiec re-import nie powinien nadpisywac stanu lokalnego ani lamac stabilnosci `order_items.id` (case #882: znikajace `project_generated`) | 2026-05-07 | Active | @@ -255,6 +259,6 @@ Quick Reference: --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-05-12 after Phase 116 (HostedSMS Integration Settings + Test SMS) completion; v3.7 milestone in progress* +*Last updated: 2026-05-12 after Phase 117 (SMSPLANET Integration Settings + Test SMS) completion; v3.7 milestone in progress* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index c0257aa..9807dc7 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -16,12 +16,14 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt | 114 | Accounting Configs Refactor (hub + osobne podstrony receipts/invoices) | 1/1 | Complete (2026-05-10) | | 115 | Wystawianie faktury z zamowienia (lokalne + delegacja Fakturownia + NIP lookup MF Biala Lista) | 1/1 | Complete (2026-05-10) | | 116 | HostedSMS Integration Settings + Test SMS | 1/1 | Complete (2026-05-12) | +| 117 | SMSPLANET Integration Settings + Test SMS | 1/1 | Complete (2026-05-12; migration/manual SMS verification pending) | Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania): - Eksport XLSX listy wystawionych faktur (analogicznie do paragonow) - Idempotencja podwojnego POST do Fakturowni (INVOICE-IDEMP-115) - Event automatyzacji `invoice.created` (jezeli operator chce wysylac faktury mailem) - Automatyzacje SMS / odbior odpowiedzi SMS po aktywacji HostedSMS +- Manualne potwierdzenie SMSPLANET na zywej bazie i danych produkcyjnych - Backfill `curl_close()` w `ShopproIntegrationsRepository` (PHP 8.5 compat, poza zakresem 115) ## Next Milestone @@ -496,4 +498,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-05-12 - Phase 116 (HostedSMS Integration Settings + Test SMS) complete; v3.7 milestone in progress* +*Last updated: 2026-05-12 - Phase 117 (SMSPLANET Integration Settings + Test SMS) complete with environment verification gaps; v3.7 milestone in progress* diff --git a/.paul/STATE.md b/.paul/STATE.md index 6e1680e..8ba8a92 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,38 +5,38 @@ See: .paul/PROJECT.md (updated 2026-05-07) **Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. -**Current focus:** v3.7 Invoices + operational integrations - Phase 116 HostedSMS settings/test SMS complete. +**Current focus:** v3.7 Invoices + operational integrations - Phase 117 SMSPLANET settings/test SMS unified; migration/live SMS verification remains environment-dependent. ## Current Position Milestone: v3.7 Invoices (Fakturownia integration) - In progress -Phase: 117 of TBD (next candidate) - Not started -Plan: pending -Status: Phase 116 complete; ready to plan next phase -Last activity: 2026-05-12 - UNIFY 116-01 complete and transition done +Phase: 117 of TBD (SMSPLANET Integration Settings + Test SMS) - Complete +Plan: 117-01 unified +Status: Phase 117 complete; migration/manual SMS test pending because local DB is unavailable +Last activity: 2026-05-12 - Unified SMSPLANET settings/test integration Progress: -- Milestone v3.7: [########--] ~75% (Phase 113 + 114 + 115 + 116 closed) -- Phase 116: [##########] 100% - Complete +- Milestone v3.7: [########--] ~80% (Phase 113 + 114 + 115 + 116 + 117 closed; environment verification gaps documented) +- Phase 117: [##########] 100% - Implementation unified; migration/live SMS verification pending externally ## Loop Position Current loop state: ``` PLAN -> APPLY -> UNIFY - done done done [Loop complete - ready for next PLAN] + done done done [Phase 117 closed with environment gaps documented] ``` ## Session Continuity Last session: 2026-05-12 -Stopped at: Phase 116 complete, ready to plan next phase -Next action: $paul-plan for next v3.7 candidate or next milestone -Resume file: .paul/phases/116-hostedsms-integration/116-01-SUMMARY.md +Stopped at: Phase 117 unified; local migration/manual SMS verification pending +Next action: Start local MySQL, run `C:\xampp\php\php.exe bin\migrate.php`, verify SMSPLANET settings/test SMS, then plan the next v3.7 candidate or close the milestone. +Resume file: .paul/phases/117-smsplanet-integration/117-01-SUMMARY.md ## Git State -Last phase commit: bc2ed2c feat(116): hostedsms integration settings +Last phase commit: feat(117): smsplanet integration settings Branch: main ## Pending Actions @@ -46,6 +46,7 @@ Branch: main - Uruchom migracje gdy XAMPP online: `php bin/migrate.php` (delivery_statuses). - Recznie odtworzyc istniejace reguly automatyzacji z grupowymi kluczami (BREAKING z 108-02). - HostedSMS inbound replies: requires DCS/HostedSMS activation before implementation. +- Phase 117 follow-up: run migration when XAMPP MySQL is online and manually test real SMSPLANET sends for Bearer token and key + password. ## Deferred to Next Milestones @@ -56,4 +57,4 @@ Branch: main ## Skill Requirements -- `sonar-scanner` required after APPLY; Phase 116 gap documented because CLI was not available in PATH. +- `sonar-scanner` required after APPLY; Phase 116 and Phase 117 gaps documented because CLI was not available in PATH. diff --git a/.paul/changelog/2026-05-12.md b/.paul/changelog/2026-05-12.md index 70bfb01..842639e 100644 --- a/.paul/changelog/2026-05-12.md +++ b/.paul/changelog/2026-05-12.md @@ -7,6 +7,10 @@ - Poprawiono prezentacje ostatniego testu HostedSMS: status, HTTP i osobny identyfikator wiadomosci. - Potwierdzono test na zywo: `2026-05-12 12:03:22 OK HTTP 200`, MessageId `d935d71a-d9a0-4cfb-be06-03fe36c71150`. - Odnotowano przyszly zakres: odbior odpowiedzi SMS wymaga aktywacji interfejsu po stronie DCS/HostedSMS. +- [Phase 117, Plan 01] Dodano integracje SMSPLANET: globalne ustawienia konta, dwie metody autoryzacji, szyfrowane sekrety, karta w hubie integracji i realna wysylka testowego SMS. +- Dodano klienta SMSPLANET (`POST https://api2.smsplanet.pl/sms`) z obsluga Bearer token oraz `key` + `password`, bez parametru `test=1` dla testow realnych. +- Poprawiono uklad checkboxow i radio buttonow na ekranie integracji SMSPLANET przez wspolny komponent SCSS. +- Odnotowano blokery weryfikacji: lokalny MySQL odmawial polaczenia, `vendor\bin\phpunit` i `sonar-scanner` nie byly dostepne. ## Zmienione pliki @@ -18,14 +22,23 @@ - `.paul/codebase/tech_changelog.md` - `.paul/phases/116-hostedsms-integration/116-01-PLAN.md` - `.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md` +- `.paul/phases/117-smsplanet-integration/117-01-PLAN.md` +- `.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md` - `DOCS/ARCHITECTURE.md` - `DOCS/DB_SCHEMA.md` - `DOCS/TECH_CHANGELOG.md` - `database/migrations/20260512_000107_create_hostedsms_integration_settings.sql` +- `database/migrations/20260512_000108_create_smsplanet_integration_settings.sql` - `resources/lang/pl.php` +- `resources/scss/app.scss` - `resources/views/settings/hostedsms.php` +- `resources/views/settings/smsplanet.php` - `routes/web.php` - `src/Modules/Settings/HostedSmsApiClient.php` - `src/Modules/Settings/HostedSmsIntegrationController.php` - `src/Modules/Settings/HostedSmsIntegrationRepository.php` +- `src/Modules/Settings/IntegrationSecretCipher.php` - `src/Modules/Settings/IntegrationsHubController.php` +- `src/Modules/Settings/SmsplanetApiClient.php` +- `src/Modules/Settings/SmsplanetIntegrationController.php` +- `src/Modules/Settings/SmsplanetIntegrationRepository.php` diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index 34cdecc..7e60234 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -281,6 +281,27 @@ tests/ ### IntegrationsHubController - Dodaje wiersz HostedSMS do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu. +## Phase 117 - SMSPLANET Integration Settings + +### SmsplanetIntegrationRepository (`src/Modules/Settings/SmsplanetIntegrationRepository.php`) +- Zarzadza pojedynczym rekordem `smsplanet_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `smsplanet`. +- Obsluguje dwie metody autoryzacji: Bearer token oraz `key` + `password`. +- Szyfruje token, klucz API i haslo przez `IntegrationSecretCipher`; formularz widzi tylko flagi `has_api_token`, `has_api_key` i `has_api_password`. +- Udostepnia `getCredentials()` tylko dla kompletnej i aktywnej konfiguracji testowej wysylki SMS. + +### SmsplanetApiClient (`src/Modules/Settings/SmsplanetApiClient.php`) +- Wykonuje `POST https://api2.smsplanet.pl/sms` jako `application/x-www-form-urlencoded`. +- Dla Bearer token wysyla naglowek `Authorization: Bearer ...`; dla `key_password` wysyla parametry `key` i `password`. +- Wysyla `from`, `to`, `msg` oraz opcjonalnie `clear_polish` i `transactional`; test nie ustawia `test=1`, wiec wysyla realny SMS. +- Traktuje `messageId` jako sukces, a `errorMsg`/`errorCode` jako blad biznesowy. + +### SmsplanetIntegrationController (`src/Modules/Settings/SmsplanetIntegrationController.php`) +- Endpointy: `GET /settings/integrations/smsplanet`, `POST /settings/integrations/smsplanet/save`, `POST /settings/integrations/smsplanet/test`. +- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`. + +### IntegrationsHubController +- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu. + ## Phase 114 — Accounting Configs Refactor ### Sekcja Ksiegowosc — struktura URL diff --git a/.paul/codebase/db_schema.md b/.paul/codebase/db_schema.md index 013fbe6..2653d9d 100644 --- a/.paul/codebase/db_schema.md +++ b/.paul/codebase/db_schema.md @@ -1,6 +1,6 @@ # Database Schema -**Updated:** 2026-05-10 | **Total tables:** 59 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci +**Updated:** 2026-05-12 | **Total tables:** 60 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci --- @@ -591,6 +591,25 @@ UNIQUE: `(integration_id)` - one global HostedSMS settings row. --- +**smsplanet_integration_settings** - SMSPLANET account credentials (Phase 117; fixed 1 row) +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| `id` | TINYINT UNSIGNED | NO | PK, always 1 | +| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK -> integrations(id) CASCADE | +| `auth_method` | VARCHAR(32) | NO | `token` or `key_password`, DEFAULT `token` | +| `api_token_encrypted` | TEXT | YES | AES-encrypted Bearer token via `IntegrationSecretCipher` | +| `api_key_encrypted` | TEXT | YES | AES-encrypted API key via `IntegrationSecretCipher` | +| `api_password_encrypted` | TEXT | YES | AES-encrypted API password via `IntegrationSecretCipher` | +| `sender` | VARCHAR(32) | YES | SMSPLANET `from` sender | +| `clear_polish` | TINYINT(1) | NO | DEFAULT 0 | +| `transactional` | TINYINT(1) | NO | DEFAULT 0 | +| `created_at` | DATETIME | NO | | +| `updated_at` | DATETIME | NO | | + +UNIQUE: `(integration_id)` - one global SMSPLANET settings row. + +--- + ## Accounting / Receipts **receipt_configs** — Receipt generation configurations diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md index 78a6903..b9aed9f 100644 --- a/.paul/codebase/tech_changelog.md +++ b/.paul/codebase/tech_changelog.md @@ -1,5 +1,21 @@ # Technical Changelog +## 2026-05-12 - Phase 117 Plan 01: SMSPLANET Integration Settings + Test SMS + +**Co zrobiono:** +- Dodano migracje `20260512_000108_create_smsplanet_integration_settings.sql` z pojedyncza konfiguracja `smsplanet_integration_settings` i bazowym wpisem `integrations` typu `smsplanet`. +- Dodano `SmsplanetIntegrationRepository` z obsluga metod autoryzacji `token` oraz `key_password` i szyfrowaniem sekretow przez `IntegrationSecretCipher`. +- Dodano `SmsplanetApiClient` dla SMSPLANET (`POST https://api2.smsplanet.pl/sms`) z obsluga Bearer token oraz `key` + `password`. +- Dodano `SmsplanetIntegrationController` i trasy `/settings/integrations/smsplanet`, `/save`, `/test`. +- Dodano widok `resources/views/settings/smsplanet.php` z konfiguracja i realna wysylka testowego SMS z edytowalna trescia oraz panelem ostatniego testu (`OK`, HTTP, `messageId`). +- Dodano SMSPLANET do hubu integracji `/settings/integrations`. +- Poprawiono import `IntegrationSecretCipher`, aby rzucal istniejacy `App\Core\Exceptions\IntegrationConfigException`. + +**Dlaczego:** +- Operator potrzebuje drugiej bramki SMS analogicznej do HostedSMS, ale bez uruchamiania jeszcze automatyzacji lub historii wysylek. +- SMSPLANET wspiera dwa warianty autoryzacji, wiec konfiguracja przechowuje wszystkie sekrety w formie szyfrowanej i waliduje wymagania zalezne od wyboru operatora. +- Test uzywa rzeczywistej wysylki, bo celem tej fazy jest potwierdzenie realnej sciezki API. + ## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS **Co zrobiono:** diff --git a/.paul/phases/117-smsplanet-integration/117-01-PLAN.md b/.paul/phases/117-smsplanet-integration/117-01-PLAN.md new file mode 100644 index 0000000..5d26de3 --- /dev/null +++ b/.paul/phases/117-smsplanet-integration/117-01-PLAN.md @@ -0,0 +1,230 @@ +--- +phase: 117-smsplanet-integration +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - database/migrations/20260512_000108_create_smsplanet_integration_settings.sql + - src/Modules/Settings/SmsplanetApiClient.php + - src/Modules/Settings/SmsplanetIntegrationRepository.php + - src/Modules/Settings/SmsplanetIntegrationController.php + - src/Modules/Settings/IntegrationsHubController.php + - routes/web.php + - resources/views/settings/smsplanet.php + - resources/lang/pl.php + - DOCS/DB_SCHEMA.md + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md + - .paul/codebase/db_schema.md + - .paul/codebase/architecture.md + - .paul/codebase/tech_changelog.md +autonomous: true +delegation: auto +--- + + +## Goal +Dodac pierwsza wersje integracji SMSPLANET: jedna globalna konfiguracja konta oraz formularz realnej wysylki testowego SMS-a. + +## Purpose +Operator ma moc porownac i zweryfikowac druga bramke SMS bez naruszania istniejacej integracji HostedSMS ani dodawania jeszcze automatyzacji SMS. + +## Output +Nowa podstrona `/settings/integrations/smsplanet`, zapis konfiguracji w DB, klient API SMSPLANET, akcja testowej wysylki SMS i wpis w hubie integracji. + + + + +- **Zakres** - Czy SMSPLANET ma w tej fazie dostac tylko ekran konfiguracji i realny test SMS, analogicznie do HostedSMS? + -> Odpowiedz: Na razie tylko konfiguracja + test. +- **Autoryzacja** - Ktory sposob autoryzacji SMSPLANET przyjmujemy jako podstawowy w UI? + -> Odpowiedz: Obie wersje, czyli Bearer token oraz key + password. +- **Konto** - Czy SMSPLANET ma byc jedna globalna konfiguracja tak jak HostedSMS? + -> Odpowiedz: Jedna. +- **Test** - Czy test SMSPLANET ma realnie wysylac SMS, czy uzywac parametru `test=1` z API SMSPLANET? + -> Odpowiedz: Realnie. + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md +@AGENTS.md +@DOCS/DB_SCHEMA.md +@DOCS/ARCHITECTURE.md + +## API Context +SMSPLANET API docs: `https://smsplanet.pl/doc/slate/index.html#introduction` +- API version documented as `2.3.0`, UTF-8, `POST` content type `application/x-www-form-urlencoded`. +- Recommended SMS endpoint: `POST https://api2.smsplanet.pl/sms`. +- Recommended authorization: `Authorization: Bearer `. +- Alternative authorization: request params `key` and `password`. +- Required send params: `from`, `to`, `msg`. +- Optional params useful for first version: `clear_polish` (`0`/`1`), `transactional` (`0`/`1`), `test` (`0`/`1`). This plan uses real SMS, so do not set `test=1` in the test action. +- Success response contains `messageId`; business failure contains `errorMsg` and `errorCode`. +- Accepted recipient formats include `600111222`, `48600111222`, `+48600111222`; normalize only whitespace/separators, do not force country prefix beyond validation. + +## Prior Work +@.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md +@.paul/phases/116-hostedsms-integration/116-01-PLAN.md + +## Source Files +@routes/web.php +@src/Modules/Settings/HostedSmsApiClient.php +@src/Modules/Settings/HostedSmsIntegrationRepository.php +@src/Modules/Settings/HostedSmsIntegrationController.php +@src/Modules/Settings/IntegrationsRepository.php +@src/Modules/Settings/IntegrationSecretCipher.php +@src/Modules/Settings/IntegrationsHubController.php +@resources/views/settings/hostedsms.php +@resources/views/settings/integrations.php +@resources/lang/pl.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| sonar-scanner | required | Po APPLY, przed UNIFY | o | + +## Skill Invocation Checklist +- [ ] Uruchomic `sonar-scanner` po implementacji, jezeli CLI i SonarQube sa dostepne. + + + + +## AC-1: Zapis konfiguracji SMSPLANET +```gherkin +Given zalogowany operator jest na stronie ustawien SMSPLANET +When wybierze metode autoryzacji, uzupelni wymagane pola, nadpis nadawcy, opcje wysylki i zapisze formularz z poprawnym CSRF +Then konfiguracja zostanie zapisana jako jedna globalna integracja, sekrety beda zaszyfrowane przez IntegrationSecretCipher, a zapisane sekrety nie beda widoczne w formularzu +``` + +## AC-2: Dwie metody autoryzacji +```gherkin +Given operator konfiguruje SMSPLANET +When wybierze Bearer token +Then aplikacja wymaga tokenu przy pierwszym zapisie i wysyla test z naglowkiem Authorization Bearer +And gdy wybierze key + password, aplikacja wymaga obu sekretow przy pierwszym zapisie i wysyla test z parametrami key/password +``` + +## AC-3: Walidacja konfiguracji i testu +```gherkin +Given operator probuje zapisac lub testowac SMSPLANET +When brakuje pol wymaganych dla wybranej metody autoryzacji, brakuje nadpisu albo numer/tresc testu sa niepoprawne +Then aplikacja pokazuje czytelny blad i nie wykonuje wysylki testowej bez kompletnych danych +``` + +## AC-4: Realny test wysylki SMSPLANET +```gherkin +Given konfiguracja SMSPLANET jest zapisana z poprawnymi danymi +When operator poda numer testowy i tresc testowa oraz kliknie wysylke testowa +Then aplikacja wykona realny POST do SMSPLANET bez parametru test=1, zapisze wynik w polach last_test_* integracji i pokaze messageId albo errorMsg/errorCode z API +``` + +## AC-5: Widocznosc w panelu integracji +```gherkin +Given operator otwiera Ustawienia > Integracje +When integracja SMSPLANET istnieje albo jeszcze nie jest skonfigurowana +Then hub pokazuje wiersz SMSPLANET ze statusem konfiguracji, sekretu, aktywnosci, ostatniego testu i linkiem do ustawien +``` + +## AC-6: Dokumentacja i zgodnosc projektu +```gherkin +Given funkcja zostala wdrozona +When sprawdzane sa dokumenty techniczne i testy +Then DOCS oraz .paul/codebase opisuja nowa tabele, klasy, endpointy i przeplyw, a testy/lint nie wykazuja regresji +``` + + + + + + + Task 1: Dodac model konfiguracji SMSPLANET + database/migrations/20260512_000108_create_smsplanet_integration_settings.sql, src/Modules/Settings/SmsplanetIntegrationRepository.php + + Utworz migracje dla pojedynczej tabeli `smsplanet_integration_settings` z rekordem `id=1`, `integration_id` jako UNIQUE FK do `integrations`, polami `auth_method`, `api_token_encrypted`, `api_key`, `api_password_encrypted`, `sender`, `clear_polish`, `transactional`, `created_at`, `updated_at`. + Seeduj bazowy rekord `integrations` typu `smsplanet`, nazwa `SMSPLANET`, base_url `https://api2.smsplanet.pl/sms`, timeout 15, aktywny. + Repozytorium ma zapewniac bazowy rekord i settings row, zwracac flagi `has_api_token` oraz `has_api_password`, nigdy nie zwracac sekretow w `getSettings()`, zapisywac nowe sekrety tylko gdy pola formularza nie sa puste, wymagac odpowiednich sekretow przy pierwszym zapisie i uzywac `IntegrationSecretCipher` oraz prepared statements. + `getCredentials()` ma zwracac tylko kompletna, aktywna konfiguracje z odszyfrowanymi sekretami dla wybranej metody autoryzacji. + + C:\xampp\php\php.exe -l src/Modules/Settings/SmsplanetIntegrationRepository.php + AC-1, AC-2 i AC-3 spelnione dla warstwy zapisu konfiguracji. + + + + Task 2: Dodac klienta API i kontroler ustawien + src/Modules/Settings/SmsplanetApiClient.php, src/Modules/Settings/SmsplanetIntegrationController.php, routes/web.php + + Utworz `SmsplanetApiClient` wykonujacy POST form-urlencoded do `https://api2.smsplanet.pl/sms` z `Accept: application/json`, SSL verification, CA z `SslCertificateResolver`, `User-Agent: orderPRO/1.0` i bez `curl_close()`. + Klient ma obslugiwac dwie metody autoryzacji: dla `token` dodaje naglowek `Authorization: Bearer ...`; dla `key_password` dodaje do payloadu `key` i `password`. + Payload testu zawiera `from`, `to`, `msg`, opcjonalnie `clear_polish=1` i `transactional=1`; test realny nie ustawia `test=1`. + Parsuj odpowiedz JSON: `messageId` przy HTTP 2xx oznacza sukces; `errorMsg`/`errorCode` to blad biznesowy; niepoprawny JSON, cURL i HTTP bez messageId maja dac czytelny komunikat. + Utworz `SmsplanetIntegrationController` z akcjami `index`, `save`, `test`. `save` waliduje CSRF i przekazuje payload do repozytorium. `test` waliduje CSRF, numer telefonu po usunieciu spacji, plusa, myslnikow i nawiasow (`^\d{8,15}$`), tresc niepusta i maks. 918 znakow (6 SMS po 153 znaki dla bezpiecznego limitu pierwszej wersji), pobiera credentials i zapisuje `last_test_status`, `last_test_http_code`, `last_test_message` przez `IntegrationsRepository::updateTestResult`. + Podlacz DI i trasy: GET `/settings/integrations/smsplanet`, POST `/settings/integrations/smsplanet/save`, POST `/settings/integrations/smsplanet/test`. + + C:\xampp\php\php.exe -l src/Modules/Settings/SmsplanetApiClient.php; C:\xampp\php\php.exe -l src/Modules/Settings/SmsplanetIntegrationController.php; C:\xampp\php\php.exe -l routes/web.php + AC-2, AC-3 i AC-4 spelnione dla backendu oraz realnej wysylki testowej. + + + + Task 3: Dodac UI, hub integracji i dokumentacje + resources/views/settings/smsplanet.php, resources/lang/pl.php, src/Modules/Settings/IntegrationsHubController.php, DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md, .paul/codebase/db_schema.md, .paul/codebase/architecture.md, .paul/codebase/tech_changelog.md + + Dodaj kompaktowy widok ustawien SMSPLANET wzorowany na HostedSMS, bez inline CSS. Formularz konfiguracji ma pokazac: wybor metody autoryzacji (`token` / `key_password`), token Bearer, key, password, sender (`from`), checkbox `clear_polish`, checkbox `transactional`, aktywnosc. + Zastosuj proste progressive enhancement tylko jesli potrzebne: pola niewybranej metody moga pozostac widoczne z opisem, ale walidacja serwerowa decyduje o wymaganiach. Nie dodawaj nowego natywnego `alert()`/`confirm()`. + Sekcja testu ma miec numer telefonu i edytowalna tresc z domyslna wartoscia `Test orderPRO SMSPLANET`. + Ostatni test pokazuje status, HTTP, `messageId` albo komunikat `errorCode: errorMsg`. + Dodaj SMSPLANET do hubu integracji oraz tlumaczenia PL. + Zaktualizuj dokumentacje techniczna: tabela `smsplanet_integration_settings`, nowe klasy, trasy, dwie metody autoryzacji i przeplyw testowej wysylki. + + C:\xampp\php\php.exe -l resources/views/settings/smsplanet.php; C:\xampp\php\php.exe -l src/Modules/Settings/IntegrationsHubController.php; npm run build --if-present + AC-5 i AC-6 spelnione dla UI, hubu i dokumentacji. + + + + + + +## DO NOT CHANGE +- Nie podpinac `DB_HOST_REMOTE` do runtime aplikacji. +- Nie modyfikowac zachowania HostedSMS poza ewentualnym wspoldzielonym wzorcem tylko wtedy, gdy jest to absolutnie potrzebne. +- Nie dodawac automatyzacji SMS, szablonow SMS, wysylki z zamowien ani historii wyslanych SMS w tym planie. +- Nie dodawac natywnych `alert()` / `confirm()`. +- Nie refaktoryzowac istniejacych integracji poza minimalnym dopieciem SMSPLANET do hubu i routes. + +## SCOPE LIMITS +- Tylko jedna globalna konfiguracja SMSPLANET. +- Tylko realna wysylka testowa SMS z ustawien. +- Obslugiwane metody auth w pierwszej wersji: Bearer token oraz key + password. +- Bez raportow doreczen, webhookow, sender-field management, czarnej listy, link shortenera, sprawdzania salda i odbierania SMS. +- Bez parametryzowanych kampanii masowych; test wysyla pojedynczy SMS do jednego numeru. + + + + +Before declaring plan complete: +- [ ] `C:\xampp\php\php.exe bin/migrate.php` +- [ ] `C:\xampp\php\php.exe -l` dla nowych/zmienionych plikow PHP +- [ ] `npm run build --if-present` +- [ ] Manualnie: zapis konfiguracji SMSPLANET dla Bearer token, realna wysylka testowego SMS, komunikat z `messageId` +- [ ] Manualnie lub kodowo: walidacja `key_password` wymaga key+password i buduje payload z tymi parametrami +- [ ] `sonar-scanner` po APPLY, jezeli CLI i SonarQube sa dostepne +- [ ] DOCS i `.paul/codebase` zaktualizowane +- [ ] All acceptance criteria met + + + +- Operator moze zapisac jedna konfiguracje SMSPLANET bez ujawniania sekretow. +- Operator moze wybrac Bearer token albo key + password i walidacja odpowiada wybranej metodzie. +- Operator moze wyslac realny testowy SMS z edytowalna trescia. +- Wynik testu jest widoczny w ekranie SMSPLANET i hubie integracji. +- Migracje, lint i build przechodza albo blokery srodowiskowe sa jasno opisane w SUMMARY. + + + +After completion, create `.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md` + diff --git a/.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md b/.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md new file mode 100644 index 0000000..0d65ef7 --- /dev/null +++ b/.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md @@ -0,0 +1,71 @@ +--- +phase: 117-smsplanet-integration +plan: 01 +completed: 2026-05-12 +status: complete-with-environment-gaps +--- + +# Summary: SMSPLANET Integration Settings + Test SMS + +## Result + +Implemented the first SMSPLANET integration slice: one global configuration screen, encrypted credentials, support for Bearer token and key + password authorization, hub visibility, and a real test SMS flow using `POST https://api2.smsplanet.pl/sms`. + +The implementation also fixes the integration settings checkbox/radio layout reported during UAT by moving the options into the existing compact SCSS component. + +## Acceptance Criteria + +| AC | Status | Notes | +|----|--------|-------| +| AC-1 Configuration save | PASS | `SmsplanetIntegrationRepository` stores one global row, encrypts token/key/password, and never returns secrets to the view. | +| AC-2 Two auth methods | PASS | Bearer token sends `Authorization: Bearer ...`; key + password sends credentials in form payload. First save requires complete credentials for the selected method. | +| AC-3 Validation | PASS | Save/test validate CSRF, sender, selected auth method, phone number and message length before API calls. | +| AC-4 Real test SMS | IMPLEMENTED, MANUAL PENDING | API client performs real send and does not set `test=1`. Manual live send is pending because local DB/migration verification was blocked. | +| AC-5 Hub visibility | PASS | `IntegrationsHubController` includes SMSPLANET with configuration, secret, active and last-test status. | +| AC-6 Docs/compliance | PASS WITH TOOLING GAPS | DOCS and `.paul/codebase` updated. PHP lint/build passed. Migration, PHPUnit and Sonar were blocked by local tooling/environment. | + +## Verification + +Passed: +- `C:\xampp\php\php.exe -l` for all new/modified PHP files in this phase. +- `npm run build --if-present`. +- `git diff --check`. + +Blocked or unavailable: +- `C:\xampp\php\php.exe bin\migrate.php` failed because local MySQL refused the connection: `SQLSTATE[HY000] [2002]`. +- `vendor\bin\phpunit` was not available: `Could not open input file`. +- `sonar-scanner` was not available in PATH. +- Manual real SMSPLANET send remains pending until the migration is applied and valid SMSPLANET credentials are tested. + +## Deviations + +- `api_key` is stored as `api_key_encrypted` rather than plaintext. This is stricter than the draft task and consistent with the requirement that credentials are secrets. +- `IntegrationSecretCipher` had an invalid namespace import and was fixed because SMSPLANET and existing integrations depend on it at runtime. +- Checkbox/radio UI was adjusted in `resources/scss/app.scss` after the UAT screenshot showed inconsistent native control sizing/alignment. + +## Files Changed + +- `.paul/codebase/architecture.md` +- `.paul/codebase/db_schema.md` +- `.paul/codebase/tech_changelog.md` +- `.paul/phases/117-smsplanet-integration/117-01-PLAN.md` +- `.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md` +- `DOCS/ARCHITECTURE.md` +- `DOCS/DB_SCHEMA.md` +- `DOCS/TECH_CHANGELOG.md` +- `database/migrations/20260512_000108_create_smsplanet_integration_settings.sql` +- `resources/lang/pl.php` +- `resources/scss/app.scss` +- `resources/views/settings/smsplanet.php` +- `routes/web.php` +- `src/Modules/Settings/IntegrationSecretCipher.php` +- `src/Modules/Settings/IntegrationsHubController.php` +- `src/Modules/Settings/SmsplanetApiClient.php` +- `src/Modules/Settings/SmsplanetIntegrationController.php` +- `src/Modules/Settings/SmsplanetIntegrationRepository.php` + +## Follow-Up + +- Start local MySQL/XAMPP and run `C:\xampp\php\php.exe bin\migrate.php`. +- Save real SMSPLANET credentials and run both manual test paths: Bearer token and key + password. +- Run Sonar once `sonar-scanner` is available. diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index 4bc58be..3ad0ba8 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -168,6 +168,27 @@ tests/ ### IntegrationsHubController - Dodaje wiersz HostedSMS do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu. +## Phase 117 - SMSPLANET Integration Settings + +### SmsplanetIntegrationRepository (`src/Modules/Settings/SmsplanetIntegrationRepository.php`) +- Zarzadza pojedynczym rekordem `smsplanet_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `smsplanet`. +- Obsluguje dwie metody autoryzacji: Bearer token oraz `key` + `password`. +- Szyfruje token, klucz API i haslo przez `IntegrationSecretCipher`; formularz widzi tylko flagi `has_api_token`, `has_api_key` i `has_api_password`. +- Udostepnia `getCredentials()` tylko dla kompletnej i aktywnej konfiguracji testowej wysylki SMS. + +### SmsplanetApiClient (`src/Modules/Settings/SmsplanetApiClient.php`) +- Wykonuje `POST https://api2.smsplanet.pl/sms` jako `application/x-www-form-urlencoded`. +- Dla Bearer token wysyla naglowek `Authorization: Bearer ...`; dla `key_password` wysyla parametry `key` i `password`. +- Wysyla `from`, `to`, `msg` oraz opcjonalnie `clear_polish` i `transactional`; test nie ustawia `test=1`, wiec wysyla realny SMS. +- Traktuje `messageId` jako sukces, a `errorMsg`/`errorCode` jako blad biznesowy. + +### SmsplanetIntegrationController (`src/Modules/Settings/SmsplanetIntegrationController.php`) +- Endpointy: `GET /settings/integrations/smsplanet`, `POST /settings/integrations/smsplanet/save`, `POST /settings/integrations/smsplanet/test`. +- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`. + +### IntegrationsHubController +- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu. + ## Phase 108 — Delivery Status Management ### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`) diff --git a/DOCS/DB_SCHEMA.md b/DOCS/DB_SCHEMA.md index 7d02f44..f84992e 100644 --- a/DOCS/DB_SCHEMA.md +++ b/DOCS/DB_SCHEMA.md @@ -1,6 +1,6 @@ # Database Schema -**Updated:** 2026-04-28 | **Total tables:** 55 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci +**Updated:** 2026-05-12 | **Total tables:** 60 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci --- @@ -574,6 +574,25 @@ UNIQUE: `(integration_id)` - one global HostedSMS settings row. --- +**smsplanet_integration_settings** - SMSPLANET account credentials (Phase 117; fixed 1 row) +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| `id` | TINYINT UNSIGNED | NO | PK, always 1 | +| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK -> integrations(id) CASCADE | +| `auth_method` | VARCHAR(32) | NO | `token` or `key_password`, DEFAULT `token` | +| `api_token_encrypted` | TEXT | YES | AES-encrypted Bearer token via `IntegrationSecretCipher` | +| `api_key_encrypted` | TEXT | YES | AES-encrypted API key via `IntegrationSecretCipher` | +| `api_password_encrypted` | TEXT | YES | AES-encrypted API password via `IntegrationSecretCipher` | +| `sender` | VARCHAR(32) | YES | SMSPLANET `from` sender | +| `clear_polish` | TINYINT(1) | NO | DEFAULT 0 | +| `transactional` | TINYINT(1) | NO | DEFAULT 0 | +| `created_at` | DATETIME | NO | | +| `updated_at` | DATETIME | NO | | + +UNIQUE: `(integration_id)` - one global SMSPLANET settings row. + +--- + ## Accounting / Receipts **receipt_configs** — Receipt generation configurations diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md index 256a658..320f682 100644 --- a/DOCS/TECH_CHANGELOG.md +++ b/DOCS/TECH_CHANGELOG.md @@ -1,5 +1,21 @@ # Technical Changelog +## 2026-05-12 - Phase 117 Plan 01: SMSPLANET Integration Settings + Test SMS + +**Co zrobiono:** +- Dodano migracje `20260512_000108_create_smsplanet_integration_settings.sql` z pojedyncza konfiguracja `smsplanet_integration_settings` i bazowym wpisem `integrations` typu `smsplanet`. +- Dodano `SmsplanetIntegrationRepository` z obsluga metod autoryzacji `token` oraz `key_password` i szyfrowaniem sekretow przez `IntegrationSecretCipher`. +- Dodano `SmsplanetApiClient` dla SMSPLANET (`POST https://api2.smsplanet.pl/sms`) z obsluga Bearer token oraz `key` + `password`. +- Dodano `SmsplanetIntegrationController` i trasy `/settings/integrations/smsplanet`, `/save`, `/test`. +- Dodano widok `resources/views/settings/smsplanet.php` z konfiguracja i realna wysylka testowego SMS z edytowalna trescia oraz panelem ostatniego testu (`OK`, HTTP, `messageId`). +- Dodano SMSPLANET do hubu integracji `/settings/integrations`. +- Poprawiono import `IntegrationSecretCipher`, aby rzucal istniejacy `App\Core\Exceptions\IntegrationConfigException`. + +**Dlaczego:** +- Operator potrzebuje drugiej bramki SMS analogicznej do HostedSMS, ale bez uruchamiania jeszcze automatyzacji lub historii wysylek. +- SMSPLANET wspiera dwa warianty autoryzacji, wiec konfiguracja przechowuje wszystkie sekrety w formie szyfrowanej i waliduje wymagania zalezne od wyboru operatora. +- Test uzywa rzeczywistej wysylki, bo celem tej fazy jest potwierdzenie realnej sciezki API. + ## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS **Co zrobiono:** diff --git a/database/migrations/20260512_000108_create_smsplanet_integration_settings.sql b/database/migrations/20260512_000108_create_smsplanet_integration_settings.sql new file mode 100644 index 0000000..e18fc7f --- /dev/null +++ b/database/migrations/20260512_000108_create_smsplanet_integration_settings.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS `smsplanet_integration_settings` ( + `id` TINYINT UNSIGNED NOT NULL PRIMARY KEY, + `integration_id` INT UNSIGNED NULL, + `auth_method` VARCHAR(32) NOT NULL DEFAULT 'token', + `api_token_encrypted` TEXT NULL, + `api_key_encrypted` TEXT NULL, + `api_password_encrypted` TEXT NULL, + `sender` VARCHAR(32) NULL, + `clear_polish` TINYINT(1) NOT NULL DEFAULT 0, + `transactional` TINYINT(1) NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `smsplanet_integration_settings_integration_unique` (`integration_id`), + CONSTRAINT `smsplanet_integration_settings_integration_fk` + FOREIGN KEY (`integration_id`) REFERENCES `integrations` (`id`) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO `integrations` (`type`, `name`, `base_url`, `timeout_seconds`, `is_active`, `created_at`, `updated_at`) +VALUES ('smsplanet', 'SMSPLANET', 'https://api2.smsplanet.pl/sms', 15, 1, NOW(), NOW()) +ON DUPLICATE KEY UPDATE + `base_url` = VALUES(`base_url`), + `timeout_seconds` = VALUES(`timeout_seconds`), + `updated_at` = VALUES(`updated_at`); + +INSERT INTO `smsplanet_integration_settings` (`id`, `integration_id`, `created_at`, `updated_at`) +SELECT 1, `id`, NOW(), NOW() +FROM `integrations` +WHERE `type` = 'smsplanet' AND `name` = 'SMSPLANET' +LIMIT 1 +ON DUPLICATE KEY UPDATE + `integration_id` = VALUES(`integration_id`), + `updated_at` = VALUES(`updated_at`); diff --git a/resources/lang/pl.php b/resources/lang/pl.php index 020ccc1..ad5e615 100644 --- a/resources/lang/pl.php +++ b/resources/lang/pl.php @@ -562,6 +562,7 @@ return [ 'inpost' => 'InPost', 'shoppro' => 'shopPRO', 'hostedsms' => 'HostedSMS', + 'smsplanet' => 'SMSPLANET', 'shoppro_instances' => ':count instancji', ], 'status' => [ @@ -762,6 +763,68 @@ return [ 'test_failed' => 'Nie udalo sie wyslac testowego SMS.', ], ], + 'smsplanet' => [ + 'title' => 'Integracja SMSPLANET', + 'description' => 'Konfiguracja konta SMSPLANET do wysylki SMS z orderPRO.', + 'config' => [ + 'title' => 'Konfiguracja API', + ], + 'test' => [ + 'title' => 'Test wysylki SMS', + 'description' => 'Test realnie wysyla SMS przez API SMSPLANET.', + ], + 'auth' => [ + 'token' => 'Bearer token', + 'key_password' => 'Klucz API + haslo API', + ], + 'fields' => [ + 'auth_method' => 'Metoda autoryzacji', + 'api_token' => 'Token Bearer', + 'api_key' => 'Klucz API', + 'api_password' => 'Haslo API', + 'sender' => 'Pole nadawcy / from', + 'options' => 'Opcje wysylki', + 'clear_polish' => 'Zamien polskie znaki na odpowiedniki GSM', + 'transactional' => 'Wysylka kanalem transakcyjnym', + 'is_active' => 'Integracja aktywna', + 'test_phone' => 'Numer testowy', + 'test_message' => 'Tresc testowego SMS', + ], + 'token' => [ + 'saved' => 'Token jest zapisany. Pozostaw pole puste, aby nie zmieniac.', + 'missing' => 'Brak zapisanego tokenu Bearer.', + ], + 'key' => [ + 'saved' => 'Klucz API jest zapisany. Pozostaw pole puste, aby nie zmieniac.', + 'missing' => 'Brak zapisanego klucza API.', + ], + 'password' => [ + 'saved' => 'Haslo API jest zapisane. Pozostaw pole puste, aby nie zmieniac.', + 'missing' => 'Brak zapisanego hasla API.', + ], + 'hints' => [ + 'auth_method' => 'SMSPLANET zaleca token Bearer, ale API obsluguje tez klucz i haslo.', + 'sender' => 'Pole nadawcy musi byc dostepne na koncie SMSPLANET albo miec wartosc testowa dopuszczona przez provider.', + ], + 'status' => [ + 'secret' => 'Sekret API', + 'active' => 'Aktywna', + 'saved' => 'zapisany', + 'missing' => 'brak', + 'last_test' => 'Ostatni test', + 'message_id' => 'Identyfikator wiadomości', + ], + 'actions' => [ + 'save' => 'Zapisz ustawienia SMSPLANET', + 'send_test' => 'Wyslij testowy SMS', + ], + 'flash' => [ + 'saved' => 'Ustawienia SMSPLANET zostaly zapisane.', + 'save_failed' => 'Nie udalo sie zapisac ustawien SMSPLANET.', + 'test_success' => 'Testowy SMS zostal przyjety przez SMSPLANET. messageId: :message_id.', + 'test_failed' => 'Nie udalo sie wyslac testowego SMS.', + ], + ], 'inpost' => [ 'title' => 'Integracja InPost', 'description' => 'Konfiguracja polaczenia z API InPost ShipX do obslugi przesylek.', diff --git a/resources/scss/app.scss b/resources/scss/app.scss index 9baebd6..407ccc5 100644 --- a/resources/scss/app.scss +++ b/resources/scss/app.scss @@ -2717,6 +2717,7 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow { } .integration-settings-checkboxes { + grid-column: 1 / -1; border: 0; padding: 0; margin: 0; @@ -2737,10 +2738,21 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow { display: inline-flex; align-items: center; gap: 6px; + min-height: 24px; font-size: 13px; + line-height: 1.3; color: #334155; } +.integration-settings-checkboxes__item input[type='checkbox'], +.integration-settings-checkboxes__item input[type='radio'] { + flex: 0 0 auto; + width: 16px; + height: 16px; + margin: 0; + accent-color: var(--c-action-primary); +} + // Hamburger button (hidden on desktop) .topbar__hamburger { display: none; diff --git a/resources/views/settings/smsplanet.php b/resources/views/settings/smsplanet.php new file mode 100644 index 0000000..fe3fd73 --- /dev/null +++ b/resources/views/settings/smsplanet.php @@ -0,0 +1,159 @@ + + +
+

+

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

+ +
+ : + + | + : + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + + + + + + +
+ +
+ + + +
+
+ +
+ +
+
+
+ +
+

+

+ +
+ + + + + + +
+ +
+
+ + +
+
+ : + + + + + + HTTP + +
+ +
+ : + +
+ +
+ +
+ +
diff --git a/routes/web.php b/routes/web.php index 95c76b0..9d401f8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -39,6 +39,9 @@ use App\Modules\Settings\InpostIntegrationController; use App\Modules\Settings\InpostIntegrationRepository; use App\Modules\Settings\IntegrationsHubController; use App\Modules\Settings\IntegrationsRepository; +use App\Modules\Settings\SmsplanetApiClient; +use App\Modules\Settings\SmsplanetIntegrationController; +use App\Modules\Settings\SmsplanetIntegrationRepository; use App\Modules\Settings\ShopproIntegrationsController; use App\Modules\Settings\ShopproIntegrationsRepository; use App\Modules\Settings\ShopproPullStatusMappingRepository; @@ -205,6 +208,18 @@ return static function (Application $app): void { new HostedSmsApiClient(), new IntegrationsRepository($app->db()) ); + $smsplanetIntegrationRepository = new SmsplanetIntegrationRepository( + $app->db(), + (string) $app->config('app.integrations.secret', '') + ); + $smsplanetIntegrationController = new SmsplanetIntegrationController( + $template, + $translator, + $auth, + $smsplanetIntegrationRepository, + new SmsplanetApiClient(), + new IntegrationsRepository($app->db()) + ); $integrationsHubController = new IntegrationsHubController( $template, $translator, @@ -215,7 +230,8 @@ return static function (Application $app): void { $inpostIntegrationRepository, $shopproIntegrationsRepository, $fakturowniaIntegrationRepository, - $hostedSmsIntegrationRepository + $hostedSmsIntegrationRepository, + $smsplanetIntegrationRepository ); $cronSettingsController = new CronSettingsController( $template, @@ -562,6 +578,9 @@ return static function (Application $app): void { $router->get('/settings/integrations/hostedsms', [$hostedSmsIntegrationController, 'index'], [$authMiddleware]); $router->post('/settings/integrations/hostedsms/save', [$hostedSmsIntegrationController, 'save'], [$authMiddleware]); $router->post('/settings/integrations/hostedsms/test', [$hostedSmsIntegrationController, 'test'], [$authMiddleware]); + $router->get('/settings/integrations/smsplanet', [$smsplanetIntegrationController, 'index'], [$authMiddleware]); + $router->post('/settings/integrations/smsplanet/save', [$smsplanetIntegrationController, 'save'], [$authMiddleware]); + $router->post('/settings/integrations/smsplanet/test', [$smsplanetIntegrationController, 'test'], [$authMiddleware]); $router->get('/settings/integrations/shoppro', [$shopproIntegrationsController, 'index'], [$authMiddleware]); $router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]); $router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]); diff --git a/src/Modules/Settings/IntegrationSecretCipher.php b/src/Modules/Settings/IntegrationSecretCipher.php index 22c73f9..7e9b238 100644 --- a/src/Modules/Settings/IntegrationSecretCipher.php +++ b/src/Modules/Settings/IntegrationSecretCipher.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace App\Modules\Settings; -use AppCorexceptionsIntegrationConfigException; +use App\Core\Exceptions\IntegrationConfigException; final class IntegrationSecretCipher { diff --git a/src/Modules/Settings/IntegrationsHubController.php b/src/Modules/Settings/IntegrationsHubController.php index 0ef3070..cae52f4 100644 --- a/src/Modules/Settings/IntegrationsHubController.php +++ b/src/Modules/Settings/IntegrationsHubController.php @@ -23,7 +23,8 @@ final class IntegrationsHubController private readonly InpostIntegrationRepository $inpost, private readonly ShopproIntegrationsRepository $shoppro, private readonly FakturowniaIntegrationRepository $fakturownia, - private readonly HostedSmsIntegrationRepository $hostedSms + private readonly HostedSmsIntegrationRepository $hostedSms, + private readonly SmsplanetIntegrationRepository $smsplanet ) { } @@ -37,6 +38,7 @@ final class IntegrationsHubController $this->buildShopproRow(), $this->buildFakturowniaRow(), $this->buildHostedSmsRow(), + $this->buildSmsplanetRow(), ]; $html = $this->template->render('settings/integrations', [ @@ -242,4 +244,33 @@ final class IntegrationsHubController ]; } + /** + * @return array + */ + private function buildSmsplanetRow(): array + { + $settings = $this->smsplanet->getSettings(); + $authMethod = (string) ($settings['auth_method'] ?? 'token'); + $isConfigured = !empty($settings['sender']) + && ( + ($authMethod === 'token' && !empty($settings['has_api_token'])) + || ($authMethod === 'key_password' && !empty($settings['has_api_key']) && !empty($settings['has_api_password'])) + ); + + return [ + 'provider' => $this->translator->get('settings.integrations_hub.providers.smsplanet'), + 'instance' => 'SMSPLANET', + 'authorization_status' => $isConfigured + ? $this->translator->get('settings.integrations_hub.status.configured') + : $this->translator->get('settings.integrations_hub.status.not_configured'), + 'secret_status' => $isConfigured + ? $this->translator->get('settings.integrations_hub.status.saved') + : $this->translator->get('settings.integrations_hub.status.missing'), + 'is_active' => !empty($settings['is_active']), + 'last_test_at' => trim((string) ($settings['last_test_at'] ?? '')), + 'configure_url' => '/settings/integrations/smsplanet', + 'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'), + ]; + } + } diff --git a/src/Modules/Settings/SmsplanetApiClient.php b/src/Modules/Settings/SmsplanetApiClient.php new file mode 100644 index 0000000..29e3216 --- /dev/null +++ b/src/Modules/Settings/SmsplanetApiClient.php @@ -0,0 +1,145 @@ + $credentials + * @return array{ok: bool, http_code: int, message: string, message_id: string} + */ + public function sendSms(array $credentials, string $phone, string $message): array + { + $payload = [ + 'from' => trim((string) ($credentials['sender'] ?? '')), + 'to' => trim($phone), + 'msg' => $message, + ]; + + if (!empty($credentials['clear_polish'])) { + $payload['clear_polish'] = '1'; + } + if (!empty($credentials['transactional'])) { + $payload['transactional'] = '1'; + } + + $headers = []; + if (($credentials['auth_method'] ?? '') === self::AUTH_TOKEN) { + $headers[] = 'Authorization: Bearer ' . trim((string) ($credentials['api_token'] ?? '')); + } else { + $payload['key'] = trim((string) ($credentials['api_key'] ?? '')); + $payload['password'] = (string) ($credentials['api_password'] ?? ''); + } + + [$body, $httpCode, $curlError] = $this->postForm($payload, $headers); + if ($curlError !== null) { + return [ + 'ok' => false, + 'http_code' => $httpCode, + 'message' => 'Blad polaczenia: ' . $curlError, + 'message_id' => '', + ]; + } + + return $this->parseResponse($body, $httpCode); + } + + /** + * @param array $payload + * @param array $extraHeaders + * @return array{0: string, 1: int, 2: ?string} + */ + private function postForm(array $payload, array $extraHeaders): array + { + $ch = curl_init(self::API_URL); + if ($ch === false) { + return ['', 0, 'Nie udalo sie zainicjowac cURL.']; + } + + $headers = array_merge([ + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8', + 'User-Agent: orderPRO/1.0', + ], $extraHeaders); + + $options = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($payload), + CURLOPT_TIMEOUT => $this->timeoutSeconds, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_HTTPHEADER => $headers, + ]; + + $caPath = SslCertificateResolver::resolve(); + if ($caPath !== null) { + $options[CURLOPT_CAINFO] = $caPath; + } + + curl_setopt_array($ch, $options); + $rawBody = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + unset($ch); + + if ($rawBody === false) { + return ['', $httpCode, $curlError !== '' ? $curlError : 'Brak odpowiedzi z API.']; + } + + return [(string) $rawBody, $httpCode, null]; + } + + /** + * @return array{ok: bool, http_code: int, message: string, message_id: string} + */ + private function parseResponse(string $body, int $httpCode): array + { + $decoded = json_decode(ltrim($body, "\xEF\xBB\xBF \t\n\r\0\x0B"), true); + if (!is_array($decoded)) { + return [ + 'ok' => false, + 'http_code' => $httpCode, + 'message' => 'Niepoprawna odpowiedz JSON SMSPLANET: ' . substr(trim(strip_tags($body)), 0, 180), + 'message_id' => '', + ]; + } + + $messageId = trim((string) ($decoded['messageId'] ?? '')); + if ($httpCode >= 200 && $httpCode < 300 && $messageId !== '') { + return [ + 'ok' => true, + 'http_code' => $httpCode, + 'message' => 'messageId: ' . $messageId, + 'message_id' => $messageId, + ]; + } + + $errorCode = trim((string) ($decoded['errorCode'] ?? '')); + $errorMessage = trim((string) ($decoded['errorMsg'] ?? '')); + if ($errorMessage === '') { + $errorMessage = 'HTTP ' . $httpCode; + } + if ($errorCode !== '') { + $errorMessage = 'errorCode ' . $errorCode . ': ' . $errorMessage; + } + + return [ + 'ok' => false, + 'http_code' => $httpCode, + 'message' => $errorMessage, + 'message_id' => '', + ]; + } +} diff --git a/src/Modules/Settings/SmsplanetIntegrationController.php b/src/Modules/Settings/SmsplanetIntegrationController.php new file mode 100644 index 0000000..0f6512e --- /dev/null +++ b/src/Modules/Settings/SmsplanetIntegrationController.php @@ -0,0 +1,145 @@ +template->render('settings/smsplanet', [ + 'title' => $this->translator->get('settings.smsplanet.title'), + 'activeMenu' => 'settings', + 'activeSettings' => 'integrations', + 'user' => $this->auth->user(), + 'csrfToken' => Csrf::token(), + 'settings' => $this->repository->getSettings(), + 'errorMessage' => (string) Flash::get('settings_error', ''), + 'successMessage' => (string) Flash::get('settings_success', ''), + 'testMessage' => (string) Flash::get('smsplanet_test', ''), + ], 'layouts/app'); + + return Response::html($html); + } + + public function save(Request $request): Response + { + $redirectTo = $this->resolveRedirect($request); + if (!Csrf::validate((string) $request->input('_token', ''))) { + Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); + return Response::redirect($redirectTo); + } + + try { + $this->repository->saveSettings([ + 'auth_method' => (string) $request->input('auth_method', ''), + 'api_token' => (string) $request->input('api_token', ''), + 'api_key' => (string) $request->input('api_key', ''), + 'api_password' => (string) $request->input('api_password', ''), + 'sender' => (string) $request->input('sender', ''), + 'clear_polish' => $request->input('clear_polish', ''), + 'transactional' => $request->input('transactional', ''), + 'is_active' => $request->input('is_active', ''), + ]); + Flash::set('settings_success', $this->translator->get('settings.smsplanet.flash.saved')); + } catch (Throwable $exception) { + Flash::set( + 'settings_error', + $this->translator->get('settings.smsplanet.flash.save_failed') . ' ' . $exception->getMessage() + ); + } + + return Response::redirect($redirectTo); + } + + public function test(Request $request): Response + { + $redirectTo = $this->resolveRedirect($request); + if (!Csrf::validate((string) $request->input('_token', ''))) { + Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); + return Response::redirect($redirectTo); + } + + try { + $phone = $this->validatePhone((string) $request->input('phone', '')); + $message = $this->validateMessage((string) $request->input('message', '')); + $credentials = $this->repository->getCredentials(); + if ($credentials === null) { + throw new IntegrationConfigException('Najpierw zapisz kompletna i aktywna konfiguracje SMSPLANET.'); + } + + $result = $this->apiClient->sendSms($credentials, $phone, $message); + $this->integrations->updateTestResult( + $credentials['integration_id'], + $result['ok'] ? 'ok' : 'fail', + (int) $result['http_code'], + (string) $result['message'] + ); + + if ($result['ok']) { + Flash::set('smsplanet_test', $this->translator->get('settings.smsplanet.flash.test_success', [ + 'message_id' => (string) $result['message_id'], + ])); + } else { + Flash::set('settings_error', $this->translator->get('settings.smsplanet.flash.test_failed') . ' ' . $result['message']); + } + } catch (Throwable $exception) { + Flash::set('settings_error', $this->translator->get('settings.smsplanet.flash.test_failed') . ' ' . $exception->getMessage()); + } + + return Response::redirect($redirectTo); + } + + private function resolveRedirect(Request $request): string + { + return RedirectPathResolver::resolve( + (string) $request->input('return_to', '/settings/integrations/smsplanet'), + ['/settings/integrations'], + '/settings/integrations/smsplanet' + ); + } + + private function validatePhone(string $value): string + { + $phone = preg_replace('/[\s+\-()]/', '', trim($value)) ?? ''; + if (preg_match('/^\d{8,15}$/', $phone) !== 1) { + throw new IntegrationConfigException('Podaj numer telefonu w formacie 600111222 albo 48600111222.'); + } + + return $phone; + } + + private function validateMessage(string $value): string + { + $message = trim($value); + if ($message === '') { + throw new IntegrationConfigException('Podaj tresc testowego SMS.'); + } + if (strlen($message) > 918) { + throw new IntegrationConfigException('Tresc testowego SMS nie moze przekraczac 918 znakow.'); + } + + return $message; + } +} diff --git a/src/Modules/Settings/SmsplanetIntegrationRepository.php b/src/Modules/Settings/SmsplanetIntegrationRepository.php new file mode 100644 index 0000000..43126f2 --- /dev/null +++ b/src/Modules/Settings/SmsplanetIntegrationRepository.php @@ -0,0 +1,330 @@ +integrations = new IntegrationsRepository($this->pdo); + $this->cipher = new IntegrationSecretCipher($this->secret); + } + + /** + * @return array + */ + public function getSettings(): array + { + $this->ensureRow(); + $integrationId = $this->ensureBaseIntegration(); + $row = $this->fetchRow() ?? []; + $integration = $this->integrations->findById($integrationId); + + return [ + 'integration_id' => $integrationId, + 'auth_method' => $this->normalizeAuthMethod((string) ($row['auth_method'] ?? '')), + 'sender' => trim((string) ($row['sender'] ?? '')), + 'clear_polish' => !empty($row['clear_polish']), + 'transactional' => !empty($row['transactional']), + 'has_api_token' => $this->hasEncryptedValue($row['api_token_encrypted'] ?? null), + 'has_api_key' => $this->hasEncryptedValue($row['api_key_encrypted'] ?? null), + 'has_api_password' => $this->hasEncryptedValue($row['api_password_encrypted'] ?? null), + 'is_active' => (int) ($integration['is_active'] ?? 1) === 1, + 'last_test_status' => trim((string) ($integration['last_test_status'] ?? '')), + 'last_test_http_code' => isset($integration['last_test_http_code']) ? (int) $integration['last_test_http_code'] : null, + 'last_test_message' => trim((string) ($integration['last_test_message'] ?? '')), + 'last_test_at' => trim((string) ($integration['last_test_at'] ?? '')), + 'updated_at' => trim((string) ($row['updated_at'] ?? '')), + ]; + } + + /** + * @param array $payload + */ + public function saveSettings(array $payload): void + { + $this->ensureRow(); + $integrationId = $this->ensureBaseIntegration(); + $row = $this->fetchRequiredRow(); + + $authMethod = $this->normalizeAuthMethod((string) ($payload['auth_method'] ?? '')); + $sender = $this->validateSender((string) ($payload['sender'] ?? '')); + $tokenEncrypted = $this->resolveTokenEncrypted($row, (string) ($payload['api_token'] ?? '')); + $keyEncrypted = $this->resolveKeyEncrypted($row, (string) ($payload['api_key'] ?? '')); + $passwordEncrypted = $this->resolvePasswordEncrypted($row, (string) ($payload['api_password'] ?? '')); + + $this->validateCredentials($authMethod, $tokenEncrypted, $keyEncrypted, $passwordEncrypted); + $this->updateSettingsRow($authMethod, $tokenEncrypted, $keyEncrypted, $passwordEncrypted, $sender, $payload); + $this->updateIntegrationActive($integrationId, !empty($payload['is_active'])); + } + + /** + * @return array{ + * integration_id: int, + * auth_method: string, + * api_token: string, + * api_key: string, + * api_password: string, + * sender: string, + * clear_polish: bool, + * transactional: bool + * }|null + */ + public function getCredentials(): ?array + { + $this->ensureRow(); + $integrationId = $this->ensureBaseIntegration(); + $row = $this->fetchRow(); + $integration = $this->integrations->findById($integrationId); + if ($row === null || (int) ($integration['is_active'] ?? 0) !== 1) { + return null; + } + + $authMethod = $this->normalizeAuthMethod((string) ($row['auth_method'] ?? '')); + $sender = trim((string) ($row['sender'] ?? '')); + $apiToken = $this->decryptValue((string) ($row['api_token_encrypted'] ?? '')); + $apiKey = $this->decryptValue((string) ($row['api_key_encrypted'] ?? '')); + $apiPassword = $this->decryptValue((string) ($row['api_password_encrypted'] ?? '')); + + if (!$this->hasCompleteCredentials($authMethod, $sender, $apiToken, $apiKey, $apiPassword)) { + return null; + } + + return [ + 'integration_id' => $integrationId, + 'auth_method' => $authMethod, + 'api_token' => $apiToken, + 'api_key' => $apiKey, + 'api_password' => $apiPassword, + 'sender' => $sender, + 'clear_polish' => !empty($row['clear_polish']), + 'transactional' => !empty($row['transactional']), + ]; + } + + private function ensureBaseIntegration(): int + { + return $this->integrations->ensureIntegration( + self::INTEGRATION_TYPE, + self::INTEGRATION_NAME, + self::INTEGRATION_BASE_URL, + 15, + true + ); + } + + private function ensureRow(): void + { + $integrationId = $this->ensureBaseIntegration(); + $statement = $this->pdo->prepare( + 'INSERT INTO smsplanet_integration_settings (id, integration_id, created_at, updated_at) + VALUES (1, :integration_id, NOW(), NOW()) + ON DUPLICATE KEY UPDATE integration_id = VALUES(integration_id), updated_at = VALUES(updated_at)' + ); + $statement->execute(['integration_id' => $integrationId]); + } + + /** + * @return array|null + */ + private function fetchRow(): ?array + { + try { + $statement = $this->pdo->prepare('SELECT * FROM smsplanet_integration_settings WHERE id = 1 LIMIT 1'); + $statement->execute(); + $row = $statement->fetch(PDO::FETCH_ASSOC); + } catch (Throwable) { + return null; + } + + return is_array($row) ? $row : null; + } + + /** + * @return array + */ + private function fetchRequiredRow(): array + { + $row = $this->fetchRow(); + if ($row === null) { + throw new IntegrationConfigException('Brak rekordu konfiguracji SMSPLANET.'); + } + + return $row; + } + + private function normalizeAuthMethod(string $value): string + { + return $value === self::AUTH_KEY_PASSWORD ? self::AUTH_KEY_PASSWORD : self::AUTH_TOKEN; + } + + private function validateSender(string $value): string + { + $sender = trim($value); + if ($sender === '' || strlen($sender) > 32) { + throw new IntegrationConfigException('Podaj pole nadawcy SMSPLANET (maks. 32 znaki).'); + } + + return $sender; + } + + /** + * @param array $row + */ + private function resolveTokenEncrypted(array $row, string $newToken): ?string + { + $token = trim($newToken); + if ($token !== '') { + return $this->cipher->encrypt($token); + } + + return StringHelper::nullableString((string) ($row['api_token_encrypted'] ?? '')); + } + + /** + * @param array $row + */ + private function resolveKeyEncrypted(array $row, string $newKey): ?string + { + $key = trim($newKey); + if ($key !== '') { + return $this->cipher->encrypt($key); + } + + return StringHelper::nullableString((string) ($row['api_key_encrypted'] ?? '')); + } + + /** + * @param array $row + */ + private function resolvePasswordEncrypted(array $row, string $newPassword): ?string + { + $password = trim($newPassword); + if ($password !== '') { + return $this->cipher->encrypt($password); + } + + return StringHelper::nullableString((string) ($row['api_password_encrypted'] ?? '')); + } + + private function validateCredentials( + string $authMethod, + ?string $tokenEncrypted, + ?string $keyEncrypted, + ?string $passwordEncrypted + ): void { + if ($authMethod === self::AUTH_TOKEN && ($tokenEncrypted === null || $tokenEncrypted === '')) { + throw new IntegrationConfigException('Podaj token Bearer SMSPLANET.'); + } + + if ($authMethod !== self::AUTH_KEY_PASSWORD) { + return; + } + + if ($keyEncrypted === null || $keyEncrypted === '') { + throw new IntegrationConfigException('Podaj klucz API SMSPLANET.'); + } + if ($passwordEncrypted === null || $passwordEncrypted === '') { + throw new IntegrationConfigException('Podaj haslo API SMSPLANET.'); + } + } + + /** + * @param array $payload + */ + private function updateSettingsRow( + string $authMethod, + ?string $tokenEncrypted, + ?string $keyEncrypted, + ?string $passwordEncrypted, + string $sender, + array $payload + ): void { + $statement = $this->pdo->prepare( + 'UPDATE smsplanet_integration_settings + SET auth_method = :auth_method, + api_token_encrypted = :api_token_encrypted, + api_key_encrypted = :api_key_encrypted, + api_password_encrypted = :api_password_encrypted, + sender = :sender, + clear_polish = :clear_polish, + transactional = :transactional, + updated_at = NOW() + WHERE id = 1' + ); + $statement->execute([ + 'auth_method' => $authMethod, + 'api_token_encrypted' => $tokenEncrypted, + 'api_key_encrypted' => $keyEncrypted, + 'api_password_encrypted' => $passwordEncrypted, + 'sender' => $sender, + 'clear_polish' => !empty($payload['clear_polish']) ? 1 : 0, + 'transactional' => !empty($payload['transactional']) ? 1 : 0, + ]); + } + + private function updateIntegrationActive(int $integrationId, bool $isActive): void + { + $statement = $this->pdo->prepare( + 'UPDATE integrations + SET is_active = :is_active, + updated_at = NOW() + WHERE id = :id AND type = :type' + ); + $statement->execute([ + 'id' => $integrationId, + 'type' => self::INTEGRATION_TYPE, + 'is_active' => $isActive ? 1 : 0, + ]); + } + + private function hasEncryptedValue(mixed $value): bool + { + return trim((string) $value) !== ''; + } + + private function decryptValue(string $encrypted): string + { + $value = StringHelper::nullableString($encrypted); + if ($value === null) { + return ''; + } + + return trim((string) $this->cipher->decrypt($value)); + } + + private function hasCompleteCredentials( + string $authMethod, + string $sender, + string $apiToken, + string $apiKey, + string $apiPassword + ): bool { + if ($sender === '') { + return false; + } + + if ($authMethod === self::AUTH_TOKEN) { + return $apiToken !== ''; + } + + return $apiKey !== '' && $apiPassword !== ''; + } +}