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 <noreply@openai.com>
This commit is contained in:
2026-05-12 12:25:07 +02:00
parent adacb65110
commit bc2ed2c8e2
20 changed files with 1282 additions and 56 deletions

View File

@@ -13,8 +13,8 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
| Attribute | Value | | Attribute | Value |
|-----------|-------| |-----------|-------|
| Version | 3.7.0-dev | | Version | 3.7.0-dev |
| Status | v3.7 in progress — Phases 113 (Fakturownia Foundation) + 114 (Accounting Configs Refactor) + 115 (Wystawianie faktury z zamowienia) shipped | | Status | v3.7 in progress — Phases 113-116 shipped (Fakturownia + HostedSMS settings/test SMS) |
| Last Updated | 2026-05-10 | | Last Updated | 2026-05-12 |
## Requirements ## 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] 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] 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] 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 ### Deferred
@@ -127,7 +128,7 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
### Active (In Progress) ### 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) ### 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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* *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*

View File

@@ -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) | | 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) | | 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) | | 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): Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania):
- Eksport XLSX listy wystawionych faktur (analogicznie do paragonow) - Eksport XLSX listy wystawionych faktur (analogicznie do paragonow)
- Idempotencja podwojnego POST do Fakturowni (INVOICE-IDEMP-115) - Idempotencja podwojnego POST do Fakturowni (INVOICE-IDEMP-115)
- Event automatyzacji `invoice.created` (jezeli operator chce wysylac faktury mailem) - 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) - Backfill `curl_close()` w `ShopproIntegrationsRepository` (PHP 8.5 compat, poza zakresem 115)
## Next Milestone ## Next Milestone
@@ -494,4 +496,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
--- ---
*Roadmap created: 2026-03-12* *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*

View File

@@ -5,79 +5,55 @@
See: .paul/PROJECT.md (updated 2026-05-07) 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. **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 ## Current Position
Milestone: v3.7 Invoices (Fakturownia integration) In progress Milestone: v3.7 Invoices (Fakturownia integration) - In progress
Phase: 116 of TBD (TBD — kandydaci v3.7 lub przejscie na kolejny milestone) Not started Phase: 117 of TBD (next candidate) - Not started
Plan: pending Plan: pending
Status: Phase 115 closed; transition done; ready to plan kolejna faze Status: Phase 116 complete; ready to plan next phase
Last activity: 2026-05-10 — UNIFY 115-01 complete + transition: PROJECT.md/ROADMAP.md/changelog zaktualizowane Last activity: 2026-05-12 - UNIFY 116-01 complete and transition done
Progress: Progress:
- Milestone v3.7: [██████░░░░] ~55% (Phase 113 + 114 + 115 zamkniete; kandydaci: XLSX invoices export, INVOICE-IDEMP-115, invoice.created event, curl_close shopPRO backfill) - Milestone v3.7: [########--] ~75% (Phase 113 + 114 + 115 + 116 closed)
- Phase 115: [██████████] 100% Complete - Phase 116: [##########] 100% - Complete
## Loop Position ## Loop Position
Current loop state: Current loop state:
``` ```
v3.7 milestone: PLAN -> APPLY -> UNIFY
Phase 113 (Fakturownia Integration Foundation): Complete done done done [Loop complete - ready for next PLAN]
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]
``` ```
## Session Continuity ## Session Continuity
Last session: 2026-05-10 Last session: 2026-05-12
Stopped at: Phase 115 transition complete (PROJECT.md + ROADMAP.md updated, SUMMARY zapisany, changelog zaktualizowany) Stopped at: Phase 116 complete, ready to plan next phase
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) Next action: $paul-plan for next v3.7 candidate or next milestone
Resume file: .paul/phases/115-invoice-from-order/115-01-SUMMARY.md Resume file: .paul/phases/116-hostedsms-integration/116-01-SUMMARY.md
## Git State ## 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 Branch: main
Feature branches merged: none
## Pending Actions ## 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).
- 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).
- Backfill zamowienia #882 — operator robi recznie po wdrozeniu (poza zakresem planu) - Uruchom migracje gdy XAMPP online: `php bin/migrate.php` (delivery_statuses).
- Uruchom migracje gdy XAMPP online: `php bin/migrate.php` (delivery_statuses) - Recznie odtworzyc istniejace reguly automatyzacji z grupowymi kluczami (BREAKING z 108-02).
- 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 ## Deferred to Next Milestones
- Phase 68 - Code Deduplication Refactor (0/2 Planning, nigdy nie rozpoczety) - 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`) - 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) - Mobile Orders List / Mobile Order Details / Mobile Settings.
- 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).
- 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 after APPLY; Phase 116 gap documented because CLI was not available in PATH.
|----------|---------|-------|
| 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.

View File

@@ -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`

View File

