feat(121+122): smsplanet conversation, notifications, default footer

Phase 121 — SMSPLANET Conversation + Notifications:
- migration 20260512_000110 adds smsplanet conversation + notifications tables
- src/Modules/Sms (SmsConversationService, SmsMessageRepository, SmsplanetWebhookController)
- src/Modules/Notifications (Repository, Controller, ApiController)
- order SMS tab, notification center, sender mode, inbound webhook
- public notifications.js + layouts/app.php integration

Phase 122 — SMSPLANET Default SMS Footer:
- migration 20260512_000111 adds smsplanet_integration_settings.default_footer
- footer appended to test SMS and order SMS, validated against 918 char limit
- settings textarea + compact order SMS note when footer configured

Bundled (could not split per-phase without hunk staging):
- routes/web.php (also carries Phase 118 fakturownia redirects)
- DOCS/{ARCHITECTURE,DB_SCHEMA,TECH_CHANGELOG}.md (118 + 121 + 122 entries)
- .paul/codebase/{architecture,db_schema,tech_changelog}.md (118 + 121 + 122)
- .paul/STATE.md, ROADMAP.md, changelog/2026-05-12.md (UNIFY closure)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-12 20:37:41 +02:00
parent 8f14851d85
commit 360eef128d
34 changed files with 2538 additions and 128 deletions

View File

@@ -22,6 +22,9 @@ use App\Modules\Automation\AutomationService;
use App\Modules\Settings\ShopproApiClient;
use App\Modules\Settings\ShopproIntegrationsRepository;
use App\Modules\Shipments\ShipmentPackageRepository;
use App\Modules\Sms\SmsConversationService;
use App\Modules\Sms\SmsMessageRepository;
use Throwable;
final class OrdersController
{
@@ -41,7 +44,9 @@ final class OrdersController
private readonly ?ShopproIntegrationsRepository $shopproIntegrations = null,
private readonly ?AutomationService $automation = null,
private readonly ?InvoiceRepository $invoiceRepo = null,
private readonly ?InvoiceConfigRepository $invoiceConfigRepo = null
private readonly ?InvoiceConfigRepository $invoiceConfigRepo = null,
private readonly ?SmsMessageRepository $smsMessages = null,
private readonly ?SmsConversationService $smsConversation = null
) {
}
@@ -247,6 +252,9 @@ final class OrdersController
$flashError = (string) Flash::get('order.error', '');
$customerRiskInfo = $this->buildCustomerRiskInfo($order, $orderId);
$smsMessages = $this->smsMessages !== null ? $this->smsMessages->findByOrderId($orderId) : [];
$smsPhone = $this->resolveSmsPhone($order, $addresses);
$smsDefaultFooterConfigured = $this->smsConversation !== null && $this->smsConversation->hasDefaultFooter();
$html = $this->template->render('orders/show', [
'title' => $this->translator->get('orders.details.title') . ' #' . $orderId,
@@ -279,11 +287,50 @@ final class OrdersController
'emailTemplates' => $emailTemplates,
'emailMailboxes' => $emailMailboxes,
'customerRiskInfo' => $customerRiskInfo,
'smsMessages' => $smsMessages,
'smsPhone' => $smsPhone,
'smsDefaultFooterConfigured' => $smsDefaultFooterConfigured,
], 'layouts/app');
return Response::html($html);
}
public function sendSms(Request $request): Response
{
$orderId = max(0, (int) $request->input('id', 0));
$redirectTo = '/orders/' . $orderId . '?tab=sms';
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('order.error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
if ($orderId <= 0 || $this->smsConversation === null) {
Flash::set('order.error', 'Modul SMS nie jest dostepny.');
return Response::redirect($redirectTo);
}
try {
$user = $this->auth->user();
$userId = is_array($user) ? (int) ($user['id'] ?? 0) : 0;
$result = $this->smsConversation->sendFromOrder(
$orderId,
(string) $request->input('phone', ''),
(string) $request->input('message', ''),
$userId > 0 ? $userId : null
);
if ($result['ok']) {
Flash::set('order.success', 'SMS zostal wyslany.');
} else {
Flash::set('order.error', 'Nie udalo sie wyslac SMS: ' . $result['message']);
}
} catch (Throwable $exception) {
Flash::set('order.error', 'Nie udalo sie wyslac SMS: ' . $exception->getMessage());
}
return Response::redirect($redirectTo);
}
/**
* Sklada informacje o historii zwrotow klienta biezacego zamowienia.
*
@@ -315,6 +362,32 @@ final class OrdersController
];
}
/**
* @param array<string, mixed> $order
* @param array<int, array<string, mixed>> $addresses
*/
private function resolveSmsPhone(array $order, array $addresses): string
{
$buyerPhone = trim((string) ($order['buyer_phone'] ?? ''));
if ($buyerPhone !== '') {
return $buyerPhone;
}
foreach (['customer', 'delivery', 'invoice'] as $wantedType) {
foreach ($addresses as $address) {
if ((string) ($address['address_type'] ?? '') !== $wantedType) {
continue;
}
$phone = trim((string) ($address['phone'] ?? ''));
if ($phone !== '') {
return $phone;
}
}
}
return '';
}
private function composeCustomerRiskText(int $count, string $email, string $phone, string $name): string
{
if ($count <= 0) {