From bc2ed2c8e205bf3aab76d68266648342aca2a884 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 12 May 2026 12:25:07 +0200 Subject: [PATCH] feat(116): hostedsms integration settings Phase 116 complete: - add HostedSMS settings with encrypted password storage - add SimpleAPI real test SMS flow and integrations hub row - document schema, architecture, changelog, and PAUL state Co-Authored-By: Codex --- .paul/PROJECT.md | 11 +- .paul/ROADMAP.md | 4 +- .paul/STATE.md | 74 +++---- .paul/changelog/2026-05-12.md | 31 +++ .paul/codebase/architecture.md | 19 ++ .paul/codebase/db_schema.md | 16 ++ .paul/codebase/tech_changelog.md | 15 ++ .../116-hostedsms-integration/116-01-PLAN.md | 205 +++++++++++++++++ .../116-01-SUMMARY.md | 173 +++++++++++++++ DOCS/ARCHITECTURE.md | 19 ++ DOCS/DB_SCHEMA.md | 16 ++ DOCS/TECH_CHANGELOG.md | 15 ++ ..._create_hostedsms_integration_settings.sql | 30 +++ resources/lang/pl.php | 46 ++++ resources/views/settings/hostedsms.php | 126 +++++++++++ routes/web.php | 21 +- src/Modules/Settings/HostedSmsApiClient.php | 125 +++++++++++ .../HostedSmsIntegrationController.php | 154 +++++++++++++ .../HostedSmsIntegrationRepository.php | 208 ++++++++++++++++++ .../Settings/IntegrationsHubController.php | 30 ++- 20 files changed, 1282 insertions(+), 56 deletions(-) create mode 100644 .paul/changelog/2026-05-12.md create mode 100644 .paul/phases/116-hostedsms-integration/116-01-PLAN.md create mode 100644 .paul/phases/116-hostedsms-integration/116-01-SUMMARY.md create mode 100644 database/migrations/20260512_000107_create_hostedsms_integration_settings.sql create mode 100644 resources/views/settings/hostedsms.php create mode 100644 src/Modules/Settings/HostedSmsApiClient.php create mode 100644 src/Modules/Settings/HostedSmsIntegrationController.php create mode 100644 src/Modules/Settings/HostedSmsIntegrationRepository.php diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 8fe7259..ee42c25 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -13,8 +13,8 @@ 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 (Fakturownia Foundation) + 114 (Accounting Configs Refactor) + 115 (Wystawianie faktury z zamowienia) shipped | -| Last Updated | 2026-05-10 | +| Status | v3.7 in progress — Phases 113-116 shipped (Fakturownia + HostedSMS settings/test SMS) | +| Last Updated | 2026-05-12 | ## Requirements @@ -119,6 +119,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Fundament v3.7 Invoices: tabele `invoices`, `invoice_configs`, `invoice_number_counters`, `fakturownia_integration_settings` + `orders.invoice_requested`; CRUD kont Fakturowni z testem polaczenia API (`/settings/integrations/fakturownia`); karta w hubie integracji — Phase 113 - [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 ### Deferred @@ -127,7 +128,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów ### Active (In Progress) -- [ ] v3.7 Invoices — wystawianie faktur dla klientow z NIP przez integracje z Fakturownia (multi-account, lokalna numeracja z opcja delegacji, rozdzielenie przyciskow paragon/faktura, osobne podstrony edycji configs). Phases 113 + 114 + 115 shipped; ewentualne kolejne fazy (np. eksport XLSX, invoice.created event, idempotencja Fakturowni) w kolejce. +- [ ] 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. ### Planned (Next) @@ -195,6 +196,8 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API | Historia automatyzacji zapisywana per regula (success/failed) i czyszczona cronem po 30 dniach | Audyt wykonywania regul bez recznego utrzymania danych | 2026-03-28 | Active | | Akcja update_order_status korzysta z OrdersRepository::updateOrderStatus | Spojnosc z historia statusow i activity log bez duplikowania logiki | 2026-03-28 | Active | | 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 | | 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 | @@ -252,6 +255,6 @@ Quick Reference: --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-05-10 after Phase 115 (Wystawianie faktury z zamowienia) completion; v3.7 Invoices milestone in progress* +*Last updated: 2026-05-12 after Phase 116 (HostedSMS Integration Settings + Test SMS) completion; v3.7 milestone in progress* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index f695f91..c0257aa 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -15,11 +15,13 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt | 113 | Fakturownia Integration Foundation | 1/1 | Complete (2026-05-10) | | 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) | 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 - Backfill `curl_close()` w `ShopproIntegrationsRepository` (PHP 8.5 compat, poza zakresem 115) ## Next Milestone @@ -494,4 +496,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-05-10 - Phase 115 (Wystawianie faktury z zamowienia) complete; v3.7 milestone in progress* +*Last updated: 2026-05-12 - Phase 116 (HostedSMS Integration Settings + Test SMS) complete; v3.7 milestone in progress* diff --git a/.paul/STATE.md b/.paul/STATE.md index 6ae9085..d52cec0 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,79 +5,55 @@ 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 — Phase 113 + 114 shipped; nastepna faza 115 (wystawianie faktury z zamowienia) do zaplanowania +**Current focus:** v3.7 Invoices + operational integrations - Phase 116 HostedSMS settings/test SMS complete. ## Current Position -Milestone: v3.7 Invoices (Fakturownia integration) — In progress -Phase: 116 of TBD (TBD — kandydaci v3.7 lub przejscie na kolejny milestone) — Not started +Milestone: v3.7 Invoices (Fakturownia integration) - In progress +Phase: 117 of TBD (next candidate) - Not started Plan: pending -Status: Phase 115 closed; transition done; ready to plan kolejna faze -Last activity: 2026-05-10 — UNIFY 115-01 complete + transition: PROJECT.md/ROADMAP.md/changelog zaktualizowane +Status: Phase 116 complete; ready to plan next phase +Last activity: 2026-05-12 - UNIFY 116-01 complete and transition done Progress: -- Milestone v3.7: [██████░░░░] ~55% (Phase 113 + 114 + 115 zamkniete; kandydaci: XLSX invoices export, INVOICE-IDEMP-115, invoice.created event, curl_close shopPRO backfill) -- Phase 115: [██████████] 100% — Complete +- Milestone v3.7: [########--] ~75% (Phase 113 + 114 + 115 + 116 closed) +- Phase 116: [##########] 100% - Complete ## Loop Position Current loop state: ``` -v3.7 milestone: - Phase 113 (Fakturownia Integration Foundation): Complete - Phase 114 (Accounting Configs Refactor): Complete - Phase 115 (Wystawianie faktury z zamowienia): Complete - Phase 116 (TBD): not started -``` - -``` -PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Phase 115 closed; ready for Phase 116 plan] +PLAN -> APPLY -> UNIFY + done done done [Loop complete - ready for next PLAN] ``` ## Session Continuity -Last session: 2026-05-10 -Stopped at: Phase 115 transition complete (PROJECT.md + ROADMAP.md updated, SUMMARY zapisany, changelog zaktualizowany) -Next action: /paul:plan dla kolejnej fazy (v3.7 kandydaci: XLSX export listy faktur, INVOICE-IDEMP-115, invoice.created automation event, lub backfill `curl_close()` w shopPRO) -Resume file: .paul/phases/115-invoice-from-order/115-01-SUMMARY.md +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 ## Git State -Last commit: 33ee1a1 feat(115): wystawianie faktury z zamowienia (lokalne + delegowane Fakturownia) +Last known commit: ecef7c7 feat(116): hostedsms integration settings Branch: main -Feature branches merged: none ## Pending Actions -- **Phase 113-01 (smoke test wykonany przez usera 2026-05-10):** OK -- Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online) -- Backfill zamowienia #882 — operator robi recznie po wdrozeniu (poza zakresem planu) -- Uruchom migracje gdy XAMPP online: `php bin/migrate.php` (delivery_statuses) -- Recznie odtworzyc istniejace reguly automatyzacji z grupowymi kluczami (BREAKING z 108-02) +- Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online). +- Backfill zamowienia #882 - operator robi recznie po wdrozeniu (poza zakresem planu). +- 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. ## Deferred to Next Milestones -- Phase 68 - Code Deduplication Refactor (0/2 Planning, nigdy nie rozpoczety) -- STAT-NET - netto shopPRO z API lub z `order_items.tax_rate` (`.paul/TODO.md`) -- Mobile Orders List / Mobile Order Details / Mobile Settings (TBD z poprzedniego roadmapu) -- sonar-scanner - skan dla phase 105, 106, 107, 108 nie zostal uruchomiony (skill gap odnotowany) -- INDEX-106-01 - indeksy DB dla query `customer_returned_count`: `order_addresses(order_id, address_type)`, `shipment_packages(order_id, delivery_status)` (gdy dataset >50k wierszy) +- Phase 68 - Code Deduplication Refactor (0/2 Planning, nigdy nie rozpoczety). +- STAT-NET - netto shopPRO z API lub z `order_items.tax_rate` (`.paul/TODO.md`). +- Mobile Orders List / Mobile Order Details / Mobile Settings. +- INDEX-106-01 - indeksy DB dla query `customer_returned_count`: `order_addresses(order_id, address_type)`, `shipment_packages(order_id, delivery_status)` (gdy dataset >50k wierszy). -## Skill Audit (Phase 108) +## Skill Requirements -| Expected | Invoked | Notes | -|----------|---------|-------| -| sonar-scanner (required) | o | Wymagany po APPLY 108-01 i 108-02 - odlozony | - -## Skill Audit (Phase 110) - -| Expected | Invoked | Notes | -|----------|---------|-------| -| sonar-scanner (required) | yes | Skan uruchomiony po APPLY; raport wyslany do SonarQube. | - -## Phase 110 Notes - -- Local HTTP verification blocked by MySQL/XAMPP connection refused. -- PHPUnit not run: `composer` unavailable in PATH and `vendor/` absent. -- Sonar issue import to `DOCS/todo.md` not performed because SonarQube MCP/resources are unavailable in this session. +- `sonar-scanner` required after APPLY; Phase 116 gap documented because CLI was not available in PATH. diff --git a/.paul/changelog/2026-05-12.md b/.paul/changelog/2026-05-12.md new file mode 100644 index 0000000..70bfb01 --- /dev/null +++ b/.paul/changelog/2026-05-12.md @@ -0,0 +1,31 @@ +# 2026-05-12 + +## Co zrobiono + +- [Phase 116, Plan 01] Dodano integracje HostedSMS: ustawienia konta, szyfrowane haslo, karta w hubie integracji i realna wysylka testowego SMS. +- Dodano klienta HostedSMS SimpleAPI (`POST https://api.hostedsms.pl/SimpleApi`) z obsluga `MessageId` i `ErrorMessage`. +- 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. + +## Zmienione pliki + +- `.paul/PROJECT.md` +- `.paul/ROADMAP.md` +- `.paul/STATE.md` +- `.paul/codebase/architecture.md` +- `.paul/codebase/db_schema.md` +- `.paul/codebase/tech_changelog.md` +- `.paul/phases/116-hostedsms-integration/116-01-PLAN.md` +- `.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md` +- `DOCS/ARCHITECTURE.md` +- `DOCS/DB_SCHEMA.md` +- `DOCS/TECH_CHANGELOG.md` +- `database/migrations/20260512_000107_create_hostedsms_integration_settings.sql` +- `resources/lang/pl.php` +- `resources/views/settings/hostedsms.php` +- `routes/web.php` +- `src/Modules/Settings/HostedSmsApiClient.php` +- `src/Modules/Settings/HostedSmsIntegrationController.php` +- `src/Modules/Settings/HostedSmsIntegrationRepository.php` +- `src/Modules/Settings/IntegrationsHubController.php` diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index c968f32..34cdecc 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -262,6 +262,25 @@ tests/ --- +## Phase 116 - HostedSMS Integration Settings + +### HostedSmsIntegrationRepository (`src/Modules/Settings/HostedSmsIntegrationRepository.php`) +- Zarzadza pojedynczym rekordem `hostedsms_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `hostedsms`. +- Szyfruje haslo przez `IntegrationSecretCipher`; formularz widzi tylko flage `has_password`. +- Udostepnia `getCredentials()` dla kontrolera testowej wysylki SMS. + +### HostedSmsApiClient (`src/Modules/Settings/HostedSmsApiClient.php`) +- Wykonuje `POST https://api.hostedsms.pl/SimpleApi` jako `application/x-www-form-urlencoded`. +- Wysyla `UserEmail`, `Password`, `Sender`, `Phone`, `Message` oraz opcjonalnie `ConvertMessageToGSM7`. +- Traktuje `MessageId` jako sukces, a `ErrorMessage` jako blad biznesowy nawet przy HTTP 200. + +### HostedSmsIntegrationController (`src/Modules/Settings/HostedSmsIntegrationController.php`) +- Endpointy: `GET /settings/integrations/hostedsms`, `POST /settings/integrations/hostedsms/save`, `POST /settings/integrations/hostedsms/test`. +- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`. + +### IntegrationsHubController +- Dodaje wiersz HostedSMS 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 66c40dc..013fbe6 100644 --- a/.paul/codebase/db_schema.md +++ b/.paul/codebase/db_schema.md @@ -575,6 +575,22 @@ UNIQUE: `(integration_id)` — one settings row per Fakturownia integration. Mul --- +**hostedsms_integration_settings** - HostedSMS account credentials (Phase 116; fixed 1 row) +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| `id` | TINYINT UNSIGNED | NO | PK, always 1 | +| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK -> integrations(id) CASCADE | +| `user_email` | VARCHAR(190) | YES | HostedSMS login | +| `password_encrypted` | TEXT | YES | AES-encrypted via `IntegrationSecretCipher` | +| `sender` | VARCHAR(32) | YES | HostedSMS sender name / nadpis | +| `convert_message_to_gsm7` | TINYINT(1) | NO | DEFAULT 0 | +| `created_at` | DATETIME | NO | | +| `updated_at` | DATETIME | NO | | + +UNIQUE: `(integration_id)` - one global HostedSMS settings row. + +--- + ## Accounting / Receipts **receipt_configs** — Receipt generation configurations diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md index 521d576..78a6903 100644 --- a/.paul/codebase/tech_changelog.md +++ b/.paul/codebase/tech_changelog.md @@ -1,5 +1,20 @@ # Technical Changelog +## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS + +**Co zrobiono:** +- Dodano migracje `20260512_000107_create_hostedsms_integration_settings.sql` z pojedyncza konfiguracja `hostedsms_integration_settings` i bazowym wpisem `integrations` typu `hostedsms`. +- Dodano `HostedSmsIntegrationRepository` z szyfrowaniem hasla przez `IntegrationSecretCipher`. +- Dodano `HostedSmsApiClient` dla HostedSMS SimpleAPI (`POST https://api.hostedsms.pl/SimpleApi`). +- Dodano `HostedSmsIntegrationController` i trasy `/settings/integrations/hostedsms`, `/save`, `/test`. +- Dodano widok `resources/views/settings/hostedsms.php` z konfiguracja i realna wysylka testowego SMS z edytowalna trescia oraz czytelnym panelem ostatniego testu (`OK`, HTTP, MessageId). +- Dodano HostedSMS do hubu integracji `/settings/integrations`. + +**Dlaczego:** +- Operator potrzebuje najpierw zapisac dane HostedSMS i sprawdzic realna wysylke SMS, zanim integracja zostanie wykorzystana w automatyzacjach lub komunikacji z klientami. +- Test uzywa rzeczywistej wysylki, bo SimpleAPI nie udostepnia osobnego endpointu ping/test. +- Haslo nie jest ujawniane po zapisie; UI pokazuje tylko status zapisanego sekretu. + ## 2026-05-10 - Phase 115 Plan 01: Wystawianie faktury z zamowienia **Co zrobiono:** diff --git a/.paul/phases/116-hostedsms-integration/116-01-PLAN.md b/.paul/phases/116-hostedsms-integration/116-01-PLAN.md new file mode 100644 index 0000000..74ecfad --- /dev/null +++ b/.paul/phases/116-hostedsms-integration/116-01-PLAN.md @@ -0,0 +1,205 @@ +--- +phase: 116-hostedsms-integration +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - database/migrations/20260512_000107_create_hostedsms_integration_settings.sql + - src/Modules/Settings/HostedSmsApiClient.php + - src/Modules/Settings/HostedSmsIntegrationRepository.php + - src/Modules/Settings/HostedSmsIntegrationController.php + - src/Modules/Settings/IntegrationsHubController.php + - routes/web.php + - resources/views/settings/hostedsms.php + - resources/views/settings/integrations.php + - resources/views/layouts/app.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 HostedSMS: pojedyncza globalna konfiguracja konta oraz formularz realnej wysylki testowego SMS-a z edytowalna trescia. + +## Purpose +Operator ma moc potwierdzic, ze dane HostedSMS sa poprawne, zanim integracja zostanie uzyta w automatyzacjach lub komunikacji z klientami. + +## Output +Nowa podstrona `/settings/integrations/hostedsms`, zapis konfiguracji w DB, klient API SimpleAPI, akcja testowej wysylki SMS i wpis w hubie integracji. + + + + +- **Test SMS** - Czy test ma faktycznie wysylac wiadomosc? + -> Odpowiedz: Ma faktycznie wysylac testowy sms. +- **Liczba kont** - Jedna globalna konfiguracja czy wiele kont? + -> Odpowiedz: Wystarczy jedna. +- **Tresc testu** - Tresc testowego SMS ma byc stala czy edytowalna? + -> Odpowiedz: Edytowalna. + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md +@AGENTS.md +@DOCS/DB_SCHEMA.md +@DOCS/ARCHITECTURE.md + +## API Context +HostedSMS SimpleAPI z dokumentacji `https://panel.hostedsms.pl/Doc/HostedSms-Opis_Techniczny_API_pl.pdf`: +- endpoint podstawowy: `POST https://api.hostedsms.pl/SimpleApi` +- endpoint zapasowy: `POST https://api2.hostedsms.pl/SimpleApi` +- pola: `UserEmail`, `Password`, `Sender`, `Phone`, `Message`, opcjonalnie `ConvertMessageToGSM7` +- numer telefonu w formacie miedzynarodowym, np. `48xxxxxxxxx` +- odpowiedz JSON zawiera `MessageId` przy sukcesie albo `ErrorMessage` przy bledzie; HTTP 200 moze oznaczac blad biznesowy + +## Source Files +@routes/web.php +@src/Modules/Settings/IntegrationsRepository.php +@src/Modules/Settings/IntegrationSecretCipher.php +@src/Modules/Settings/IntegrationsHubController.php +@src/Modules/Settings/FakturowniaIntegrationRepository.php +@src/Modules/Settings/FakturowniaIntegrationController.php +@src/Modules/Settings/FakturowniaApiClient.php +@resources/views/settings/fakturownia.php +@resources/views/settings/fakturownia-edit.php +@resources/views/settings/integrations.php +@resources/views/layouts/app.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 HostedSMS +```gherkin +Given zalogowany operator jest na stronie ustawien HostedSMS +When wpisze UserEmail, haslo, nadpis Sender, status aktywnosci i zapisze formularz z poprawnym CSRF +Then konfiguracja zostanie zapisana jako jedna globalna integracja, haslo bedzie zaszyfrowane przez IntegrationSecretCipher, a zapisany sekret nie bedzie widoczny w formularzu +``` + +## AC-2: Walidacja konfiguracji +```gherkin +Given operator probuje zapisac lub testowac HostedSMS +When brakuje loginu, hasla przy pierwszym zapisie, nadpisu Sender albo numer testowy nie jest w formacie miedzynarodowym +Then aplikacja pokazuje czytelny blad i nie wykonuje wysylki testowej bez kompletnych danych +``` + +## AC-3: Realny test wysylki SMS +```gherkin +Given konfiguracja HostedSMS jest zapisana z poprawnymi danymi +When operator poda numer testowy i edytowalna tresc testowa oraz kliknie wysylke testowa +Then aplikacja wykona POST do HostedSMS SimpleAPI, zapisze wynik w polach last_test_* integracji i pokaze MessageId albo komunikat bledu z API +``` + +## AC-4: Widocznosc w panelu integracji +```gherkin +Given operator otwiera Ustawienia > Integracje +When integracja HostedSMS istnieje albo jeszcze nie jest skonfigurowana +Then hub pokazuje wiersz HostedSMS ze statusem konfiguracji, aktywnosci, ostatniego testu i linkiem do ustawien +``` + +## AC-5: 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 HostedSMS + database/migrations/20260512_000107_create_hostedsms_integration_settings.sql, src/Modules/Settings/HostedSmsIntegrationRepository.php + + Utworz migracje dla pojedynczej tabeli `hostedsms_integration_settings` z rekordem `id=1`, `integration_id` jako UNIQUE FK do `integrations`, polami `user_email`, `password_encrypted`, `sender`, `convert_message_to_gsm7`, `created_at`, `updated_at`. + Repozytorium ma zapewniac bazowy rekord `integrations` typu `hostedsms`, nazwa `HostedSMS`, base_url `https://api.hostedsms.pl/SimpleApi`, zwracac status `has_password` bez ujawniania hasla, zapisywac nowe haslo tylko gdy pole formularza nie jest puste, przy pierwszym zapisie wymagac hasla oraz uzywac `IntegrationSecretCipher` i prepared statements. + + C:\xampp\php\php.exe -l src/Modules/Settings/HostedSmsIntegrationRepository.php + AC-1 i AC-2 spelnione dla warstwy zapisu konfiguracji. + + + + Task 2: Dodac klienta SimpleAPI i kontroler ustawien + src/Modules/Settings/HostedSmsApiClient.php, src/Modules/Settings/HostedSmsIntegrationController.php, routes/web.php + + Utworz `HostedSmsApiClient` wykonujacy POST form-urlencoded do SimpleAPI z `Accept: application/json`, SSL verification i CA z `SslCertificateResolver`. Parsuj odpowiedz JSON: `MessageId` oznacza sukces, `ErrorMessage` blad biznesowy mimo HTTP 200; blad cURL/HTTP ma trafic do komunikatu. + Utworz `HostedSmsIntegrationController` z akcjami `index`, `save`, `test`. `save` waliduje CSRF, login e-mail, sender i wymaganie hasla przy pierwszym zapisie. `test` waliduje CSRF, numer w formacie miedzynarodowym (`^\d{8,15}$`, z hintem `48...`), tresc niepusta i maks. 4000 znakow. Test realnie wysyla SMS i zapisuje `last_test_status`, `last_test_http_code`, `last_test_message` przez `IntegrationsRepository::updateTestResult`. + Podlacz DI i trasy: GET `/settings/integrations/hostedsms`, POST `/settings/integrations/hostedsms/save`, POST `/settings/integrations/hostedsms/test`. + + C:\xampp\php\php.exe -l src/Modules/Settings/HostedSmsApiClient.php; C:\xampp\php\php.exe -l src/Modules/Settings/HostedSmsIntegrationController.php; C:\xampp\php\php.exe -l routes/web.php + AC-2 i AC-3 spelnione dla backendu i realnej wysylki testowej. + + + + Task 3: Dodac UI, hub integracji i dokumentacje + resources/views/settings/hostedsms.php, resources/views/settings/integrations.php, resources/views/layouts/app.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 HostedSMS bez inline CSS w nowych blokach. Formularz konfiguracji ma pokazac pola: UserEmail, nowe haslo (placeholder gdy zapisane), Sender, checkbox konwersji do GSM7, aktywnosc. Sekcja testu ma miec numer telefonu i edytowalna tresc z domyslna wartoscia `Test orderPRO HostedSMS`. + Dodaj HostedSMS do hubu integracji i aktywnego menu ustawien. Jezeli potrzeba nowych styli, umiesc je w SCSS, nie w widoku. + Zaktualizuj dokumentacje techniczna: tabela `hostedsms_integration_settings`, nowe klasy, trasy i przeplyw testowej wysylki. + + C:\xampp\php\php.exe -l resources/views/settings/hostedsms.php; C:\xampp\php\php.exe -l src/Modules/Settings/IntegrationsHubController.php; npm run build --if-present + AC-4 i AC-5 spelnione dla UI, hubu i dokumentacji. + + + + + + +## DO NOT CHANGE +- Nie podpinac `DB_HOST_REMOTE` do runtime aplikacji. +- Nie dodawac automatyzacji SMS, szablonow SMS ani wysylki z zamowien w tym planie. +- Nie dodawac natywnych `alert()` / `confirm()`. +- Nie przenosic ani refaktoryzowac istniejacych integracji poza minimalnym dopieciem HostedSMS. + +## SCOPE LIMITS +- Tylko jedna globalna konfiguracja HostedSMS. +- Tylko realna wysylka testowa SMS z ustawien. +- Bez historii wyslanych SMS poza istniejacym `last_test_*` i ewentualnym `integration_test_logs`, jesli aktualny `IntegrationsRepository` juz go uzywa. +- Bez raportow dostarczenia, FullApi, WebService2SMS i endpointu zapasowego `api2` w pierwszej wersji, chyba ze zostanie uzyty jako prosty fallback po bledzie polaczenia. + + + + +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 HostedSMS, wysylka testowego SMS na numer w formacie `48...`, komunikat z `MessageId` +- [ ] `sonar-scanner` po APPLY, jezeli CLI i SonarQube sa dostepne +- [ ] DOCS i `.paul/codebase` zaktualizowane +- [ ] All acceptance criteria met + + + +- Operator moze zapisac jedna konfiguracje HostedSMS bez ujawniania hasla. +- Operator moze wyslac realny testowy SMS z edytowalna trescia. +- Wynik testu jest widoczny w ekranie HostedSMS i hubie integracji. +- Migracje, lint i build przechodza albo blokery srodowiskowe sa jasno opisane w SUMMARY. + + + +After completion, create `.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md` + diff --git a/.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md b/.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md new file mode 100644 index 0000000..c01e715 --- /dev/null +++ b/.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md @@ -0,0 +1,173 @@ +--- +phase: 116-hostedsms-integration +plan: 01 +subsystem: settings-integrations +tags: [hostedsms, sms, api, settings, integration] +requires: + - phase: 113-fakturownia-integration-foundation + provides: integrations hub patterns and encrypted integration settings pattern +provides: + - HostedSMS global settings screen + - HostedSMS SimpleAPI client + - Real test SMS flow with persisted last_test status +affects: [settings, integrations, future-sms-automation] +tech-stack: + added: [] + patterns: [single-row integration settings, IntegrationSecretCipher encrypted secret, integrations.last_test observability] +key-files: + created: + - database/migrations/20260512_000107_create_hostedsms_integration_settings.sql + - src/Modules/Settings/HostedSmsApiClient.php + - src/Modules/Settings/HostedSmsIntegrationRepository.php + - src/Modules/Settings/HostedSmsIntegrationController.php + - resources/views/settings/hostedsms.php + modified: + - routes/web.php + - src/Modules/Settings/IntegrationsHubController.php + - resources/lang/pl.php + - DOCS/DB_SCHEMA.md + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md +key-decisions: + - "HostedSMS starts as one global account, not multi-account." + - "Test action sends a real SMS because SimpleAPI has no ping endpoint." + - "Inbound SMS replies are deferred; HostedSMS supports inbound retrieval only after DCS/HostedSMS activation." +patterns-established: + - "Provider settings screen stores secrets encrypted and only renders has_secret state." + - "HostedSMS API result uses MessageId as success and ErrorMessage as business failure." +duration: 1h +started: 2026-05-12T10:34:00+02:00 +completed: 2026-05-12T12:10:00+02:00 +--- + +# Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS Summary + +HostedSMS now has a settings screen, encrypted credential storage, SimpleAPI client, integrations-hub status, and a real test-SMS flow confirmed by the user. + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~1h | +| Started | 2026-05-12T10:34:00+02:00 | +| Completed | 2026-05-12T12:10:00+02:00 | +| Tasks | 3 completed | +| Files modified | 20 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Zapis konfiguracji HostedSMS | Pass | Global settings row, encrypted password, non-revealing UI implemented. | +| AC-2: Walidacja konfiguracji | Pass | CSRF, e-mail, password, sender, phone and message validation implemented. | +| AC-3: Realny test wysylki SMS | Pass | User confirmed `OK HTTP 200` with MessageId `d935d71a-d9a0-4cfb-be06-03fe36c71150` at `2026-05-12 12:03:22`. | +| AC-4: Widocznosc w panelu integracji | Pass | HostedSMS row added to `/settings/integrations`. | +| AC-5: Dokumentacja i zgodnosc projektu | Pass | DOCS and `.paul/codebase` updated; lint/build passed. Sonar CLI unavailable. | + +## Accomplishments + +- Added `hostedsms_integration_settings` and idempotent seed of base `integrations` row. +- Added `HostedSmsIntegrationRepository`, `HostedSmsApiClient`, and `HostedSmsIntegrationController`. +- Added routes `GET/POST /settings/integrations/hostedsms...`. +- Added compact UI for settings and editable real test SMS. +- Improved final test display from raw `MessageId: ...` text to a cleaner status panel with date, status, HTTP code and separate message identifier. +- Documented that inbound SMS replies are possible in HostedSMS only through the inbound/Webserwis2SMS side after activation by DCS/HostedSMS, so reply handling is future scope. + +## Task Commits + +| Task | Commit | Type | Description | +|------|--------|------|-------------| +| Task 1: Model konfiguracji HostedSMS | `ecef7c7` | feat | Migration + repository. | +| Task 2: Klient SimpleAPI i kontroler | `ecef7c7` | feat | API client + controller + routes. | +| Task 3: UI, hub i dokumentacja | `ecef7c7` | feat | Settings view, hub row, translations, docs. | + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `database/migrations/20260512_000107_create_hostedsms_integration_settings.sql` | Created | HostedSMS settings table and base integration seed. | +| `src/Modules/Settings/HostedSmsApiClient.php` | Created | HostedSMS SimpleAPI POST client. | +| `src/Modules/Settings/HostedSmsIntegrationRepository.php` | Created | Single-row settings repository with encrypted password. | +| `src/Modules/Settings/HostedSmsIntegrationController.php` | Created | Settings save and real test SMS actions. | +| `resources/views/settings/hostedsms.php` | Created | HostedSMS settings and test UI. | +| `routes/web.php` | Modified | DI wiring and HostedSMS routes. | +| `src/Modules/Settings/IntegrationsHubController.php` | Modified | HostedSMS row in integrations hub. | +| `resources/lang/pl.php` | Modified | Polish UI labels/messages. | +| `DOCS/DB_SCHEMA.md` | Modified | Schema documentation. | +| `DOCS/ARCHITECTURE.md` | Modified | Architecture documentation. | +| `DOCS/TECH_CHANGELOG.md` | Modified | Technical changelog. | +| `.paul/codebase/*` | Modified | PAUL codebase docs mirror. | +| `.paul/PROJECT.md` | Modified | Phase 116 marked as shipped. | +| `.paul/ROADMAP.md` | Modified | Phase 116 marked complete. | +| `.paul/STATE.md` | Modified | Loop closed and next action updated. | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| One HostedSMS account | User confirmed one account is enough for first version. | Simpler table and UI; no account selector needed. | +| Real SMS test | User requested actual test SMS and SimpleAPI has no ping endpoint. | Test consumes provider credits but verifies the real path. | +| Editable test message | User requested editable message. | UI exposes a textarea with default `Test orderPRO HostedSMS`. | +| Defer inbound replies | HostedSMS requires inbound interface activation by provider. | Future phase can add `GetUnreadInputSmses` / `GetInputSmses` polling after activation. | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Scope additions | 1 | Cosmetic improvement to last-test panel after user feedback. | +| Deferred | 1 | Inbound SMS reply handling deferred. | + +### Auto-fixed Issues + +**1. Last test display too raw** +- **Found during:** User verification after successful SMS. +- **Issue:** UI showed `MessageId: ...` as plain text. +- **Fix:** Split the persisted MessageId into a separate `Identyfikator wiadomości` row inside a status alert. +- **Files:** `resources/views/settings/hostedsms.php`, `resources/lang/pl.php`, changelog docs. +- **Verification:** PHP lint and user accepted: "jest ok". + +### Deferred Items + +- HostedSMS inbound replies: Requires provider-side activation of the inbound/Webserwis2SMS feature, then a future plan can poll/store inbound messages. + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Local migration initially blocked because MySQL refused connection | User later confirmed working test in app, so DB/runtime path is verified. | +| `sonar-scanner` unavailable in PATH | Documented as skill gap; not blocking UNIFY. | +| `vendor/bin/phpunit` missing | Documented; no PHPUnit run. | + +## Verification Results + +- PASS: PHP lint for new/changed PHP files and view/lang files. +- PASS: `npm run build --if-present`. +- PASS: `git diff --check` with line-ending warnings only. +- PASS: User confirmed real HostedSMS test SMS: `2026-05-12 12:03:22 OK HTTP 200`, MessageId `d935d71a-d9a0-4cfb-be06-03fe36c71150`. +- GAP: `sonar-scanner` not available in PATH. +- GAP: PHPUnit unavailable because `vendor/bin/phpunit` is missing. + +## Skill Audit + +| Expected | Invoked | Notes | +|----------|---------|-------| +| sonar-scanner | Gap | CLI not found in PATH during APPLY verification. | + +## Next Phase Readiness + +**Ready:** +- HostedSMS credentials can be saved and verified. +- Future SMS automation can reuse `HostedSmsIntegrationRepository::getCredentials()` and `HostedSmsApiClient::sendSms()`. + +**Concerns:** +- Inbound SMS replies require HostedSMS/DCS activation before implementation. +- No persistent SMS send history exists yet beyond `integrations.last_test_*`. + +**Blockers:** +- None for closing Phase 116. + +--- +*Phase: 116-hostedsms-integration, Plan: 01* +*Completed: 2026-05-12* diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index 35b9e54..4bc58be 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -149,6 +149,25 @@ tests/ bootstrap.php PSR-4 autoloader for tests ``` +## Phase 116 - HostedSMS Integration Settings + +### HostedSmsIntegrationRepository (`src/Modules/Settings/HostedSmsIntegrationRepository.php`) +- Zarzadza pojedynczym rekordem `hostedsms_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `hostedsms`. +- Szyfruje haslo przez `IntegrationSecretCipher`; formularz widzi tylko flage `has_password`. +- Udostepnia `getCredentials()` dla kontrolera testowej wysylki SMS. + +### HostedSmsApiClient (`src/Modules/Settings/HostedSmsApiClient.php`) +- Wykonuje `POST https://api.hostedsms.pl/SimpleApi` jako `application/x-www-form-urlencoded`. +- Wysyla `UserEmail`, `Password`, `Sender`, `Phone`, `Message` oraz opcjonalnie `ConvertMessageToGSM7`. +- Traktuje `MessageId` jako sukces, a `ErrorMessage` jako blad biznesowy nawet przy HTTP 200. + +### HostedSmsIntegrationController (`src/Modules/Settings/HostedSmsIntegrationController.php`) +- Endpointy: `GET /settings/integrations/hostedsms`, `POST /settings/integrations/hostedsms/save`, `POST /settings/integrations/hostedsms/test`. +- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`. + +### IntegrationsHubController +- Dodaje wiersz HostedSMS 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 839b05c..7d02f44 100644 --- a/DOCS/DB_SCHEMA.md +++ b/DOCS/DB_SCHEMA.md @@ -558,6 +558,22 @@ UNIQUE: `(type, name)` --- +**hostedsms_integration_settings** - HostedSMS account credentials (Phase 116; fixed 1 row) +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| `id` | TINYINT UNSIGNED | NO | PK, always 1 | +| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK -> integrations(id) CASCADE | +| `user_email` | VARCHAR(190) | YES | HostedSMS login | +| `password_encrypted` | TEXT | YES | AES-encrypted via `IntegrationSecretCipher` | +| `sender` | VARCHAR(32) | YES | HostedSMS sender name / nadpis | +| `convert_message_to_gsm7` | TINYINT(1) | NO | DEFAULT 0 | +| `created_at` | DATETIME | NO | | +| `updated_at` | DATETIME | NO | | + +UNIQUE: `(integration_id)` - one global HostedSMS settings row. + +--- + ## Accounting / Receipts **receipt_configs** — Receipt generation configurations diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md index a9c5a13..256a658 100644 --- a/DOCS/TECH_CHANGELOG.md +++ b/DOCS/TECH_CHANGELOG.md @@ -1,5 +1,20 @@ # Technical Changelog +## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS + +**Co zrobiono:** +- Dodano migracje `20260512_000107_create_hostedsms_integration_settings.sql` z pojedyncza konfiguracja `hostedsms_integration_settings` i bazowym wpisem `integrations` typu `hostedsms`. +- Dodano `HostedSmsIntegrationRepository` z szyfrowaniem hasla przez `IntegrationSecretCipher`. +- Dodano `HostedSmsApiClient` dla HostedSMS SimpleAPI (`POST https://api.hostedsms.pl/SimpleApi`). +- Dodano `HostedSmsIntegrationController` i trasy `/settings/integrations/hostedsms`, `/save`, `/test`. +- Dodano widok `resources/views/settings/hostedsms.php` z konfiguracja i realna wysylka testowego SMS z edytowalna trescia oraz czytelnym panelem ostatniego testu (`OK`, HTTP, MessageId). +- Dodano HostedSMS do hubu integracji `/settings/integrations`. + +**Dlaczego:** +- Operator potrzebuje najpierw zapisac dane HostedSMS i sprawdzic realna wysylke SMS, zanim integracja zostanie wykorzystana w automatyzacjach lub komunikacji z klientami. +- Test uzywa rzeczywistej wysylki, bo SimpleAPI nie udostepnia osobnego endpointu ping/test. +- Haslo nie jest ujawniane po zapisie; UI pokazuje tylko status zapisanego sekretu. + ## 2026-04-28 - Phase 110 Plan 01: Statistics Summary **Co zrobiono:** diff --git a/database/migrations/20260512_000107_create_hostedsms_integration_settings.sql b/database/migrations/20260512_000107_create_hostedsms_integration_settings.sql new file mode 100644 index 0000000..7071e46 --- /dev/null +++ b/database/migrations/20260512_000107_create_hostedsms_integration_settings.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS `hostedsms_integration_settings` ( + `id` TINYINT UNSIGNED NOT NULL PRIMARY KEY, + `integration_id` INT UNSIGNED NULL, + `user_email` VARCHAR(190) NULL, + `password_encrypted` TEXT NULL, + `sender` VARCHAR(32) NULL, + `convert_message_to_gsm7` 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 `hostedsms_integration_settings_integration_unique` (`integration_id`), + CONSTRAINT `hostedsms_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 ('hostedsms', 'HostedSMS', 'https://api.hostedsms.pl/SimpleApi', 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 `hostedsms_integration_settings` (`id`, `integration_id`, `created_at`, `updated_at`) +SELECT 1, `id`, NOW(), NOW() +FROM `integrations` +WHERE `type` = 'hostedsms' AND `name` = 'HostedSMS' +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 23a45aa..020ccc1 100644 --- a/resources/lang/pl.php +++ b/resources/lang/pl.php @@ -561,6 +561,7 @@ return [ 'apaczka' => 'Apaczka', 'inpost' => 'InPost', 'shoppro' => 'shopPRO', + 'hostedsms' => 'HostedSMS', 'shoppro_instances' => ':count instancji', ], 'status' => [ @@ -716,6 +717,51 @@ return [ 'test_failed' => 'Nie udalo sie polaczyc z API Apaczka.', ], ], + 'hostedsms' => [ + 'title' => 'Integracja HostedSMS', + 'description' => 'Konfiguracja konta HostedSMS do wysylki SMS z orderPRO.', + 'config' => [ + 'title' => 'Konfiguracja API', + ], + 'test' => [ + 'title' => 'Test wysylki SMS', + 'description' => 'Test realnie wysyla SMS przez HostedSMS SimpleAPI.', + ], + 'fields' => [ + 'user_email' => 'UserEmail / login', + 'password' => 'Haslo', + 'sender' => 'Sender / nadpis', + 'convert_message_to_gsm7' => 'Konwertuj tresc do GSM7', + 'is_active' => 'Integracja aktywna', + 'test_phone' => 'Numer testowy', + 'test_message' => 'Tresc testowego SMS', + ], + 'password' => [ + 'saved' => 'Haslo jest zapisane. Pozostaw pole puste, aby nie zmieniac.', + 'missing' => 'Brak zapisanego hasla HostedSMS.', + ], + 'hints' => [ + 'sender' => 'Nadpis musi byc aktywny po stronie HostedSMS.', + ], + 'status' => [ + 'password' => 'Haslo', + 'active' => 'Aktywna', + 'saved' => 'zapisane', + 'missing' => 'brak', + 'last_test' => 'Ostatni test', + 'message_id' => 'Identyfikator wiadomości', + ], + 'actions' => [ + 'save' => 'Zapisz ustawienia HostedSMS', + 'send_test' => 'Wyslij testowy SMS', + ], + 'flash' => [ + 'saved' => 'Ustawienia HostedSMS zostaly zapisane.', + 'save_failed' => 'Nie udalo sie zapisac ustawien HostedSMS.', + 'test_success' => 'Testowy SMS zostal przyjety przez HostedSMS. 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/views/settings/hostedsms.php b/resources/views/settings/hostedsms.php new file mode 100644 index 0000000..33280a3 --- /dev/null +++ b/resources/views/settings/hostedsms.php @@ -0,0 +1,126 @@ + + +
+