@@ -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 ## Phase 114 — Accounting Configs Refactor
### Sekcja Ksiegowosc — struktura URL ### Sekcja Ksiegowosc — struktura URL

View File

@@ -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 ## Accounting / Receipts
**receipt_configs** — Receipt generation configurations **receipt_configs** — Receipt generation configurations

View File

@@ -1,5 +1,20 @@
# Technical Changelog # 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 ## 2026-05-10 - Phase 115 Plan 01: Wystawianie faktury z zamowienia
**Co zrobiono:** **Co zrobiono:**

View File

@@ -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
---
<objective>
## 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.
</objective>
<context>
<clarifications>
- **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.
</clarifications>
## 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
</context>
<skills>
## 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.
</skills>
<acceptance_criteria>
## 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Dodac model konfiguracji HostedSMS</name>
<files>database/migrations/20260512_000107_create_hostedsms_integration_settings.sql, src/Modules/Settings/HostedSmsIntegrationRepository.php</files>
<action>
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.
</action>
<verify>C:\xampp\php\php.exe -l src/Modules/Settings/HostedSmsIntegrationRepository.php</verify>
<done>AC-1 i AC-2 spelnione dla warstwy zapisu konfiguracji.</done>
</task>
<task type="auto">
<name>Task 2: Dodac klienta SimpleAPI i kontroler ustawien</name>
<files>src/Modules/Settings/HostedSmsApiClient.php, src/Modules/Settings/HostedSmsIntegrationController.php, routes/web.php</files>
<action>
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`.
</action>
<verify>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</verify>
<done>AC-2 i AC-3 spelnione dla backendu i realnej wysylki testowej.</done>
</task>
<task type="auto">
<name>Task 3: Dodac UI, hub integracji i dokumentacje</name>
<files>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</files>
<action>
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.
</action>
<verify>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</verify>
<done>AC-4 i AC-5 spelnione dla UI, hubu i dokumentacji.</done>
</task>
</tasks>
<boundaries>
## 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.
</boundaries>
<verification>
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
</verification>
<success_criteria>
- 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.
</success_criteria>
<output>
After completion, create `.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md`
</output>

View File

@@ -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*

View File

@@ -149,6 +149,25 @@ tests/
bootstrap.php PSR-4 autoloader for 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 ## Phase 108 — Delivery Status Management
### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`) ### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`)

View File

@@ -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 ## Accounting / Receipts
**receipt_configs** — Receipt generation configurations **receipt_configs** — Receipt generation configurations

View File

@@ -1,5 +1,20 @@
# Technical Changelog # 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 ## 2026-04-28 - Phase 110 Plan 01: Statistics Summary
**Co zrobiono:** **Co zrobiono:**

View File

