feat(117): smsplanet integration settings
This commit is contained in:
@@ -13,7 +13,7 @@ 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-116 shipped (Fakturownia + HostedSMS settings/test SMS) |
|
| Status | v3.7 in progress — Phases 113-117 shipped (Fakturownia + HostedSMS/SMSPLANET settings/test SMS) |
|
||||||
| Last Updated | 2026-05-12 |
|
| Last Updated | 2026-05-12 |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -120,6 +120,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
- [x] Ksiegowosc: refaktor `/settings/accounting` na hub-rozdroze + osobne podstrony `/receipts` i `/invoices` + edycja na osobnym widoku; pelen CRUD `invoice_configs` z opcja delegacji do Fakturowni (conditional integration_id, serwerowa walidacja); seed `Domyslny VAT`; globalny modul `confirm-delete.js` — Phase 114
|
- [x] 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
|
- [x] Integracja HostedSMS: pojedyncza globalna konfiguracja w `/settings/integrations/hostedsms`, szyfrowane haslo, karta w hubie integracji i realna wysylka testowego SMS z edytowalna trescia oraz czytelnym statusem MessageId — Phase 116
|
||||||
|
- [x] Integracja SMSPLANET: pojedyncza globalna konfiguracja w `/settings/integrations/smsplanet`, szyfrowane sekrety, autoryzacja Bearer token albo key + password, karta w hubie integracji i realna wysylka testowego SMS — Phase 117
|
||||||
|
|
||||||
### Deferred
|
### Deferred
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
|
|
||||||
### Active (In Progress)
|
### Active (In Progress)
|
||||||
|
|
||||||
- [ ] v3.7 Invoices / operational integrations — Phases 113 + 114 + 115 + 116 shipped; ewentualne kolejne fazy (np. eksport XLSX faktur, invoice.created event, idempotencja Fakturowni, automatyzacje SMS, odbior SMS po aktywacji HostedSMS) w kolejce.
|
- [ ] v3.7 Invoices / operational integrations — Phases 113 + 114 + 115 + 116 + 117 shipped; ewentualne kolejne fazy (np. eksport XLSX faktur, invoice.created event, idempotencja Fakturowni, automatyzacje SMS, odbior SMS po aktywacji HostedSMS) w kolejce.
|
||||||
|
|
||||||
### Planned (Next)
|
### Planned (Next)
|
||||||
|
|
||||||
@@ -198,6 +199,9 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
|||||||
| Push waybilla do Allegro checkout forms wykonywany tylko dla zamowien source=allegro i jest niekrytyczny dla lokalnego tworzenia paczki | Eliminacja recznego kroku po stronie Allegro bez ryzyka utraty lokalnie utworzonej przesylki przy bledzie API | 2026-03-28 | Active |
|
| 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 |
|
| 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 |
|
| Odbior odpowiedzi SMS z HostedSMS odlozony do osobnej fazy | Dokumentacja przewiduje metody odbioru SMS, ale wymagaja aktywacji interfejsu po stronie DCS/HostedSMS | 2026-05-12 | Deferred |
|
||||||
|
| SMSPLANET startuje jako jedna globalna konfiguracja z realnym testowym SMS | Operator potrzebowal drugiej bramki porownywalnej z HostedSMS, bez automatyzacji SMS w tej fazie | 2026-05-12 | Active |
|
||||||
|
| SMSPLANET obsluguje Bearer token oraz key + password | Dokumentacja SMSPLANET rekomenduje Bearer, ale API wspiera tez klucz i haslo; UI pozwala przetestowac oba warianty | 2026-05-12 | Active |
|
||||||
|
| Test SMSPLANET nie uzywa parametru `test=1` | Wymaganie UAT: test ma realnie wysylac SMS i zapisac wynik API w hubie integracji | 2026-05-12 | Active |
|
||||||
| Event `order.imported` emitowany tylko przy pierwszym imporcie zamowienia | Unikniecie duplikatow reakcji automatyzacji przy kolejnych synchronizacjach | 2026-04-15 | Active |
|
| 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 |
|
||||||
@@ -255,6 +259,6 @@ Quick Reference:
|
|||||||
|
|
||||||
---
|
---
|
||||||
*PROJECT.md — Updated when requirements or context change*
|
*PROJECT.md — Updated when requirements or context change*
|
||||||
*Last updated: 2026-05-12 after Phase 116 (HostedSMS Integration Settings + Test SMS) completion; v3.7 milestone in progress*
|
*Last updated: 2026-05-12 after Phase 117 (SMSPLANET Integration Settings + Test SMS) completion; v3.7 milestone in progress*
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt
|
|||||||
| 114 | Accounting Configs Refactor (hub + osobne podstrony receipts/invoices) | 1/1 | Complete (2026-05-10) |
|
| 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) |
|
| 116 | HostedSMS Integration Settings + Test SMS | 1/1 | Complete (2026-05-12) |
|
||||||
|
| 117 | SMSPLANET Integration Settings + Test SMS | 1/1 | Complete (2026-05-12; migration/manual SMS verification pending) |
|
||||||
|
|
||||||
Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania):
|
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
|
- Automatyzacje SMS / odbior odpowiedzi SMS po aktywacji HostedSMS
|
||||||
|
- Manualne potwierdzenie SMSPLANET na zywej bazie i danych produkcyjnych
|
||||||
- Backfill `curl_close()` w `ShopproIntegrationsRepository` (PHP 8.5 compat, poza zakresem 115)
|
- Backfill `curl_close()` w `ShopproIntegrationsRepository` (PHP 8.5 compat, poza zakresem 115)
|
||||||
|
|
||||||
## Next Milestone
|
## Next Milestone
|
||||||
@@ -496,4 +498,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap created: 2026-03-12*
|
*Roadmap created: 2026-03-12*
|
||||||
*Last updated: 2026-05-12 - Phase 116 (HostedSMS Integration Settings + Test SMS) complete; v3.7 milestone in progress*
|
*Last updated: 2026-05-12 - Phase 117 (SMSPLANET Integration Settings + Test SMS) complete with environment verification gaps; v3.7 milestone in progress*
|
||||||
|
|||||||
@@ -5,38 +5,38 @@
|
|||||||
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 + operational integrations - Phase 116 HostedSMS settings/test SMS complete.
|
**Current focus:** v3.7 Invoices + operational integrations - Phase 117 SMSPLANET settings/test SMS unified; migration/live SMS verification remains environment-dependent.
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v3.7 Invoices (Fakturownia integration) - In progress
|
Milestone: v3.7 Invoices (Fakturownia integration) - In progress
|
||||||
Phase: 117 of TBD (next candidate) - Not started
|
Phase: 117 of TBD (SMSPLANET Integration Settings + Test SMS) - Complete
|
||||||
Plan: pending
|
Plan: 117-01 unified
|
||||||
Status: Phase 116 complete; ready to plan next phase
|
Status: Phase 117 complete; migration/manual SMS test pending because local DB is unavailable
|
||||||
Last activity: 2026-05-12 - UNIFY 116-01 complete and transition done
|
Last activity: 2026-05-12 - Unified SMSPLANET settings/test integration
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone v3.7: [########--] ~75% (Phase 113 + 114 + 115 + 116 closed)
|
- Milestone v3.7: [########--] ~80% (Phase 113 + 114 + 115 + 116 + 117 closed; environment verification gaps documented)
|
||||||
- Phase 116: [##########] 100% - Complete
|
- Phase 117: [##########] 100% - Implementation unified; migration/live SMS verification pending externally
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN -> APPLY -> UNIFY
|
PLAN -> APPLY -> UNIFY
|
||||||
done done done [Loop complete - ready for next PLAN]
|
done done done [Phase 117 closed with environment gaps documented]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-05-12
|
Last session: 2026-05-12
|
||||||
Stopped at: Phase 116 complete, ready to plan next phase
|
Stopped at: Phase 117 unified; local migration/manual SMS verification pending
|
||||||
Next action: $paul-plan for next v3.7 candidate or next milestone
|
Next action: Start local MySQL, run `C:\xampp\php\php.exe bin\migrate.php`, verify SMSPLANET settings/test SMS, then plan the next v3.7 candidate or close the milestone.
|
||||||
Resume file: .paul/phases/116-hostedsms-integration/116-01-SUMMARY.md
|
Resume file: .paul/phases/117-smsplanet-integration/117-01-SUMMARY.md
|
||||||
|
|
||||||
## Git State
|
## Git State
|
||||||
|
|
||||||
Last phase commit: bc2ed2c feat(116): hostedsms integration settings
|
Last phase commit: feat(117): smsplanet integration settings
|
||||||
Branch: main
|
Branch: main
|
||||||
|
|
||||||
## Pending Actions
|
## Pending Actions
|
||||||
@@ -46,6 +46,7 @@ Branch: main
|
|||||||
- 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.
|
- HostedSMS inbound replies: requires DCS/HostedSMS activation before implementation.
|
||||||
|
- Phase 117 follow-up: run migration when XAMPP MySQL is online and manually test real SMSPLANET sends for Bearer token and key + password.
|
||||||
|
|
||||||
## Deferred to Next Milestones
|
## Deferred to Next Milestones
|
||||||
|
|
||||||
@@ -56,4 +57,4 @@ Branch: main
|
|||||||
|
|
||||||
## Skill Requirements
|
## Skill Requirements
|
||||||
|
|
||||||
- `sonar-scanner` required after APPLY; Phase 116 gap documented because CLI was not available in PATH.
|
- `sonar-scanner` required after APPLY; Phase 116 and Phase 117 gaps documented because CLI was not available in PATH.
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
- Poprawiono prezentacje ostatniego testu HostedSMS: status, HTTP i osobny identyfikator wiadomosci.
|
- 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`.
|
- 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.
|
- Odnotowano przyszly zakres: odbior odpowiedzi SMS wymaga aktywacji interfejsu po stronie DCS/HostedSMS.
|
||||||
|
- [Phase 117, Plan 01] Dodano integracje SMSPLANET: globalne ustawienia konta, dwie metody autoryzacji, szyfrowane sekrety, karta w hubie integracji i realna wysylka testowego SMS.
|
||||||
|
- Dodano klienta SMSPLANET (`POST https://api2.smsplanet.pl/sms`) z obsluga Bearer token oraz `key` + `password`, bez parametru `test=1` dla testow realnych.
|
||||||
|
- Poprawiono uklad checkboxow i radio buttonow na ekranie integracji SMSPLANET przez wspolny komponent SCSS.
|
||||||
|
- Odnotowano blokery weryfikacji: lokalny MySQL odmawial polaczenia, `vendor\bin\phpunit` i `sonar-scanner` nie byly dostepne.
|
||||||
|
|
||||||
## Zmienione pliki
|
## Zmienione pliki
|
||||||
|
|
||||||
@@ -18,14 +22,23 @@
|
|||||||
- `.paul/codebase/tech_changelog.md`
|
- `.paul/codebase/tech_changelog.md`
|
||||||
- `.paul/phases/116-hostedsms-integration/116-01-PLAN.md`
|
- `.paul/phases/116-hostedsms-integration/116-01-PLAN.md`
|
||||||
- `.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md`
|
- `.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md`
|
||||||
|
- `.paul/phases/117-smsplanet-integration/117-01-PLAN.md`
|
||||||
|
- `.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md`
|
||||||
- `DOCS/ARCHITECTURE.md`
|
- `DOCS/ARCHITECTURE.md`
|
||||||
- `DOCS/DB_SCHEMA.md`
|
- `DOCS/DB_SCHEMA.md`
|
||||||
- `DOCS/TECH_CHANGELOG.md`
|
- `DOCS/TECH_CHANGELOG.md`
|
||||||
- `database/migrations/20260512_000107_create_hostedsms_integration_settings.sql`
|
- `database/migrations/20260512_000107_create_hostedsms_integration_settings.sql`
|
||||||
|
- `database/migrations/20260512_000108_create_smsplanet_integration_settings.sql`
|
||||||
- `resources/lang/pl.php`
|
- `resources/lang/pl.php`
|
||||||
|
- `resources/scss/app.scss`
|
||||||
- `resources/views/settings/hostedsms.php`
|
- `resources/views/settings/hostedsms.php`
|
||||||
|
- `resources/views/settings/smsplanet.php`
|
||||||
- `routes/web.php`
|
- `routes/web.php`
|
||||||
- `src/Modules/Settings/HostedSmsApiClient.php`
|
- `src/Modules/Settings/HostedSmsApiClient.php`
|
||||||
- `src/Modules/Settings/HostedSmsIntegrationController.php`
|
- `src/Modules/Settings/HostedSmsIntegrationController.php`
|
||||||
- `src/Modules/Settings/HostedSmsIntegrationRepository.php`
|
- `src/Modules/Settings/HostedSmsIntegrationRepository.php`
|
||||||
|
- `src/Modules/Settings/IntegrationSecretCipher.php`
|
||||||
- `src/Modules/Settings/IntegrationsHubController.php`
|
- `src/Modules/Settings/IntegrationsHubController.php`
|
||||||
|
- `src/Modules/Settings/SmsplanetApiClient.php`
|
||||||
|
- `src/Modules/Settings/SmsplanetIntegrationController.php`
|
||||||
|
- `src/Modules/Settings/SmsplanetIntegrationRepository.php`
|
||||||
|
|||||||
@@ -281,6 +281,27 @@ tests/
|
|||||||
### IntegrationsHubController
|
### IntegrationsHubController
|
||||||
- Dodaje wiersz HostedSMS do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
- Dodaje wiersz HostedSMS do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
||||||
|
|
||||||
|
## Phase 117 - SMSPLANET Integration Settings
|
||||||
|
|
||||||
|
### SmsplanetIntegrationRepository (`src/Modules/Settings/SmsplanetIntegrationRepository.php`)
|
||||||
|
- Zarzadza pojedynczym rekordem `smsplanet_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `smsplanet`.
|
||||||
|
- Obsluguje dwie metody autoryzacji: Bearer token oraz `key` + `password`.
|
||||||
|
- Szyfruje token, klucz API i haslo przez `IntegrationSecretCipher`; formularz widzi tylko flagi `has_api_token`, `has_api_key` i `has_api_password`.
|
||||||
|
- Udostepnia `getCredentials()` tylko dla kompletnej i aktywnej konfiguracji testowej wysylki SMS.
|
||||||
|
|
||||||
|
### SmsplanetApiClient (`src/Modules/Settings/SmsplanetApiClient.php`)
|
||||||
|
- Wykonuje `POST https://api2.smsplanet.pl/sms` jako `application/x-www-form-urlencoded`.
|
||||||
|
- Dla Bearer token wysyla naglowek `Authorization: Bearer ...`; dla `key_password` wysyla parametry `key` i `password`.
|
||||||
|
- Wysyla `from`, `to`, `msg` oraz opcjonalnie `clear_polish` i `transactional`; test nie ustawia `test=1`, wiec wysyla realny SMS.
|
||||||
|
- Traktuje `messageId` jako sukces, a `errorMsg`/`errorCode` jako blad biznesowy.
|
||||||
|
|
||||||
|
### SmsplanetIntegrationController (`src/Modules/Settings/SmsplanetIntegrationController.php`)
|
||||||
|
- Endpointy: `GET /settings/integrations/smsplanet`, `POST /settings/integrations/smsplanet/save`, `POST /settings/integrations/smsplanet/test`.
|
||||||
|
- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`.
|
||||||
|
|
||||||
|
### IntegrationsHubController
|
||||||
|
- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
||||||
|
|
||||||
## Phase 114 — Accounting Configs Refactor
|
## Phase 114 — Accounting Configs Refactor
|
||||||
|
|
||||||
### Sekcja Ksiegowosc — struktura URL
|
### Sekcja Ksiegowosc — struktura URL
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Database Schema
|
# Database Schema
|
||||||
|
|
||||||
**Updated:** 2026-05-10 | **Total tables:** 59 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
**Updated:** 2026-05-12 | **Total tables:** 60 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -591,6 +591,25 @@ UNIQUE: `(integration_id)` - one global HostedSMS settings row.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**smsplanet_integration_settings** - SMSPLANET account credentials (Phase 117; fixed 1 row)
|
||||||
|
| Column | Type | Nullable | Notes |
|
||||||
|
|--------|------|----------|-------|
|
||||||
|
| `id` | TINYINT UNSIGNED | NO | PK, always 1 |
|
||||||
|
| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK -> integrations(id) CASCADE |
|
||||||
|
| `auth_method` | VARCHAR(32) | NO | `token` or `key_password`, DEFAULT `token` |
|
||||||
|
| `api_token_encrypted` | TEXT | YES | AES-encrypted Bearer token via `IntegrationSecretCipher` |
|
||||||
|
| `api_key_encrypted` | TEXT | YES | AES-encrypted API key via `IntegrationSecretCipher` |
|
||||||
|
| `api_password_encrypted` | TEXT | YES | AES-encrypted API password via `IntegrationSecretCipher` |
|
||||||
|
| `sender` | VARCHAR(32) | YES | SMSPLANET `from` sender |
|
||||||
|
| `clear_polish` | TINYINT(1) | NO | DEFAULT 0 |
|
||||||
|
| `transactional` | TINYINT(1) | NO | DEFAULT 0 |
|
||||||
|
| `created_at` | DATETIME | NO | |
|
||||||
|
| `updated_at` | DATETIME | NO | |
|
||||||
|
|
||||||
|
UNIQUE: `(integration_id)` - one global SMSPLANET settings row.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Accounting / Receipts
|
## Accounting / Receipts
|
||||||
|
|
||||||
**receipt_configs** — Receipt generation configurations
|
**receipt_configs** — Receipt generation configurations
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Technical Changelog
|
# Technical Changelog
|
||||||
|
|
||||||
|
## 2026-05-12 - Phase 117 Plan 01: SMSPLANET Integration Settings + Test SMS
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- Dodano migracje `20260512_000108_create_smsplanet_integration_settings.sql` z pojedyncza konfiguracja `smsplanet_integration_settings` i bazowym wpisem `integrations` typu `smsplanet`.
|
||||||
|
- Dodano `SmsplanetIntegrationRepository` z obsluga metod autoryzacji `token` oraz `key_password` i szyfrowaniem sekretow przez `IntegrationSecretCipher`.
|
||||||
|
- Dodano `SmsplanetApiClient` dla SMSPLANET (`POST https://api2.smsplanet.pl/sms`) z obsluga Bearer token oraz `key` + `password`.
|
||||||
|
- Dodano `SmsplanetIntegrationController` i trasy `/settings/integrations/smsplanet`, `/save`, `/test`.
|
||||||
|
- Dodano widok `resources/views/settings/smsplanet.php` z konfiguracja i realna wysylka testowego SMS z edytowalna trescia oraz panelem ostatniego testu (`OK`, HTTP, `messageId`).
|
||||||
|
- Dodano SMSPLANET do hubu integracji `/settings/integrations`.
|
||||||
|
- Poprawiono import `IntegrationSecretCipher`, aby rzucal istniejacy `App\Core\Exceptions\IntegrationConfigException`.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Operator potrzebuje drugiej bramki SMS analogicznej do HostedSMS, ale bez uruchamiania jeszcze automatyzacji lub historii wysylek.
|
||||||
|
- SMSPLANET wspiera dwa warianty autoryzacji, wiec konfiguracja przechowuje wszystkie sekrety w formie szyfrowanej i waliduje wymagania zalezne od wyboru operatora.
|
||||||
|
- Test uzywa rzeczywistej wysylki, bo celem tej fazy jest potwierdzenie realnej sciezki API.
|
||||||
|
|
||||||
## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS
|
## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS
|
||||||
|
|
||||||
**Co zrobiono:**
|
**Co zrobiono:**
|
||||||
|
|||||||
230
.paul/phases/117-smsplanet-integration/117-01-PLAN.md
Normal file
230
.paul/phases/117-smsplanet-integration/117-01-PLAN.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
---
|
||||||
|
phase: 117-smsplanet-integration
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/20260512_000108_create_smsplanet_integration_settings.sql
|
||||||
|
- src/Modules/Settings/SmsplanetApiClient.php
|
||||||
|
- src/Modules/Settings/SmsplanetIntegrationRepository.php
|
||||||
|
- src/Modules/Settings/SmsplanetIntegrationController.php
|
||||||
|
- src/Modules/Settings/IntegrationsHubController.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/settings/smsplanet.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- DOCS/DB_SCHEMA.md
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
- .paul/codebase/db_schema.md
|
||||||
|
- .paul/codebase/architecture.md
|
||||||
|
- .paul/codebase/tech_changelog.md
|
||||||
|
autonomous: true
|
||||||
|
delegation: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Dodac pierwsza wersje integracji SMSPLANET: jedna globalna konfiguracja konta oraz formularz realnej wysylki testowego SMS-a.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Operator ma moc porownac i zweryfikowac druga bramke SMS bez naruszania istniejacej integracji HostedSMS ani dodawania jeszcze automatyzacji SMS.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
Nowa podstrona `/settings/integrations/smsplanet`, zapis konfiguracji w DB, klient API SMSPLANET, akcja testowej wysylki SMS i wpis w hubie integracji.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
<clarifications>
|
||||||
|
- **Zakres** - Czy SMSPLANET ma w tej fazie dostac tylko ekran konfiguracji i realny test SMS, analogicznie do HostedSMS?
|
||||||
|
-> Odpowiedz: Na razie tylko konfiguracja + test.
|
||||||
|
- **Autoryzacja** - Ktory sposob autoryzacji SMSPLANET przyjmujemy jako podstawowy w UI?
|
||||||
|
-> Odpowiedz: Obie wersje, czyli Bearer token oraz key + password.
|
||||||
|
- **Konto** - Czy SMSPLANET ma byc jedna globalna konfiguracja tak jak HostedSMS?
|
||||||
|
-> Odpowiedz: Jedna.
|
||||||
|
- **Test** - Czy test SMSPLANET ma realnie wysylac SMS, czy uzywac parametru `test=1` z API SMSPLANET?
|
||||||
|
-> Odpowiedz: Realnie.
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@AGENTS.md
|
||||||
|
@DOCS/DB_SCHEMA.md
|
||||||
|
@DOCS/ARCHITECTURE.md
|
||||||
|
|
||||||
|
## API Context
|
||||||
|
SMSPLANET API docs: `https://smsplanet.pl/doc/slate/index.html#introduction`
|
||||||
|
- API version documented as `2.3.0`, UTF-8, `POST` content type `application/x-www-form-urlencoded`.
|
||||||
|
- Recommended SMS endpoint: `POST https://api2.smsplanet.pl/sms`.
|
||||||
|
- Recommended authorization: `Authorization: Bearer <token>`.
|
||||||
|
- Alternative authorization: request params `key` and `password`.
|
||||||
|
- Required send params: `from`, `to`, `msg`.
|
||||||
|
- Optional params useful for first version: `clear_polish` (`0`/`1`), `transactional` (`0`/`1`), `test` (`0`/`1`). This plan uses real SMS, so do not set `test=1` in the test action.
|
||||||
|
- Success response contains `messageId`; business failure contains `errorMsg` and `errorCode`.
|
||||||
|
- Accepted recipient formats include `600111222`, `48600111222`, `+48600111222`; normalize only whitespace/separators, do not force country prefix beyond validation.
|
||||||
|
|
||||||
|
## Prior Work
|
||||||
|
@.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md
|
||||||
|
@.paul/phases/116-hostedsms-integration/116-01-PLAN.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@routes/web.php
|
||||||
|
@src/Modules/Settings/HostedSmsApiClient.php
|
||||||
|
@src/Modules/Settings/HostedSmsIntegrationRepository.php
|
||||||
|
@src/Modules/Settings/HostedSmsIntegrationController.php
|
||||||
|
@src/Modules/Settings/IntegrationsRepository.php
|
||||||
|
@src/Modules/Settings/IntegrationSecretCipher.php
|
||||||
|
@src/Modules/Settings/IntegrationsHubController.php
|
||||||
|
@resources/views/settings/hostedsms.php
|
||||||
|
@resources/views/settings/integrations.php
|
||||||
|
@resources/lang/pl.php
|
||||||
|
</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 SMSPLANET
|
||||||
|
```gherkin
|
||||||
|
Given zalogowany operator jest na stronie ustawien SMSPLANET
|
||||||
|
When wybierze metode autoryzacji, uzupelni wymagane pola, nadpis nadawcy, opcje wysylki i zapisze formularz z poprawnym CSRF
|
||||||
|
Then konfiguracja zostanie zapisana jako jedna globalna integracja, sekrety beda zaszyfrowane przez IntegrationSecretCipher, a zapisane sekrety nie beda widoczne w formularzu
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Dwie metody autoryzacji
|
||||||
|
```gherkin
|
||||||
|
Given operator konfiguruje SMSPLANET
|
||||||
|
When wybierze Bearer token
|
||||||
|
Then aplikacja wymaga tokenu przy pierwszym zapisie i wysyla test z naglowkiem Authorization Bearer
|
||||||
|
And gdy wybierze key + password, aplikacja wymaga obu sekretow przy pierwszym zapisie i wysyla test z parametrami key/password
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Walidacja konfiguracji i testu
|
||||||
|
```gherkin
|
||||||
|
Given operator probuje zapisac lub testowac SMSPLANET
|
||||||
|
When brakuje pol wymaganych dla wybranej metody autoryzacji, brakuje nadpisu albo numer/tresc testu sa niepoprawne
|
||||||
|
Then aplikacja pokazuje czytelny blad i nie wykonuje wysylki testowej bez kompletnych danych
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Realny test wysylki SMSPLANET
|
||||||
|
```gherkin
|
||||||
|
Given konfiguracja SMSPLANET jest zapisana z poprawnymi danymi
|
||||||
|
When operator poda numer testowy i tresc testowa oraz kliknie wysylke testowa
|
||||||
|
Then aplikacja wykona realny POST do SMSPLANET bez parametru test=1, zapisze wynik w polach last_test_* integracji i pokaze messageId albo errorMsg/errorCode z API
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Widocznosc w panelu integracji
|
||||||
|
```gherkin
|
||||||
|
Given operator otwiera Ustawienia > Integracje
|
||||||
|
When integracja SMSPLANET istnieje albo jeszcze nie jest skonfigurowana
|
||||||
|
Then hub pokazuje wiersz SMSPLANET ze statusem konfiguracji, sekretu, aktywnosci, ostatniego testu i linkiem do ustawien
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-6: Dokumentacja i zgodnosc projektu
|
||||||
|
```gherkin
|
||||||
|
Given funkcja zostala wdrozona
|
||||||
|
When sprawdzane sa dokumenty techniczne i testy
|
||||||
|
Then DOCS oraz .paul/codebase opisuja nowa tabele, klasy, endpointy i przeplyw, a testy/lint nie wykazuja regresji
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Dodac model konfiguracji SMSPLANET</name>
|
||||||
|
<files>database/migrations/20260512_000108_create_smsplanet_integration_settings.sql, src/Modules/Settings/SmsplanetIntegrationRepository.php</files>
|
||||||
|
<action>
|
||||||
|
Utworz migracje dla pojedynczej tabeli `smsplanet_integration_settings` z rekordem `id=1`, `integration_id` jako UNIQUE FK do `integrations`, polami `auth_method`, `api_token_encrypted`, `api_key`, `api_password_encrypted`, `sender`, `clear_polish`, `transactional`, `created_at`, `updated_at`.
|
||||||
|
Seeduj bazowy rekord `integrations` typu `smsplanet`, nazwa `SMSPLANET`, base_url `https://api2.smsplanet.pl/sms`, timeout 15, aktywny.
|
||||||
|
Repozytorium ma zapewniac bazowy rekord i settings row, zwracac flagi `has_api_token` oraz `has_api_password`, nigdy nie zwracac sekretow w `getSettings()`, zapisywac nowe sekrety tylko gdy pola formularza nie sa puste, wymagac odpowiednich sekretow przy pierwszym zapisie i uzywac `IntegrationSecretCipher` oraz prepared statements.
|
||||||
|
`getCredentials()` ma zwracac tylko kompletna, aktywna konfiguracje z odszyfrowanymi sekretami dla wybranej metody autoryzacji.
|
||||||
|
</action>
|
||||||
|
<verify>C:\xampp\php\php.exe -l src/Modules/Settings/SmsplanetIntegrationRepository.php</verify>
|
||||||
|
<done>AC-1, AC-2 i AC-3 spelnione dla warstwy zapisu konfiguracji.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Dodac klienta API i kontroler ustawien</name>
|
||||||
|
<files>src/Modules/Settings/SmsplanetApiClient.php, src/Modules/Settings/SmsplanetIntegrationController.php, routes/web.php</files>
|
||||||
|
<action>
|
||||||
|
Utworz `SmsplanetApiClient` wykonujacy POST form-urlencoded do `https://api2.smsplanet.pl/sms` z `Accept: application/json`, SSL verification, CA z `SslCertificateResolver`, `User-Agent: orderPRO/1.0` i bez `curl_close()`.
|
||||||
|
Klient ma obslugiwac dwie metody autoryzacji: dla `token` dodaje naglowek `Authorization: Bearer ...`; dla `key_password` dodaje do payloadu `key` i `password`.
|
||||||
|
Payload testu zawiera `from`, `to`, `msg`, opcjonalnie `clear_polish=1` i `transactional=1`; test realny nie ustawia `test=1`.
|
||||||
|
Parsuj odpowiedz JSON: `messageId` przy HTTP 2xx oznacza sukces; `errorMsg`/`errorCode` to blad biznesowy; niepoprawny JSON, cURL i HTTP bez messageId maja dac czytelny komunikat.
|
||||||
|
Utworz `SmsplanetIntegrationController` z akcjami `index`, `save`, `test`. `save` waliduje CSRF i przekazuje payload do repozytorium. `test` waliduje CSRF, numer telefonu po usunieciu spacji, plusa, myslnikow i nawiasow (`^\d{8,15}$`), tresc niepusta i maks. 918 znakow (6 SMS po 153 znaki dla bezpiecznego limitu pierwszej wersji), pobiera credentials i zapisuje `last_test_status`, `last_test_http_code`, `last_test_message` przez `IntegrationsRepository::updateTestResult`.
|
||||||
|
Podlacz DI i trasy: GET `/settings/integrations/smsplanet`, POST `/settings/integrations/smsplanet/save`, POST `/settings/integrations/smsplanet/test`.
|
||||||
|
</action>
|
||||||
|
<verify>C:\xampp\php\php.exe -l src/Modules/Settings/SmsplanetApiClient.php; C:\xampp\php\php.exe -l src/Modules/Settings/SmsplanetIntegrationController.php; C:\xampp\php\php.exe -l routes/web.php</verify>
|
||||||
|
<done>AC-2, AC-3 i AC-4 spelnione dla backendu oraz realnej wysylki testowej.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Dodac UI, hub integracji i dokumentacje</name>
|
||||||
|
<files>resources/views/settings/smsplanet.php, resources/lang/pl.php, src/Modules/Settings/IntegrationsHubController.php, DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md, .paul/codebase/db_schema.md, .paul/codebase/architecture.md, .paul/codebase/tech_changelog.md</files>
|
||||||
|
<action>
|
||||||
|
Dodaj kompaktowy widok ustawien SMSPLANET wzorowany na HostedSMS, bez inline CSS. Formularz konfiguracji ma pokazac: wybor metody autoryzacji (`token` / `key_password`), token Bearer, key, password, sender (`from`), checkbox `clear_polish`, checkbox `transactional`, aktywnosc.
|
||||||
|
Zastosuj proste progressive enhancement tylko jesli potrzebne: pola niewybranej metody moga pozostac widoczne z opisem, ale walidacja serwerowa decyduje o wymaganiach. Nie dodawaj nowego natywnego `alert()`/`confirm()`.
|
||||||
|
Sekcja testu ma miec numer telefonu i edytowalna tresc z domyslna wartoscia `Test orderPRO SMSPLANET`.
|
||||||
|
Ostatni test pokazuje status, HTTP, `messageId` albo komunikat `errorCode: errorMsg`.
|
||||||
|
Dodaj SMSPLANET do hubu integracji oraz tlumaczenia PL.
|
||||||
|
Zaktualizuj dokumentacje techniczna: tabela `smsplanet_integration_settings`, nowe klasy, trasy, dwie metody autoryzacji i przeplyw testowej wysylki.
|
||||||
|
</action>
|
||||||
|
<verify>C:\xampp\php\php.exe -l resources/views/settings/smsplanet.php; C:\xampp\php\php.exe -l src/Modules/Settings/IntegrationsHubController.php; npm run build --if-present</verify>
|
||||||
|
<done>AC-5 i AC-6 spelnione dla UI, hubu i dokumentacji.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- Nie podpinac `DB_HOST_REMOTE` do runtime aplikacji.
|
||||||
|
- Nie modyfikowac zachowania HostedSMS poza ewentualnym wspoldzielonym wzorcem tylko wtedy, gdy jest to absolutnie potrzebne.
|
||||||
|
- Nie dodawac automatyzacji SMS, szablonow SMS, wysylki z zamowien ani historii wyslanych SMS w tym planie.
|
||||||
|
- Nie dodawac natywnych `alert()` / `confirm()`.
|
||||||
|
- Nie refaktoryzowac istniejacych integracji poza minimalnym dopieciem SMSPLANET do hubu i routes.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Tylko jedna globalna konfiguracja SMSPLANET.
|
||||||
|
- Tylko realna wysylka testowa SMS z ustawien.
|
||||||
|
- Obslugiwane metody auth w pierwszej wersji: Bearer token oraz key + password.
|
||||||
|
- Bez raportow doreczen, webhookow, sender-field management, czarnej listy, link shortenera, sprawdzania salda i odbierania SMS.
|
||||||
|
- Bez parametryzowanych kampanii masowych; test wysyla pojedynczy SMS do jednego numeru.
|
||||||
|
|
||||||
|
</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 SMSPLANET dla Bearer token, realna wysylka testowego SMS, komunikat z `messageId`
|
||||||
|
- [ ] Manualnie lub kodowo: walidacja `key_password` wymaga key+password i buduje payload z tymi parametrami
|
||||||
|
- [ ] `sonar-scanner` po APPLY, jezeli CLI i SonarQube sa dostepne
|
||||||
|
- [ ] DOCS i `.paul/codebase` zaktualizowane
|
||||||
|
- [ ] All acceptance criteria met
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Operator moze zapisac jedna konfiguracje SMSPLANET bez ujawniania sekretow.
|
||||||
|
- Operator moze wybrac Bearer token albo key + password i walidacja odpowiada wybranej metodzie.
|
||||||
|
- Operator moze wyslac realny testowy SMS z edytowalna trescia.
|
||||||
|
- Wynik testu jest widoczny w ekranie SMSPLANET i hubie integracji.
|
||||||
|
- Migracje, lint i build przechodza albo blokery srodowiskowe sa jasno opisane w SUMMARY.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
71
.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md
Normal file
71
.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
phase: 117-smsplanet-integration
|
||||||
|
plan: 01
|
||||||
|
completed: 2026-05-12
|
||||||
|
status: complete-with-environment-gaps
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary: SMSPLANET Integration Settings + Test SMS
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
Implemented the first SMSPLANET integration slice: one global configuration screen, encrypted credentials, support for Bearer token and key + password authorization, hub visibility, and a real test SMS flow using `POST https://api2.smsplanet.pl/sms`.
|
||||||
|
|
||||||
|
The implementation also fixes the integration settings checkbox/radio layout reported during UAT by moving the options into the existing compact SCSS component.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
| AC | Status | Notes |
|
||||||
|
|----|--------|-------|
|
||||||
|
| AC-1 Configuration save | PASS | `SmsplanetIntegrationRepository` stores one global row, encrypts token/key/password, and never returns secrets to the view. |
|
||||||
|
| AC-2 Two auth methods | PASS | Bearer token sends `Authorization: Bearer ...`; key + password sends credentials in form payload. First save requires complete credentials for the selected method. |
|
||||||
|
| AC-3 Validation | PASS | Save/test validate CSRF, sender, selected auth method, phone number and message length before API calls. |
|
||||||
|
| AC-4 Real test SMS | IMPLEMENTED, MANUAL PENDING | API client performs real send and does not set `test=1`. Manual live send is pending because local DB/migration verification was blocked. |
|
||||||
|
| AC-5 Hub visibility | PASS | `IntegrationsHubController` includes SMSPLANET with configuration, secret, active and last-test status. |
|
||||||
|
| AC-6 Docs/compliance | PASS WITH TOOLING GAPS | DOCS and `.paul/codebase` updated. PHP lint/build passed. Migration, PHPUnit and Sonar were blocked by local tooling/environment. |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Passed:
|
||||||
|
- `C:\xampp\php\php.exe -l` for all new/modified PHP files in this phase.
|
||||||
|
- `npm run build --if-present`.
|
||||||
|
- `git diff --check`.
|
||||||
|
|
||||||
|
Blocked or unavailable:
|
||||||
|
- `C:\xampp\php\php.exe bin\migrate.php` failed because local MySQL refused the connection: `SQLSTATE[HY000] [2002]`.
|
||||||
|
- `vendor\bin\phpunit` was not available: `Could not open input file`.
|
||||||
|
- `sonar-scanner` was not available in PATH.
|
||||||
|
- Manual real SMSPLANET send remains pending until the migration is applied and valid SMSPLANET credentials are tested.
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
- `api_key` is stored as `api_key_encrypted` rather than plaintext. This is stricter than the draft task and consistent with the requirement that credentials are secrets.
|
||||||
|
- `IntegrationSecretCipher` had an invalid namespace import and was fixed because SMSPLANET and existing integrations depend on it at runtime.
|
||||||
|
- Checkbox/radio UI was adjusted in `resources/scss/app.scss` after the UAT screenshot showed inconsistent native control sizing/alignment.
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
- `.paul/codebase/architecture.md`
|
||||||
|
- `.paul/codebase/db_schema.md`
|
||||||
|
- `.paul/codebase/tech_changelog.md`
|
||||||
|
- `.paul/phases/117-smsplanet-integration/117-01-PLAN.md`
|
||||||
|
- `.paul/phases/117-smsplanet-integration/117-01-SUMMARY.md`
|
||||||
|
- `DOCS/ARCHITECTURE.md`
|
||||||
|
- `DOCS/DB_SCHEMA.md`
|
||||||
|
- `DOCS/TECH_CHANGELOG.md`
|
||||||
|
- `database/migrations/20260512_000108_create_smsplanet_integration_settings.sql`
|
||||||
|
- `resources/lang/pl.php`
|
||||||
|
- `resources/scss/app.scss`
|
||||||
|
- `resources/views/settings/smsplanet.php`
|
||||||
|
- `routes/web.php`
|
||||||
|
- `src/Modules/Settings/IntegrationSecretCipher.php`
|
||||||
|
- `src/Modules/Settings/IntegrationsHubController.php`
|
||||||
|
- `src/Modules/Settings/SmsplanetApiClient.php`
|
||||||
|
- `src/Modules/Settings/SmsplanetIntegrationController.php`
|
||||||
|
- `src/Modules/Settings/SmsplanetIntegrationRepository.php`
|
||||||
|
|
||||||
|
## Follow-Up
|
||||||
|
|
||||||
|
- Start local MySQL/XAMPP and run `C:\xampp\php\php.exe bin\migrate.php`.
|
||||||
|
- Save real SMSPLANET credentials and run both manual test paths: Bearer token and key + password.
|
||||||
|
- Run Sonar once `sonar-scanner` is available.
|
||||||
@@ -168,6 +168,27 @@ tests/
|
|||||||
### IntegrationsHubController
|
### IntegrationsHubController
|
||||||
- Dodaje wiersz HostedSMS do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
- Dodaje wiersz HostedSMS do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
||||||
|
|
||||||
|
## Phase 117 - SMSPLANET Integration Settings
|
||||||
|
|
||||||
|
### SmsplanetIntegrationRepository (`src/Modules/Settings/SmsplanetIntegrationRepository.php`)
|
||||||
|
- Zarzadza pojedynczym rekordem `smsplanet_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `smsplanet`.
|
||||||
|
- Obsluguje dwie metody autoryzacji: Bearer token oraz `key` + `password`.
|
||||||
|
- Szyfruje token, klucz API i haslo przez `IntegrationSecretCipher`; formularz widzi tylko flagi `has_api_token`, `has_api_key` i `has_api_password`.
|
||||||
|
- Udostepnia `getCredentials()` tylko dla kompletnej i aktywnej konfiguracji testowej wysylki SMS.
|
||||||
|
|
||||||
|
### SmsplanetApiClient (`src/Modules/Settings/SmsplanetApiClient.php`)
|
||||||
|
- Wykonuje `POST https://api2.smsplanet.pl/sms` jako `application/x-www-form-urlencoded`.
|
||||||
|
- Dla Bearer token wysyla naglowek `Authorization: Bearer ...`; dla `key_password` wysyla parametry `key` i `password`.
|
||||||
|
- Wysyla `from`, `to`, `msg` oraz opcjonalnie `clear_polish` i `transactional`; test nie ustawia `test=1`, wiec wysyla realny SMS.
|
||||||
|
- Traktuje `messageId` jako sukces, a `errorMsg`/`errorCode` jako blad biznesowy.
|
||||||
|
|
||||||
|
### SmsplanetIntegrationController (`src/Modules/Settings/SmsplanetIntegrationController.php`)
|
||||||
|
- Endpointy: `GET /settings/integrations/smsplanet`, `POST /settings/integrations/smsplanet/save`, `POST /settings/integrations/smsplanet/test`.
|
||||||
|
- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`.
|
||||||
|
|
||||||
|
### IntegrationsHubController
|
||||||
|
- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
||||||
|
|
||||||
## Phase 108 — Delivery Status Management
|
## Phase 108 — Delivery Status Management
|
||||||
|
|
||||||
### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`)
|
### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Database Schema
|
# Database Schema
|
||||||
|
|
||||||
**Updated:** 2026-04-28 | **Total tables:** 55 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
**Updated:** 2026-05-12 | **Total tables:** 60 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -574,6 +574,25 @@ UNIQUE: `(integration_id)` - one global HostedSMS settings row.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**smsplanet_integration_settings** - SMSPLANET account credentials (Phase 117; fixed 1 row)
|
||||||
|
| Column | Type | Nullable | Notes |
|
||||||
|
|--------|------|----------|-------|
|
||||||
|
| `id` | TINYINT UNSIGNED | NO | PK, always 1 |
|
||||||
|
| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK -> integrations(id) CASCADE |
|
||||||
|
| `auth_method` | VARCHAR(32) | NO | `token` or `key_password`, DEFAULT `token` |
|
||||||
|
| `api_token_encrypted` | TEXT | YES | AES-encrypted Bearer token via `IntegrationSecretCipher` |
|
||||||
|
| `api_key_encrypted` | TEXT | YES | AES-encrypted API key via `IntegrationSecretCipher` |
|
||||||
|
| `api_password_encrypted` | TEXT | YES | AES-encrypted API password via `IntegrationSecretCipher` |
|
||||||
|
| `sender` | VARCHAR(32) | YES | SMSPLANET `from` sender |
|
||||||
|
| `clear_polish` | TINYINT(1) | NO | DEFAULT 0 |
|
||||||
|
| `transactional` | TINYINT(1) | NO | DEFAULT 0 |
|
||||||
|
| `created_at` | DATETIME | NO | |
|
||||||
|
| `updated_at` | DATETIME | NO | |
|
||||||
|
|
||||||
|
UNIQUE: `(integration_id)` - one global SMSPLANET settings row.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Accounting / Receipts
|
## Accounting / Receipts
|
||||||
|
|
||||||
**receipt_configs** — Receipt generation configurations
|
**receipt_configs** — Receipt generation configurations
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Technical Changelog
|
# Technical Changelog
|
||||||
|
|
||||||
|
## 2026-05-12 - Phase 117 Plan 01: SMSPLANET Integration Settings + Test SMS
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- Dodano migracje `20260512_000108_create_smsplanet_integration_settings.sql` z pojedyncza konfiguracja `smsplanet_integration_settings` i bazowym wpisem `integrations` typu `smsplanet`.
|
||||||
|
- Dodano `SmsplanetIntegrationRepository` z obsluga metod autoryzacji `token` oraz `key_password` i szyfrowaniem sekretow przez `IntegrationSecretCipher`.
|
||||||
|
- Dodano `SmsplanetApiClient` dla SMSPLANET (`POST https://api2.smsplanet.pl/sms`) z obsluga Bearer token oraz `key` + `password`.
|
||||||
|
- Dodano `SmsplanetIntegrationController` i trasy `/settings/integrations/smsplanet`, `/save`, `/test`.
|
||||||
|
- Dodano widok `resources/views/settings/smsplanet.php` z konfiguracja i realna wysylka testowego SMS z edytowalna trescia oraz panelem ostatniego testu (`OK`, HTTP, `messageId`).
|
||||||
|
- Dodano SMSPLANET do hubu integracji `/settings/integrations`.
|
||||||
|
- Poprawiono import `IntegrationSecretCipher`, aby rzucal istniejacy `App\Core\Exceptions\IntegrationConfigException`.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Operator potrzebuje drugiej bramki SMS analogicznej do HostedSMS, ale bez uruchamiania jeszcze automatyzacji lub historii wysylek.
|
||||||
|
- SMSPLANET wspiera dwa warianty autoryzacji, wiec konfiguracja przechowuje wszystkie sekrety w formie szyfrowanej i waliduje wymagania zalezne od wyboru operatora.
|
||||||
|
- Test uzywa rzeczywistej wysylki, bo celem tej fazy jest potwierdzenie realnej sciezki API.
|
||||||
|
|
||||||
## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS
|
## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS
|
||||||
|
|
||||||
**Co zrobiono:**
|
**Co zrobiono:**
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `smsplanet_integration_settings` (
|
||||||
|
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
|
||||||
|
`integration_id` INT UNSIGNED NULL,
|
||||||
|
`auth_method` VARCHAR(32) NOT NULL DEFAULT 'token',
|
||||||
|
`api_token_encrypted` TEXT NULL,
|
||||||
|
`api_key_encrypted` TEXT NULL,
|
||||||
|
`api_password_encrypted` TEXT NULL,
|
||||||
|
`sender` VARCHAR(32) NULL,
|
||||||
|
`clear_polish` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`transactional` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY `smsplanet_integration_settings_integration_unique` (`integration_id`),
|
||||||
|
CONSTRAINT `smsplanet_integration_settings_integration_fk`
|
||||||
|
FOREIGN KEY (`integration_id`) REFERENCES `integrations` (`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO `integrations` (`type`, `name`, `base_url`, `timeout_seconds`, `is_active`, `created_at`, `updated_at`)
|
||||||
|
VALUES ('smsplanet', 'SMSPLANET', 'https://api2.smsplanet.pl/sms', 15, 1, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`base_url` = VALUES(`base_url`),
|
||||||
|
`timeout_seconds` = VALUES(`timeout_seconds`),
|
||||||
|
`updated_at` = VALUES(`updated_at`);
|
||||||
|
|
||||||
|
INSERT INTO `smsplanet_integration_settings` (`id`, `integration_id`, `created_at`, `updated_at`)
|
||||||
|
SELECT 1, `id`, NOW(), NOW()
|
||||||
|
FROM `integrations`
|
||||||
|
WHERE `type` = 'smsplanet' AND `name` = 'SMSPLANET'
|
||||||
|
LIMIT 1
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`integration_id` = VALUES(`integration_id`),
|
||||||
|
`updated_at` = VALUES(`updated_at`);
|
||||||
@@ -562,6 +562,7 @@ return [
|
|||||||
'inpost' => 'InPost',
|
'inpost' => 'InPost',
|
||||||
'shoppro' => 'shopPRO',
|
'shoppro' => 'shopPRO',
|
||||||
'hostedsms' => 'HostedSMS',
|
'hostedsms' => 'HostedSMS',
|
||||||
|
'smsplanet' => 'SMSPLANET',
|
||||||
'shoppro_instances' => ':count instancji',
|
'shoppro_instances' => ':count instancji',
|
||||||
],
|
],
|
||||||
'status' => [
|
'status' => [
|
||||||
@@ -762,6 +763,68 @@ return [
|
|||||||
'test_failed' => 'Nie udalo sie wyslac testowego SMS.',
|
'test_failed' => 'Nie udalo sie wyslac testowego SMS.',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'smsplanet' => [
|
||||||
|
'title' => 'Integracja SMSPLANET',
|
||||||
|
'description' => 'Konfiguracja konta SMSPLANET do wysylki SMS z orderPRO.',
|
||||||
|
'config' => [
|
||||||
|
'title' => 'Konfiguracja API',
|
||||||
|
],
|
||||||
|
'test' => [
|
||||||
|
'title' => 'Test wysylki SMS',
|
||||||
|
'description' => 'Test realnie wysyla SMS przez API SMSPLANET.',
|
||||||
|
],
|
||||||
|
'auth' => [
|
||||||
|
'token' => 'Bearer token',
|
||||||
|
'key_password' => 'Klucz API + haslo API',
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
'auth_method' => 'Metoda autoryzacji',
|
||||||
|
'api_token' => 'Token Bearer',
|
||||||
|
'api_key' => 'Klucz API',
|
||||||
|
'api_password' => 'Haslo API',
|
||||||
|
'sender' => 'Pole nadawcy / from',
|
||||||
|
'options' => 'Opcje wysylki',
|
||||||
|
'clear_polish' => 'Zamien polskie znaki na odpowiedniki GSM',
|
||||||
|
'transactional' => 'Wysylka kanalem transakcyjnym',
|
||||||
|
'is_active' => 'Integracja aktywna',
|
||||||
|
'test_phone' => 'Numer testowy',
|
||||||
|
'test_message' => 'Tresc testowego SMS',
|
||||||
|
],
|
||||||
|
'token' => [
|
||||||
|
'saved' => 'Token jest zapisany. Pozostaw pole puste, aby nie zmieniac.',
|
||||||
|
'missing' => 'Brak zapisanego tokenu Bearer.',
|
||||||
|
],
|
||||||
|
'key' => [
|
||||||
|
'saved' => 'Klucz API jest zapisany. Pozostaw pole puste, aby nie zmieniac.',
|
||||||
|
'missing' => 'Brak zapisanego klucza API.',
|
||||||
|
],
|
||||||
|
'password' => [
|
||||||
|
'saved' => 'Haslo API jest zapisane. Pozostaw pole puste, aby nie zmieniac.',
|
||||||
|
'missing' => 'Brak zapisanego hasla API.',
|
||||||
|
],
|
||||||
|
'hints' => [
|
||||||
|
'auth_method' => 'SMSPLANET zaleca token Bearer, ale API obsluguje tez klucz i haslo.',
|
||||||
|
'sender' => 'Pole nadawcy musi byc dostepne na koncie SMSPLANET albo miec wartosc testowa dopuszczona przez provider.',
|
||||||
|
],
|
||||||
|
'status' => [
|
||||||
|
'secret' => 'Sekret API',
|
||||||
|
'active' => 'Aktywna',
|
||||||
|
'saved' => 'zapisany',
|
||||||
|
'missing' => 'brak',
|
||||||
|
'last_test' => 'Ostatni test',
|
||||||
|
'message_id' => 'Identyfikator wiadomości',
|
||||||
|
],
|
||||||
|
'actions' => [
|
||||||
|
'save' => 'Zapisz ustawienia SMSPLANET',
|
||||||
|
'send_test' => 'Wyslij testowy SMS',
|
||||||
|
],
|
||||||
|
'flash' => [
|
||||||
|
'saved' => 'Ustawienia SMSPLANET zostaly zapisane.',
|
||||||
|
'save_failed' => 'Nie udalo sie zapisac ustawien SMSPLANET.',
|
||||||
|
'test_success' => 'Testowy SMS zostal przyjety przez SMSPLANET. messageId: :message_id.',
|
||||||
|
'test_failed' => 'Nie udalo sie wyslac testowego SMS.',
|
||||||
|
],
|
||||||
|
],
|
||||||
'inpost' => [
|
'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.',
|
||||||
|
|||||||
@@ -2717,6 +2717,7 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.integration-settings-checkboxes {
|
.integration-settings-checkboxes {
|
||||||
|
grid-column: 1 / -1;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -2737,10 +2738,21 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
min-height: 24px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.integration-settings-checkboxes__item input[type='checkbox'],
|
||||||
|
.integration-settings-checkboxes__item input[type='radio'] {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: var(--c-action-primary);
|
||||||
|
}
|
||||||
|
|
||||||
// Hamburger button (hidden on desktop)
|
// Hamburger button (hidden on desktop)
|
||||||
.topbar__hamburger {
|
.topbar__hamburger {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
159
resources/views/settings/smsplanet.php
Normal file
159
resources/views/settings/smsplanet.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
$settings = is_array($settings ?? null) ? $settings : [];
|
||||||
|
$authMethod = (string) ($settings['auth_method'] ?? 'token');
|
||||||
|
$sender = trim((string) ($settings['sender'] ?? ''));
|
||||||
|
$hasApiToken = (bool) ($settings['has_api_token'] ?? false);
|
||||||
|
$hasApiKey = (bool) ($settings['has_api_key'] ?? false);
|
||||||
|
$hasApiPassword = (bool) ($settings['has_api_password'] ?? false);
|
||||||
|
$isActive = (bool) ($settings['is_active'] ?? true);
|
||||||
|
$clearPolish = (bool) ($settings['clear_polish'] ?? false);
|
||||||
|
$transactional = (bool) ($settings['transactional'] ?? 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.smsplanet.title')) ?></h2>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.smsplanet.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.smsplanet.config.title')) ?></h3>
|
||||||
|
|
||||||
|
<div class="muted mt-12">
|
||||||
|
<?= $e($t('settings.smsplanet.status.secret')) ?>:
|
||||||
|
<strong><?= $e(($hasApiToken || $hasApiPassword) ? $t('settings.smsplanet.status.saved') : $t('settings.smsplanet.status.missing')) ?></strong>
|
||||||
|
|
|
||||||
|
<?= $e($t('settings.smsplanet.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/smsplanet/save" method="post" novalidate>
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
|
||||||
|
<fieldset class="integration-settings-checkboxes">
|
||||||
|
<legend class="field-label"><?= $e($t('settings.smsplanet.fields.auth_method')) ?></legend>
|
||||||
|
<div class="integration-settings-checkboxes__list">
|
||||||
|
<label class="integration-settings-checkboxes__item">
|
||||||
|
<input type="radio" name="auth_method" value="token"<?= $authMethod === 'token' ? ' checked' : '' ?>>
|
||||||
|
<span><?= $e($t('settings.smsplanet.auth.token')) ?></span>
|
||||||
|
</label>
|
||||||
|
<label class="integration-settings-checkboxes__item">
|
||||||
|
<input type="radio" name="auth_method" value="key_password"<?= $authMethod === 'key_password' ? ' checked' : '' ?>>
|
||||||
|
<span><?= $e($t('settings.smsplanet.auth.key_password')) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span class="muted"><?= $e($t('settings.smsplanet.hints.auth_method')) ?></span>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.smsplanet.fields.api_token')) ?></span>
|
||||||
|
<input class="form-control" type="password" name="api_token" autocomplete="new-password" placeholder="<?= $hasApiToken ? '********' : '' ?>">
|
||||||
|
<span class="muted"><?= $e($hasApiToken ? $t('settings.smsplanet.token.saved') : $t('settings.smsplanet.token.missing')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.smsplanet.fields.api_key')) ?></span>
|
||||||
|
<input class="form-control" type="password" name="api_key" autocomplete="new-password" placeholder="<?= $hasApiKey ? '********' : '' ?>">
|
||||||
|
<span class="muted"><?= $e($hasApiKey ? $t('settings.smsplanet.key.saved') : $t('settings.smsplanet.key.missing')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.smsplanet.fields.api_password')) ?></span>
|
||||||
|
<input class="form-control" type="password" name="api_password" autocomplete="new-password" placeholder="<?= $hasApiPassword ? '********' : '' ?>">
|
||||||
|
<span class="muted"><?= $e($hasApiPassword ? $t('settings.smsplanet.password.saved') : $t('settings.smsplanet.password.missing')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.smsplanet.fields.sender')) ?></span>
|
||||||
|
<input class="form-control" type="text" name="sender" maxlength="32" value="<?= $e($sender) ?>" required>
|
||||||
|
<span class="muted"><?= $e($t('settings.smsplanet.hints.sender')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<fieldset class="integration-settings-checkboxes">
|
||||||
|
<legend class="field-label"><?= $e($t('settings.smsplanet.fields.options')) ?></legend>
|
||||||
|
<div class="integration-settings-checkboxes__list">
|
||||||
|
<label class="integration-settings-checkboxes__item">
|
||||||
|
<input type="checkbox" name="clear_polish" value="1"<?= $clearPolish ? ' checked' : '' ?>>
|
||||||
|
<span><?= $e($t('settings.smsplanet.fields.clear_polish')) ?></span>
|
||||||
|
</label>
|
||||||
|
<label class="integration-settings-checkboxes__item">
|
||||||
|
<input type="checkbox" name="transactional" value="1"<?= $transactional ? ' checked' : '' ?>>
|
||||||
|
<span><?= $e($t('settings.smsplanet.fields.transactional')) ?></span>
|
||||||
|
</label>
|
||||||
|
<label class="integration-settings-checkboxes__item">
|
||||||
|
<input type="checkbox" name="is_active" value="1"<?= $isActive ? ' checked' : '' ?>>
|
||||||
|
<span><?= $e($t('settings.smsplanet.fields.is_active')) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-actions mt-16">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.smsplanet.actions.save')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.smsplanet.test.title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.smsplanet.test.description')) ?></p>
|
||||||
|
|
||||||
|
<form class="statuses-form mt-16" action="/settings/integrations/smsplanet/test" method="post" novalidate>
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.smsplanet.fields.test_phone')) ?></span>
|
||||||
|
<input class="form-control" type="tel" name="phone" inputmode="tel" placeholder="48600111222" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.smsplanet.fields.test_message')) ?></span>
|
||||||
|
<textarea class="form-control" name="message" rows="4" maxlength="918" required>Test orderPRO SMSPLANET</textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="form-actions mt-16">
|
||||||
|
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.smsplanet.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.smsplanet.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.smsplanet.status.message_id')) ?>:
|
||||||
|
<code><?= $e($lastMessageId) ?></code>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($lastTestMessage !== ''): ?>
|
||||||
|
<div class="mt-12"><?= $e($lastTestMessage) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
@@ -39,6 +39,9 @@ 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;
|
||||||
use App\Modules\Settings\IntegrationsRepository;
|
use App\Modules\Settings\IntegrationsRepository;
|
||||||
|
use App\Modules\Settings\SmsplanetApiClient;
|
||||||
|
use App\Modules\Settings\SmsplanetIntegrationController;
|
||||||
|
use App\Modules\Settings\SmsplanetIntegrationRepository;
|
||||||
use App\Modules\Settings\ShopproIntegrationsController;
|
use App\Modules\Settings\ShopproIntegrationsController;
|
||||||
use App\Modules\Settings\ShopproIntegrationsRepository;
|
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||||
use App\Modules\Settings\ShopproPullStatusMappingRepository;
|
use App\Modules\Settings\ShopproPullStatusMappingRepository;
|
||||||
@@ -205,6 +208,18 @@ return static function (Application $app): void {
|
|||||||
new HostedSmsApiClient(),
|
new HostedSmsApiClient(),
|
||||||
new IntegrationsRepository($app->db())
|
new IntegrationsRepository($app->db())
|
||||||
);
|
);
|
||||||
|
$smsplanetIntegrationRepository = new SmsplanetIntegrationRepository(
|
||||||
|
$app->db(),
|
||||||
|
(string) $app->config('app.integrations.secret', '')
|
||||||
|
);
|
||||||
|
$smsplanetIntegrationController = new SmsplanetIntegrationController(
|
||||||
|
$template,
|
||||||
|
$translator,
|
||||||
|
$auth,
|
||||||
|
$smsplanetIntegrationRepository,
|
||||||
|
new SmsplanetApiClient(),
|
||||||
|
new IntegrationsRepository($app->db())
|
||||||
|
);
|
||||||
$integrationsHubController = new IntegrationsHubController(
|
$integrationsHubController = new IntegrationsHubController(
|
||||||
$template,
|
$template,
|
||||||
$translator,
|
$translator,
|
||||||
@@ -215,7 +230,8 @@ return static function (Application $app): void {
|
|||||||
$inpostIntegrationRepository,
|
$inpostIntegrationRepository,
|
||||||
$shopproIntegrationsRepository,
|
$shopproIntegrationsRepository,
|
||||||
$fakturowniaIntegrationRepository,
|
$fakturowniaIntegrationRepository,
|
||||||
$hostedSmsIntegrationRepository
|
$hostedSmsIntegrationRepository,
|
||||||
|
$smsplanetIntegrationRepository
|
||||||
);
|
);
|
||||||
$cronSettingsController = new CronSettingsController(
|
$cronSettingsController = new CronSettingsController(
|
||||||
$template,
|
$template,
|
||||||
@@ -562,6 +578,9 @@ return static function (Application $app): void {
|
|||||||
$router->get('/settings/integrations/hostedsms', [$hostedSmsIntegrationController, 'index'], [$authMiddleware]);
|
$router->get('/settings/integrations/hostedsms', [$hostedSmsIntegrationController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/hostedsms/save', [$hostedSmsIntegrationController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/hostedsms/save', [$hostedSmsIntegrationController, 'save'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/hostedsms/test', [$hostedSmsIntegrationController, 'test'], [$authMiddleware]);
|
$router->post('/settings/integrations/hostedsms/test', [$hostedSmsIntegrationController, 'test'], [$authMiddleware]);
|
||||||
|
$router->get('/settings/integrations/smsplanet', [$smsplanetIntegrationController, 'index'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/smsplanet/save', [$smsplanetIntegrationController, 'save'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/smsplanet/test', [$smsplanetIntegrationController, 'test'], [$authMiddleware]);
|
||||||
$router->get('/settings/integrations/shoppro', [$shopproIntegrationsController, 'index'], [$authMiddleware]);
|
$router->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]);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Modules\Settings;
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
use AppCorexceptionsIntegrationConfigException;
|
use App\Core\Exceptions\IntegrationConfigException;
|
||||||
|
|
||||||
final class IntegrationSecretCipher
|
final class IntegrationSecretCipher
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ final class IntegrationsHubController
|
|||||||
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
|
private readonly HostedSmsIntegrationRepository $hostedSms,
|
||||||
|
private readonly SmsplanetIntegrationRepository $smsplanet
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ final class IntegrationsHubController
|
|||||||
$this->buildShopproRow(),
|
$this->buildShopproRow(),
|
||||||
$this->buildFakturowniaRow(),
|
$this->buildFakturowniaRow(),
|
||||||
$this->buildHostedSmsRow(),
|
$this->buildHostedSmsRow(),
|
||||||
|
$this->buildSmsplanetRow(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$html = $this->template->render('settings/integrations', [
|
$html = $this->template->render('settings/integrations', [
|
||||||
@@ -242,4 +244,33 @@ final class IntegrationsHubController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildSmsplanetRow(): array
|
||||||
|
{
|
||||||
|
$settings = $this->smsplanet->getSettings();
|
||||||
|
$authMethod = (string) ($settings['auth_method'] ?? 'token');
|
||||||
|
$isConfigured = !empty($settings['sender'])
|
||||||
|
&& (
|
||||||
|
($authMethod === 'token' && !empty($settings['has_api_token']))
|
||||||
|
|| ($authMethod === 'key_password' && !empty($settings['has_api_key']) && !empty($settings['has_api_password']))
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'provider' => $this->translator->get('settings.integrations_hub.providers.smsplanet'),
|
||||||
|
'instance' => 'SMSPLANET',
|
||||||
|
'authorization_status' => $isConfigured
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.configured')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.not_configured'),
|
||||||
|
'secret_status' => $isConfigured
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.saved')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.missing'),
|
||||||
|
'is_active' => !empty($settings['is_active']),
|
||||||
|
'last_test_at' => trim((string) ($settings['last_test_at'] ?? '')),
|
||||||
|
'configure_url' => '/settings/integrations/smsplanet',
|
||||||
|
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
145
src/Modules/Settings/SmsplanetApiClient.php
Normal file
145
src/Modules/Settings/SmsplanetApiClient.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Http\SslCertificateResolver;
|
||||||
|
|
||||||
|
final class SmsplanetApiClient
|
||||||
|
{
|
||||||
|
private const API_URL = 'https://api2.smsplanet.pl/sms';
|
||||||
|
private const AUTH_TOKEN = 'token';
|
||||||
|
|
||||||
|
public function __construct(private readonly int $timeoutSeconds = 15)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $credentials
|
||||||
|
* @return array{ok: bool, http_code: int, message: string, message_id: string}
|
||||||
|
*/
|
||||||
|
public function sendSms(array $credentials, string $phone, string $message): array
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'from' => trim((string) ($credentials['sender'] ?? '')),
|
||||||
|
'to' => trim($phone),
|
||||||
|
'msg' => $message,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($credentials['clear_polish'])) {
|
||||||
|
$payload['clear_polish'] = '1';
|
||||||
|
}
|
||||||
|
if (!empty($credentials['transactional'])) {
|
||||||
|
$payload['transactional'] = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
if (($credentials['auth_method'] ?? '') === self::AUTH_TOKEN) {
|
||||||
|
$headers[] = 'Authorization: Bearer ' . trim((string) ($credentials['api_token'] ?? ''));
|
||||||
|
} else {
|
||||||
|
$payload['key'] = trim((string) ($credentials['api_key'] ?? ''));
|
||||||
|
$payload['password'] = (string) ($credentials['api_password'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$body, $httpCode, $curlError] = $this->postForm($payload, $headers);
|
||||||
|
if ($curlError !== null) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'message' => 'Blad polaczenia: ' . $curlError,
|
||||||
|
'message_id' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parseResponse($body, $httpCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $payload
|
||||||
|
* @param array<int, string> $extraHeaders
|
||||||
|
* @return array{0: string, 1: int, 2: ?string}
|
||||||
|
*/
|
||||||
|
private function postForm(array $payload, array $extraHeaders): array
|
||||||
|
{
|
||||||
|
$ch = curl_init(self::API_URL);
|
||||||
|
if ($ch === false) {
|
||||||
|
return ['', 0, 'Nie udalo sie zainicjowac cURL.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = array_merge([
|
||||||
|
'Accept: application/json',
|
||||||
|
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
'User-Agent: orderPRO/1.0',
|
||||||
|
], $extraHeaders);
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => http_build_query($payload),
|
||||||
|
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 10,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => true,
|
||||||
|
CURLOPT_SSL_VERIFYHOST => 2,
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
];
|
||||||
|
|
||||||
|
$caPath = SslCertificateResolver::resolve();
|
||||||
|
if ($caPath !== null) {
|
||||||
|
$options[CURLOPT_CAINFO] = $caPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($ch, $options);
|
||||||
|
$rawBody = curl_exec($ch);
|
||||||
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
unset($ch);
|
||||||
|
|
||||||
|
if ($rawBody === false) {
|
||||||
|
return ['', $httpCode, $curlError !== '' ? $curlError : 'Brak odpowiedzi z API.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [(string) $rawBody, $httpCode, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok: bool, http_code: int, message: string, message_id: string}
|
||||||
|
*/
|
||||||
|
private function parseResponse(string $body, int $httpCode): array
|
||||||
|
{
|
||||||
|
$decoded = json_decode(ltrim($body, "\xEF\xBB\xBF \t\n\r\0\x0B"), true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'message' => 'Niepoprawna odpowiedz JSON SMSPLANET: ' . substr(trim(strip_tags($body)), 0, 180),
|
||||||
|
'message_id' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$messageId = trim((string) ($decoded['messageId'] ?? ''));
|
||||||
|
if ($httpCode >= 200 && $httpCode < 300 && $messageId !== '') {
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'message' => 'messageId: ' . $messageId,
|
||||||
|
'message_id' => $messageId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorCode = trim((string) ($decoded['errorCode'] ?? ''));
|
||||||
|
$errorMessage = trim((string) ($decoded['errorMsg'] ?? ''));
|
||||||
|
if ($errorMessage === '') {
|
||||||
|
$errorMessage = 'HTTP ' . $httpCode;
|
||||||
|
}
|
||||||
|
if ($errorCode !== '') {
|
||||||
|
$errorMessage = 'errorCode ' . $errorCode . ': ' . $errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'message' => $errorMessage,
|
||||||
|
'message_id' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/Modules/Settings/SmsplanetIntegrationController.php
Normal file
145
src/Modules/Settings/SmsplanetIntegrationController.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?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 SmsplanetIntegrationController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Template $template,
|
||||||
|
private readonly Translator $translator,
|
||||||
|
private readonly AuthService $auth,
|
||||||
|
private readonly SmsplanetIntegrationRepository $repository,
|
||||||
|
private readonly SmsplanetApiClient $apiClient,
|
||||||
|
private readonly IntegrationsRepository $integrations
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$html = $this->template->render('settings/smsplanet', [
|
||||||
|
'title' => $this->translator->get('settings.smsplanet.title'),
|
||||||
|
'activeMenu' => 'settings',
|
||||||
|
'activeSettings' => 'integrations',
|
||||||
|
'user' => $this->auth->user(),
|
||||||
|
'csrfToken' => Csrf::token(),
|
||||||
|
'settings' => $this->repository->getSettings(),
|
||||||
|
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||||
|
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||||
|
'testMessage' => (string) Flash::get('smsplanet_test', ''),
|
||||||
|
], 'layouts/app');
|
||||||
|
|
||||||
|
return Response::html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Request $request): Response
|
||||||
|
{
|
||||||
|
$redirectTo = $this->resolveRedirect($request);
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->repository->saveSettings([
|
||||||
|
'auth_method' => (string) $request->input('auth_method', ''),
|
||||||
|
'api_token' => (string) $request->input('api_token', ''),
|
||||||
|
'api_key' => (string) $request->input('api_key', ''),
|
||||||
|
'api_password' => (string) $request->input('api_password', ''),
|
||||||
|
'sender' => (string) $request->input('sender', ''),
|
||||||
|
'clear_polish' => $request->input('clear_polish', ''),
|
||||||
|
'transactional' => $request->input('transactional', ''),
|
||||||
|
'is_active' => $request->input('is_active', ''),
|
||||||
|
]);
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.smsplanet.flash.saved'));
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set(
|
||||||
|
'settings_error',
|
||||||
|
$this->translator->get('settings.smsplanet.flash.save_failed') . ' ' . $exception->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test(Request $request): Response
|
||||||
|
{
|
||||||
|
$redirectTo = $this->resolveRedirect($request);
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$phone = $this->validatePhone((string) $request->input('phone', ''));
|
||||||
|
$message = $this->validateMessage((string) $request->input('message', ''));
|
||||||
|
$credentials = $this->repository->getCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
throw new IntegrationConfigException('Najpierw zapisz kompletna i aktywna konfiguracje SMSPLANET.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->apiClient->sendSms($credentials, $phone, $message);
|
||||||
|
$this->integrations->updateTestResult(
|
||||||
|
$credentials['integration_id'],
|
||||||
|
$result['ok'] ? 'ok' : 'fail',
|
||||||
|
(int) $result['http_code'],
|
||||||
|
(string) $result['message']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['ok']) {
|
||||||
|
Flash::set('smsplanet_test', $this->translator->get('settings.smsplanet.flash.test_success', [
|
||||||
|
'message_id' => (string) $result['message_id'],
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.smsplanet.flash.test_failed') . ' ' . $result['message']);
|
||||||
|
}
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.smsplanet.flash.test_failed') . ' ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRedirect(Request $request): string
|
||||||
|
{
|
||||||
|
return RedirectPathResolver::resolve(
|
||||||
|
(string) $request->input('return_to', '/settings/integrations/smsplanet'),
|
||||||
|
['/settings/integrations'],
|
||||||
|
'/settings/integrations/smsplanet'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validatePhone(string $value): string
|
||||||
|
{
|
||||||
|
$phone = preg_replace('/[\s+\-()]/', '', trim($value)) ?? '';
|
||||||
|
if (preg_match('/^\d{8,15}$/', $phone) !== 1) {
|
||||||
|
throw new IntegrationConfigException('Podaj numer telefonu w formacie 600111222 albo 48600111222.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateMessage(string $value): string
|
||||||
|
{
|
||||||
|
$message = trim($value);
|
||||||
|
if ($message === '') {
|
||||||
|
throw new IntegrationConfigException('Podaj tresc testowego SMS.');
|
||||||
|
}
|
||||||
|
if (strlen($message) > 918) {
|
||||||
|
throw new IntegrationConfigException('Tresc testowego SMS nie moze przekraczac 918 znakow.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
330
src/Modules/Settings/SmsplanetIntegrationRepository.php
Normal file
330
src/Modules/Settings/SmsplanetIntegrationRepository.php
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
<?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 SmsplanetIntegrationRepository
|
||||||
|
{
|
||||||
|
private const INTEGRATION_TYPE = 'smsplanet';
|
||||||
|
private const INTEGRATION_NAME = 'SMSPLANET';
|
||||||
|
private const INTEGRATION_BASE_URL = 'https://api2.smsplanet.pl/sms';
|
||||||
|
private const AUTH_TOKEN = 'token';
|
||||||
|
private const AUTH_KEY_PASSWORD = 'key_password';
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'auth_method' => $this->normalizeAuthMethod((string) ($row['auth_method'] ?? '')),
|
||||||
|
'sender' => trim((string) ($row['sender'] ?? '')),
|
||||||
|
'clear_polish' => !empty($row['clear_polish']),
|
||||||
|
'transactional' => !empty($row['transactional']),
|
||||||
|
'has_api_token' => $this->hasEncryptedValue($row['api_token_encrypted'] ?? null),
|
||||||
|
'has_api_key' => $this->hasEncryptedValue($row['api_key_encrypted'] ?? null),
|
||||||
|
'has_api_password' => $this->hasEncryptedValue($row['api_password_encrypted'] ?? null),
|
||||||
|
'is_active' => (int) ($integration['is_active'] ?? 1) === 1,
|
||||||
|
'last_test_status' => trim((string) ($integration['last_test_status'] ?? '')),
|
||||||
|
'last_test_http_code' => isset($integration['last_test_http_code']) ? (int) $integration['last_test_http_code'] : null,
|
||||||
|
'last_test_message' => trim((string) ($integration['last_test_message'] ?? '')),
|
||||||
|
'last_test_at' => trim((string) ($integration['last_test_at'] ?? '')),
|
||||||
|
'updated_at' => trim((string) ($row['updated_at'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function saveSettings(array $payload): void
|
||||||
|
{
|
||||||
|
$this->ensureRow();
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$row = $this->fetchRequiredRow();
|
||||||
|
|
||||||
|
$authMethod = $this->normalizeAuthMethod((string) ($payload['auth_method'] ?? ''));
|
||||||
|
$sender = $this->validateSender((string) ($payload['sender'] ?? ''));
|
||||||
|
$tokenEncrypted = $this->resolveTokenEncrypted($row, (string) ($payload['api_token'] ?? ''));
|
||||||
|
$keyEncrypted = $this->resolveKeyEncrypted($row, (string) ($payload['api_key'] ?? ''));
|
||||||
|
$passwordEncrypted = $this->resolvePasswordEncrypted($row, (string) ($payload['api_password'] ?? ''));
|
||||||
|
|
||||||
|
$this->validateCredentials($authMethod, $tokenEncrypted, $keyEncrypted, $passwordEncrypted);
|
||||||
|
$this->updateSettingsRow($authMethod, $tokenEncrypted, $keyEncrypted, $passwordEncrypted, $sender, $payload);
|
||||||
|
$this->updateIntegrationActive($integrationId, !empty($payload['is_active']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* integration_id: int,
|
||||||
|
* auth_method: string,
|
||||||
|
* api_token: string,
|
||||||
|
* api_key: string,
|
||||||
|
* api_password: string,
|
||||||
|
* sender: string,
|
||||||
|
* clear_polish: bool,
|
||||||
|
* transactional: bool
|
||||||
|
* }|null
|
||||||
|
*/
|
||||||
|
public function getCredentials(): ?array
|
||||||
|
{
|
||||||
|
$this->ensureRow();
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$row = $this->fetchRow();
|
||||||
|
$integration = $this->integrations->findById($integrationId);
|
||||||
|
if ($row === null || (int) ($integration['is_active'] ?? 0) !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authMethod = $this->normalizeAuthMethod((string) ($row['auth_method'] ?? ''));
|
||||||
|
$sender = trim((string) ($row['sender'] ?? ''));
|
||||||
|
$apiToken = $this->decryptValue((string) ($row['api_token_encrypted'] ?? ''));
|
||||||
|
$apiKey = $this->decryptValue((string) ($row['api_key_encrypted'] ?? ''));
|
||||||
|
$apiPassword = $this->decryptValue((string) ($row['api_password_encrypted'] ?? ''));
|
||||||
|
|
||||||
|
if (!$this->hasCompleteCredentials($authMethod, $sender, $apiToken, $apiKey, $apiPassword)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'auth_method' => $authMethod,
|
||||||
|
'api_token' => $apiToken,
|
||||||
|
'api_key' => $apiKey,
|
||||||
|
'api_password' => $apiPassword,
|
||||||
|
'sender' => $sender,
|
||||||
|
'clear_polish' => !empty($row['clear_polish']),
|
||||||
|
'transactional' => !empty($row['transactional']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureBaseIntegration(): int
|
||||||
|
{
|
||||||
|
return $this->integrations->ensureIntegration(
|
||||||
|
self::INTEGRATION_TYPE,
|
||||||
|
self::INTEGRATION_NAME,
|
||||||
|
self::INTEGRATION_BASE_URL,
|
||||||
|
15,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureRow(): void
|
||||||
|
{
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO smsplanet_integration_settings (id, integration_id, created_at, updated_at)
|
||||||
|
VALUES (1, :integration_id, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE integration_id = VALUES(integration_id), updated_at = VALUES(updated_at)'
|
||||||
|
);
|
||||||
|
$statement->execute(['integration_id' => $integrationId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function fetchRow(): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare('SELECT * FROM smsplanet_integration_settings WHERE id = 1 LIMIT 1');
|
||||||
|
$statement->execute();
|
||||||
|
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($row) ? $row : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function fetchRequiredRow(): array
|
||||||
|
{
|
||||||
|
$row = $this->fetchRow();
|
||||||
|
if ($row === null) {
|
||||||
|
throw new IntegrationConfigException('Brak rekordu konfiguracji SMSPLANET.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeAuthMethod(string $value): string
|
||||||
|
{
|
||||||
|
return $value === self::AUTH_KEY_PASSWORD ? self::AUTH_KEY_PASSWORD : self::AUTH_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateSender(string $value): string
|
||||||
|
{
|
||||||
|
$sender = trim($value);
|
||||||
|
if ($sender === '' || strlen($sender) > 32) {
|
||||||
|
throw new IntegrationConfigException('Podaj pole nadawcy SMSPLANET (maks. 32 znaki).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $row
|
||||||
|
*/
|
||||||
|
private function resolveTokenEncrypted(array $row, string $newToken): ?string
|
||||||
|
{
|
||||||
|
$token = trim($newToken);
|
||||||
|
if ($token !== '') {
|
||||||
|
return $this->cipher->encrypt($token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringHelper::nullableString((string) ($row['api_token_encrypted'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $row
|
||||||
|
*/
|
||||||
|
private function resolveKeyEncrypted(array $row, string $newKey): ?string
|
||||||
|
{
|
||||||
|
$key = trim($newKey);
|
||||||
|
if ($key !== '') {
|
||||||
|
return $this->cipher->encrypt($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringHelper::nullableString((string) ($row['api_key_encrypted'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $row
|
||||||
|
*/
|
||||||
|
private function resolvePasswordEncrypted(array $row, string $newPassword): ?string
|
||||||
|
{
|
||||||
|
$password = trim($newPassword);
|
||||||
|
if ($password !== '') {
|
||||||
|
return $this->cipher->encrypt($password);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringHelper::nullableString((string) ($row['api_password_encrypted'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateCredentials(
|
||||||
|
string $authMethod,
|
||||||
|
?string $tokenEncrypted,
|
||||||
|
?string $keyEncrypted,
|
||||||
|
?string $passwordEncrypted
|
||||||
|
): void {
|
||||||
|
if ($authMethod === self::AUTH_TOKEN && ($tokenEncrypted === null || $tokenEncrypted === '')) {
|
||||||
|
throw new IntegrationConfigException('Podaj token Bearer SMSPLANET.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authMethod !== self::AUTH_KEY_PASSWORD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyEncrypted === null || $keyEncrypted === '') {
|
||||||
|
throw new IntegrationConfigException('Podaj klucz API SMSPLANET.');
|
||||||
|
}
|
||||||
|
if ($passwordEncrypted === null || $passwordEncrypted === '') {
|
||||||
|
throw new IntegrationConfigException('Podaj haslo API SMSPLANET.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
private function updateSettingsRow(
|
||||||
|
string $authMethod,
|
||||||
|
?string $tokenEncrypted,
|
||||||
|
?string $keyEncrypted,
|
||||||
|
?string $passwordEncrypted,
|
||||||
|
string $sender,
|
||||||
|
array $payload
|
||||||
|
): void {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'UPDATE smsplanet_integration_settings
|
||||||
|
SET auth_method = :auth_method,
|
||||||
|
api_token_encrypted = :api_token_encrypted,
|
||||||
|
api_key_encrypted = :api_key_encrypted,
|
||||||
|
api_password_encrypted = :api_password_encrypted,
|
||||||
|
sender = :sender,
|
||||||
|
clear_polish = :clear_polish,
|
||||||
|
transactional = :transactional,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = 1'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'auth_method' => $authMethod,
|
||||||
|
'api_token_encrypted' => $tokenEncrypted,
|
||||||
|
'api_key_encrypted' => $keyEncrypted,
|
||||||
|
'api_password_encrypted' => $passwordEncrypted,
|
||||||
|
'sender' => $sender,
|
||||||
|
'clear_polish' => !empty($payload['clear_polish']) ? 1 : 0,
|
||||||
|
'transactional' => !empty($payload['transactional']) ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateIntegrationActive(int $integrationId, bool $isActive): void
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'UPDATE integrations
|
||||||
|
SET is_active = :is_active,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id AND type = :type'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'id' => $integrationId,
|
||||||
|
'type' => self::INTEGRATION_TYPE,
|
||||||
|
'is_active' => $isActive ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasEncryptedValue(mixed $value): bool
|
||||||
|
{
|
||||||
|
return trim((string) $value) !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decryptValue(string $encrypted): string
|
||||||
|
{
|
||||||
|
$value = StringHelper::nullableString($encrypted);
|
||||||
|
if ($value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string) $this->cipher->decrypt($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasCompleteCredentials(
|
||||||
|
string $authMethod,
|
||||||
|
string $sender,
|
||||||
|
string $apiToken,
|
||||||
|
string $apiKey,
|
||||||
|
string $apiPassword
|
||||||
|
): bool {
|
||||||
|
if ($sender === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authMethod === self::AUTH_TOKEN) {
|
||||||
|
return $apiToken !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $apiKey !== '' && $apiPassword !== '';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user