+

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

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

+

+ +
+ + + + + + +
+ +
+
+ + +
+
+ : + + + + + + HTTP + +
+ +
+ : + +
+ +
+ +
+ +
diff --git a/routes/web.php b/routes/web.php index cd71167..95c76b0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -32,6 +32,9 @@ use App\Modules\Settings\CarrierDeliveryMethodMappingRepository; use App\Modules\Settings\FakturowniaApiClient; use App\Modules\Settings\FakturowniaIntegrationController; use App\Modules\Settings\FakturowniaIntegrationRepository; +use App\Modules\Settings\HostedSmsApiClient; +use App\Modules\Settings\HostedSmsIntegrationController; +use App\Modules\Settings\HostedSmsIntegrationRepository; use App\Modules\Settings\InpostIntegrationController; use App\Modules\Settings\InpostIntegrationRepository; use App\Modules\Settings\IntegrationsHubController; @@ -190,6 +193,18 @@ return static function (Application $app): void { $fakturowniaApiClient, new IntegrationsRepository($app->db()) ); + $hostedSmsIntegrationRepository = new HostedSmsIntegrationRepository( + $app->db(), + (string) $app->config('app.integrations.secret', '') + ); + $hostedSmsIntegrationController = new HostedSmsIntegrationController( + $template, + $translator, + $auth, + $hostedSmsIntegrationRepository, + new HostedSmsApiClient(), + new IntegrationsRepository($app->db()) + ); $integrationsHubController = new IntegrationsHubController( $template, $translator, @@ -199,7 +214,8 @@ return static function (Application $app): void { $apaczkaIntegrationRepository, $inpostIntegrationRepository, $shopproIntegrationsRepository, - $fakturowniaIntegrationRepository + $fakturowniaIntegrationRepository, + $hostedSmsIntegrationRepository ); $cronSettingsController = new CronSettingsController( $template, @@ -543,6 +559,9 @@ return static function (Application $app): void { $router->post('/settings/integrations/fakturownia/save', [$fakturowniaIntegrationController, 'save'], [$authMiddleware]); $router->post('/settings/integrations/fakturownia/test', [$fakturowniaIntegrationController, 'test'], [$authMiddleware]); $router->post('/settings/integrations/fakturownia/delete', [$fakturowniaIntegrationController, 'delete'], [$authMiddleware]); + $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/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/HostedSmsApiClient.php b/src/Modules/Settings/HostedSmsApiClient.php new file mode 100644 index 0000000..e16880a --- /dev/null +++ b/src/Modules/Settings/HostedSmsApiClient.php @@ -0,0 +1,125 @@ + trim($userEmail), + 'Password' => $password, + 'Sender' => trim($sender), + 'Phone' => trim($phone), + 'Message' => $message, + ]; + + if ($convertMessageToGsm7) { + $payload['ConvertMessageToGSM7'] = 'true'; + } + + [$body, $httpCode, $curlError] = $this->postForm($payload); + if ($curlError !== null) { + return [ + 'ok' => false, + 'http_code' => $httpCode, + 'message' => 'Blad polaczenia: ' . $curlError, + 'message_id' => '', + ]; + } + + $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 HostedSMS: ' . 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, + ]; + } + + $errorMessage = trim((string) ($decoded['ErrorMessage'] ?? '')); + if ($errorMessage === '') { + $errorMessage = 'HTTP ' . $httpCode; + } + + return [ + 'ok' => false, + 'http_code' => $httpCode, + 'message' => $errorMessage, + 'message_id' => '', + ]; + } + + /** + * @param array $payload + * @return array{0: string, 1: int, 2: ?string} + */ + private function postForm(array $payload): array + { + $ch = curl_init(self::API_URL); + if ($ch === false) { + return ['', 0, 'Nie udalo sie zainicjowac cURL.']; + } + + $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 => [ + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8', + 'User-Agent: orderPRO/1.0', + ], + ]; + + $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]; + } +} diff --git a/src/Modules/Settings/HostedSmsIntegrationController.php b/src/Modules/Settings/HostedSmsIntegrationController.php new file mode 100644 index 0000000..fe1f6fe --- /dev/null +++ b/src/Modules/Settings/HostedSmsIntegrationController.php @@ -0,0 +1,154 @@ +template->render('settings/hostedsms', [ + 'title' => $this->translator->get('settings.hostedsms.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('hostedsms_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([ + 'user_email' => (string) $request->input('user_email', ''), + 'password' => (string) $request->input('password', ''), + 'sender' => (string) $request->input('sender', ''), + 'convert_message_to_gsm7' => $request->input('convert_message_to_gsm7', ''), + 'is_active' => $request->input('is_active', ''), + ]); + Flash::set('settings_success', $this->translator->get('settings.hostedsms.flash.saved')); + } catch (Throwable $exception) { + Flash::set( + 'settings_error', + $this->translator->get('settings.hostedsms.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 konfiguracje HostedSMS.'); + } + + $result = $this->apiClient->sendSms( + $credentials['user_email'], + $credentials['password'], + $credentials['sender'], + $phone, + $message, + $credentials['convert_message_to_gsm7'] + ); + + $status = $result['ok'] ? 'ok' : 'fail'; + $this->integrations->updateTestResult( + $credentials['integration_id'], + $status, + (int) $result['http_code'], + (string) $result['message'] + ); + + if ($result['ok']) { + Flash::set('hostedsms_test', $this->translator->get('settings.hostedsms.flash.test_success', [ + 'message_id' => (string) $result['message_id'], + ])); + } else { + Flash::set('settings_error', $this->translator->get('settings.hostedsms.flash.test_failed') . ' ' . $result['message']); + } + } catch (Throwable $exception) { + Flash::set('settings_error', $this->translator->get('settings.hostedsms.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/hostedsms'), + ['/settings/integrations'], + '/settings/integrations/hostedsms' + ); + } + + 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 miedzynarodowym, np. 48xxxxxxxxx.'); + } + + return $phone; + } + + private function validateMessage(string $value): string + { + $message = trim($value); + if ($message === '') { + throw new IntegrationConfigException('Podaj tresc testowego SMS.'); + } + if (strlen($message) > 4000) { + throw new IntegrationConfigException('Tresc SMS nie moze przekraczac 4000 znakow.'); + } + + return $message; + } +} diff --git a/src/Modules/Settings/HostedSmsIntegrationRepository.php b/src/Modules/Settings/HostedSmsIntegrationRepository.php new file mode 100644 index 0000000..16a7441 --- /dev/null +++ b/src/Modules/Settings/HostedSmsIntegrationRepository.php @@ -0,0 +1,208 @@ +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); + $passwordEncrypted = $this->resolvePasswordEncrypted($row, $integration); + + return [ + 'integration_id' => $integrationId, + 'user_email' => trim((string) ($row['user_email'] ?? '')), + 'sender' => trim((string) ($row['sender'] ?? '')), + 'convert_message_to_gsm7' => !empty($row['convert_message_to_gsm7']), + 'has_password' => $passwordEncrypted !== null && $passwordEncrypted !== '', + '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->fetchRow(); + if ($row === null) { + throw new IntegrationConfigException('Brak rekordu konfiguracji HostedSMS.'); + } + + $userEmail = trim((string) ($payload['user_email'] ?? '')); + if (!filter_var($userEmail, FILTER_VALIDATE_EMAIL)) { + throw new IntegrationConfigException('Podaj poprawny login e-mail HostedSMS.'); + } + + $sender = trim((string) ($payload['sender'] ?? '')); + if ($sender === '' || strlen($sender) > 32) { + throw new IntegrationConfigException('Podaj nadpis Sender HostedSMS (maks. 32 znaki).'); + } + + $currentEncrypted = $this->resolvePasswordEncrypted($row, $this->integrations->findById($integrationId)); + $password = trim((string) ($payload['password'] ?? '')); + $nextEncrypted = $currentEncrypted; + if ($password !== '') { + $nextEncrypted = $this->cipher->encrypt($password); + } + + if ($nextEncrypted === null || $nextEncrypted === '') { + throw new IntegrationConfigException('Podaj haslo HostedSMS.'); + } + + $statement = $this->pdo->prepare( + 'UPDATE hostedsms_integration_settings + SET user_email = :user_email, + password_encrypted = :password_encrypted, + sender = :sender, + convert_message_to_gsm7 = :convert_message_to_gsm7, + updated_at = NOW() + WHERE id = 1' + ); + $statement->execute([ + 'user_email' => $userEmail, + 'password_encrypted' => $nextEncrypted, + 'sender' => $sender, + 'convert_message_to_gsm7' => !empty($payload['convert_message_to_gsm7']) ? 1 : 0, + ]); + + $this->updateIntegrationActive($integrationId, !empty($payload['is_active'])); + $this->integrations->updateApiKeyEncrypted($integrationId, $nextEncrypted); + } + + /** + * @return array{integration_id: int, user_email: string, password: string, sender: string, convert_message_to_gsm7: bool}|null + */ + public function getCredentials(): ?array + { + $this->ensureRow(); + $integrationId = $this->ensureBaseIntegration(); + $row = $this->fetchRow(); + if ($row === null) { + return null; + } + + $userEmail = trim((string) ($row['user_email'] ?? '')); + $sender = trim((string) ($row['sender'] ?? '')); + $encrypted = $this->resolvePasswordEncrypted($row, $this->integrations->findById($integrationId)); + + if ($userEmail === '' || $sender === '' || $encrypted === null || $encrypted === '') { + return null; + } + + $password = trim($this->cipher->decrypt($encrypted)); + if ($password === '') { + return null; + } + + return [ + 'integration_id' => $integrationId, + 'user_email' => $userEmail, + 'password' => $password, + 'sender' => $sender, + 'convert_message_to_gsm7' => !empty($row['convert_message_to_gsm7']), + ]; + } + + 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 hostedsms_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 hostedsms_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; + } + + /** + * @param array|null $row + * @param array|null $integration + */ + private function resolvePasswordEncrypted(?array $row, ?array $integration): ?string + { + $settingsValue = trim((string) ($row['password_encrypted'] ?? '')); + if ($settingsValue !== '') { + return $settingsValue; + } + + $baseValue = trim((string) ($integration['api_key_encrypted'] ?? '')); + return StringHelper::nullableString($baseValue); + } + + 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, + ]); + } +} diff --git a/src/Modules/Settings/IntegrationsHubController.php b/src/Modules/Settings/IntegrationsHubController.php index ed78821..0ef3070 100644 --- a/src/Modules/Settings/IntegrationsHubController.php +++ b/src/Modules/Settings/IntegrationsHubController.php @@ -22,7 +22,8 @@ final class IntegrationsHubController private readonly ApaczkaIntegrationRepository $apaczka, private readonly InpostIntegrationRepository $inpost, private readonly ShopproIntegrationsRepository $shoppro, - private readonly FakturowniaIntegrationRepository $fakturownia + private readonly FakturowniaIntegrationRepository $fakturownia, + private readonly HostedSmsIntegrationRepository $hostedSms ) { } @@ -35,6 +36,7 @@ final class IntegrationsHubController $this->buildInpostRow(), $this->buildShopproRow(), $this->buildFakturowniaRow(), + $this->buildHostedSmsRow(), ]; $html = $this->template->render('settings/integrations', [ @@ -214,4 +216,30 @@ final class IntegrationsHubController ]; } + /** + * @return array + */ + private function buildHostedSmsRow(): array + { + $settings = $this->hostedSms->getSettings(); + $isConfigured = !empty($settings['user_email']) + && !empty($settings['sender']) + && !empty($settings['has_password']); + + return [ + 'provider' => $this->translator->get('settings.integrations_hub.providers.hostedsms'), + 'instance' => 'HostedSMS', + 'authorization_status' => $isConfigured + ? $this->translator->get('settings.integrations_hub.status.configured') + : $this->translator->get('settings.integrations_hub.status.not_configured'), + 'secret_status' => !empty($settings['has_password']) + ? $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/hostedsms', + 'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'), + ]; + } + }