@@ -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`);

View File

@@ -561,6 +561,7 @@ return [
'apaczka' => 'Apaczka', 'apaczka' => 'Apaczka',
'inpost' => 'InPost', 'inpost' => 'InPost',
'shoppro' => 'shopPRO', 'shoppro' => 'shopPRO',
'hostedsms' => 'HostedSMS',
'shoppro_instances' => ':count instancji', 'shoppro_instances' => ':count instancji',
], ],
'status' => [ 'status' => [
@@ -716,6 +717,51 @@ return [
'test_failed' => 'Nie udalo sie polaczyc z API Apaczka.', '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' => [ 'inpost' => [
'title' => 'Integracja InPost', 'title' => 'Integracja InPost',
'description' => 'Konfiguracja polaczenia z API InPost ShipX do obslugi przesylek.', 'description' => 'Konfiguracja polaczenia z API InPost ShipX do obslugi przesylek.',

View File

@@ -0,0 +1,126 @@
<?php
$settings = is_array($settings ?? null) ? $settings : [];
$userEmail = trim((string) ($settings['user_email'] ?? ''));
$sender = trim((string) ($settings['sender'] ?? ''));
$hasPassword = (bool) ($settings['has_password'] ?? false);
$isActive = (bool) ($settings['is_active'] ?? true);
$convertToGsm7 = (bool) ($settings['convert_message_to_gsm7'] ?? false);
$lastTestAt = trim((string) ($settings['last_test_at'] ?? ''));
$lastTestStatus = trim((string) ($settings['last_test_status'] ?? ''));
$lastTestMessage = trim((string) ($settings['last_test_message'] ?? ''));
$lastTestHttpCode = $settings['last_test_http_code'] ?? null;
$lastMessageId = '';
if (str_starts_with($lastTestMessage, 'MessageId:')) {
$lastMessageId = trim(substr($lastTestMessage, strlen('MessageId:')));
}
?>
<section class="card">
<h2 class="section-title"><?= $e($t('settings.hostedsms.title')) ?></h2>
<p class="muted mt-12"><?= $e($t('settings.hostedsms.description')) ?></p>
<?php if (!empty($errorMessage)): ?>
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
<?php endif; ?>
<?php if (!empty($successMessage)): ?>
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
<?php endif; ?>
<?php if (!empty($testMessage)): ?>
<div class="alert alert--info mt-12" role="status"><?= $e((string) $testMessage) ?></div>
<?php endif; ?>
</section>
<section class="card mt-16">
<h3 class="section-title"><?= $e($t('settings.hostedsms.config.title')) ?></h3>
<div class="muted mt-12">
<?= $e($t('settings.hostedsms.status.password')) ?>:
<strong><?= $e($hasPassword ? $t('settings.hostedsms.status.saved') : $t('settings.hostedsms.status.missing')) ?></strong>
|
<?= $e($t('settings.hostedsms.status.active')) ?>:
<strong><?= $e($isActive ? $t('settings.integrations_hub.active.yes') : $t('settings.integrations_hub.active.no')) ?></strong>
</div>
<form class="statuses-form mt-16" action="/settings/integrations/hostedsms/save" method="post" novalidate>
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<label class="form-field">
<span class="field-label"><?= $e($t('settings.hostedsms.fields.user_email')) ?></span>
<input class="form-control" type="email" name="user_email" maxlength="190" value="<?= $e($userEmail) ?>" required>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('settings.hostedsms.fields.password')) ?></span>
<input class="form-control" type="password" name="password" autocomplete="new-password" placeholder="<?= $hasPassword ? '********' : '' ?>" <?= $hasPassword ? '' : 'required' ?>>
<span class="muted"><?= $e($hasPassword ? $t('settings.hostedsms.password.saved') : $t('settings.hostedsms.password.missing')) ?></span>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('settings.hostedsms.fields.sender')) ?></span>
<input class="form-control" type="text" name="sender" maxlength="32" value="<?= $e($sender) ?>" required>
<span class="muted"><?= $e($t('settings.hostedsms.hints.sender')) ?></span>
</label>
<label class="form-field form-field--inline">
<input type="checkbox" name="convert_message_to_gsm7" value="1"<?= $convertToGsm7 ? ' checked' : '' ?>>
<span class="field-label"><?= $e($t('settings.hostedsms.fields.convert_message_to_gsm7')) ?></span>
</label>
<label class="form-field form-field--inline">
<input type="checkbox" name="is_active" value="1"<?= $isActive ? ' checked' : '' ?>>
<span class="field-label"><?= $e($t('settings.hostedsms.fields.is_active')) ?></span>
</label>
<div class="form-actions mt-16">
<button type="submit" class="btn btn--primary"><?= $e($t('settings.hostedsms.actions.save')) ?></button>
</div>
</form>
</section>
<section class="card mt-16">
<h3 class="section-title"><?= $e($t('settings.hostedsms.test.title')) ?></h3>
<p class="muted mt-12"><?= $e($t('settings.hostedsms.test.description')) ?></p>
<form class="statuses-form mt-16" action="/settings/integrations/hostedsms/test" method="post" novalidate>
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<label class="form-field">
<span class="field-label"><?= $e($t('settings.hostedsms.fields.test_phone')) ?></span>
<input class="form-control" type="tel" name="phone" inputmode="tel" placeholder="48xxxxxxxxx" required>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('settings.hostedsms.fields.test_message')) ?></span>
<textarea class="form-control" name="message" rows="4" maxlength="4000" required>Test orderPRO HostedSMS</textarea>
</label>
<div class="form-actions mt-16">
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.hostedsms.actions.send_test')) ?></button>
</div>
</form>
<?php if ($lastTestAt !== ''): ?>
<div class="alert <?= $lastTestStatus === 'ok' ? 'alert--success' : 'alert--danger' ?> mt-16" role="status">
<div>
<strong><?= $e($t('settings.hostedsms.status.last_test')) ?>:</strong>
<?= $e($lastTestAt) ?>
<?php if ($lastTestStatus !== ''): ?>
<span class="badge badge--<?= $lastTestStatus === 'ok' ? 'success' : 'muted' ?>"><?= $e(strtoupper($lastTestStatus)) ?></span>
<?php endif; ?>
<?php if ($lastTestHttpCode !== null): ?>
<span class="badge badge--muted">HTTP <?= $e((string) $lastTestHttpCode) ?></span>
<?php endif; ?>
</div>
<?php if ($lastMessageId !== ''): ?>
<div class="mt-12">
<?= $e($t('settings.hostedsms.status.message_id')) ?>:
<code><?= $e($lastMessageId) ?></code>
</div>
<?php elseif ($lastTestMessage !== ''): ?>
<div class="mt-12"><?= $e($lastTestMessage) ?></div>
<?php endif; ?>
</div>
<?php endif; ?>
</section>

View File

@@ -32,6 +32,9 @@ use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
use App\Modules\Settings\FakturowniaApiClient; use App\Modules\Settings\FakturowniaApiClient;
use App\Modules\Settings\FakturowniaIntegrationController; use App\Modules\Settings\FakturowniaIntegrationController;
use App\Modules\Settings\FakturowniaIntegrationRepository; 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\InpostIntegrationController;
use App\Modules\Settings\InpostIntegrationRepository; use App\Modules\Settings\InpostIntegrationRepository;
use App\Modules\Settings\IntegrationsHubController; use App\Modules\Settings\IntegrationsHubController;
@@ -190,6 +193,18 @@ return static function (Application $app): void {
$fakturowniaApiClient, $fakturowniaApiClient,
new IntegrationsRepository($app->db()) 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( $integrationsHubController = new IntegrationsHubController(
$template, $template,
$translator, $translator,
@@ -199,7 +214,8 @@ return static function (Application $app): void {
$apaczkaIntegrationRepository, $apaczkaIntegrationRepository,
$inpostIntegrationRepository, $inpostIntegrationRepository,
$shopproIntegrationsRepository, $shopproIntegrationsRepository,
$fakturowniaIntegrationRepository $fakturowniaIntegrationRepository,
$hostedSmsIntegrationRepository
); );
$cronSettingsController = new CronSettingsController( $cronSettingsController = new CronSettingsController(
$template, $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/save', [$fakturowniaIntegrationController, 'save'], [$authMiddleware]);
$router->post('/settings/integrations/fakturownia/test', [$fakturowniaIntegrationController, 'test'], [$authMiddleware]); $router->post('/settings/integrations/fakturownia/test', [$fakturowniaIntegrationController, 'test'], [$authMiddleware]);
$router->post('/settings/integrations/fakturownia/delete', [$fakturowniaIntegrationController, 'delete'], [$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->get('/settings/integrations/shoppro', [$shopproIntegrationsController, 'index'], [$authMiddleware]);
$router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]); $router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]);
$router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]); $router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]);

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Http\SslCertificateResolver;
final class HostedSmsApiClient
{
private const API_URL = 'https://api.hostedsms.pl/SimpleApi';
public function __construct(private readonly int $timeoutSeconds = 15)
{
}
/**
* @return array{ok: bool, http_code: int, message: string, message_id: string}
*/
public function sendSms(
string $userEmail,
string $password,
string $sender,
string $phone,
string $message,
bool $convertMessageToGsm7
): array {
$payload = [
'UserEmail' => 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<string, string> $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];
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Exceptions\IntegrationConfigException;
use App\Core\Http\RedirectPathResolver;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\I18n\Translator;
use App\Core\Security\Csrf;
use App\Core\Support\Flash;
use App\Core\View\Template;
use App\Modules\Auth\AuthService;
use Throwable;
final class HostedSmsIntegrationController
{
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
private readonly AuthService $auth,
private readonly HostedSmsIntegrationRepository $repository,
private readonly HostedSmsApiClient $apiClient,
private readonly IntegrationsRepository $integrations
) {
}
public function index(Request $request): Response
{
$html = $this->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;
}
}

View File

@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Exceptions\IntegrationConfigException;
use App\Core\Support\StringHelper;
use PDO;
use Throwable;
final class HostedSmsIntegrationRepository
{
private const INTEGRATION_TYPE = 'hostedsms';
private const INTEGRATION_NAME = 'HostedSMS';
private const INTEGRATION_BASE_URL = 'https://api.hostedsms.pl/SimpleApi';
private readonly IntegrationsRepository $integrations;
private readonly IntegrationSecretCipher $cipher;
public function __construct(
private readonly PDO $pdo,
private readonly string $secret
) {
$this->integrations = new IntegrationsRepository($this->pdo);
$this->cipher = new IntegrationSecretCipher($this->secret);
}
/**
* @return array<string, mixed>
*/
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<string, mixed> $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<string, mixed>|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<string, mixed>|null $row
* @param array<string, mixed>|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,
]);
}
}

View File

@@ -22,7 +22,8 @@ final class IntegrationsHubController
private readonly ApaczkaIntegrationRepository $apaczka, private readonly ApaczkaIntegrationRepository $apaczka,
private readonly InpostIntegrationRepository $inpost, private readonly InpostIntegrationRepository $inpost,
private readonly ShopproIntegrationsRepository $shoppro, 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->buildInpostRow(),
$this->buildShopproRow(), $this->buildShopproRow(),
$this->buildFakturowniaRow(), $this->buildFakturowniaRow(),
$this->buildHostedSmsRow(),
]; ];
$html = $this->template->render('settings/integrations', [ $html = $this->template->render('settings/integrations', [
@@ -214,4 +216,30 @@ final class IntegrationsHubController
]; ];
} }
/**
* @return array<string, mixed>
*/
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'),
];
}
} }