--- phase: 15-email-sending plan: 01 type: execute wave: 1 depends_on: [] files_modified: - composer.json - composer.lock - src/Modules/Email/EmailSendingService.php - src/Modules/Email/VariableResolver.php - src/Modules/Email/AttachmentGenerator.php - src/Modules/Orders/OrdersController.php - resources/views/orders/show.php - resources/scss/modules/_email-send.scss - resources/views/orders/partials/email-send-modal.php - routes/web.php - DOCS/DB_SCHEMA.md - DOCS/ARCHITECTURE.md - DOCS/TECH_CHANGELOG.md autonomous: false --- ## Goal Zaimplementować pełny flow wysyłki e-mail z widoku zamówienia: wybór szablonu, podgląd z rozwiązanymi zmiennymi, wysyłka przez SMTP (PHPMailer), automatyczne załączniki (paragon PDF), logowanie do email_logs. ## Purpose Zamyka milestone v0.4 Moduł E-mail — sprzedawca może wysyłać maile do kupujących bezpośrednio z zamówienia, z gotowymi szablonami i załącznikami, bez opuszczania panelu. ## Output - PHPMailer jako zależność composer - EmailSendingService: resolwer zmiennych, generowanie załączników, wysyłka SMTP - Modal wysyłki e-mail na widoku zamówienia (wybór szablonu, podgląd, wysyłka) - Endpoint POST /orders/{id}/send-email - Logowanie wysyłek w email_logs z wyświetleniem w zakładce Dokumenty ## Project Context @.paul/PROJECT.md @.paul/ROADMAP.md @.paul/STATE.md ## Prior Work @.paul/phases/13-email-mailboxes/13-01-SUMMARY.md — DB foundation, SMTP mailbox CRUD, IntegrationSecretCipher @.paul/phases/14-email-templates/14-01-SUMMARY.md — Template CRUD, Quill.js, VARIABLE_GROUPS, ATTACHMENT_TYPES ## Source Files @src/Modules/Settings/EmailMailboxRepository.php @src/Modules/Settings/EmailTemplateRepository.php @src/Modules/Settings/EmailTemplateController.php — VARIABLE_GROUPS, SAMPLE_DATA, ATTACHMENT_TYPES @src/Modules/Settings/IntegrationSecretCipher.php @src/Modules/Orders/OrdersController.php @src/Modules/Accounting/ReceiptController.php — pdf() method, dompdf pattern @src/Modules/Accounting/ReceiptRepository.php @resources/views/orders/show.php @routes/web.php @database/migrations/20260315_000056_create_email_logs_table.sql ## Required Skills (from SPECIAL-FLOWS.md) | Skill | Priority | When to Invoke | Loaded? | |-------|----------|----------------|---------| | sonar-scanner | required | Po APPLY, przed UNIFY | ○ | ## Skill Invocation Checklist - [ ] sonar-scanner loaded (run after APPLY) ## AC-1: Wysyłka e-mail z zamówienia ```gherkin Given użytkownik jest na widoku zamówienia z adresem e-mail kupującego When kliknie "Wyślij e-mail", wybierze szablon i kliknie "Wyślij" Then e-mail zostanie wysłany na adres kupującego przez skonfigurowaną skrzynkę SMTP And zmienne w szablonie ({{zamowienie.numer}}, {{kupujacy.imie_nazwisko}} itd.) zostaną zastąpione danymi zamówienia ``` ## AC-2: Załącznik paragon PDF ```gherkin Given szablon ma ustawiony attachment_1 = 'receipt' And zamówienie ma wystawiony paragon When użytkownik wyśle e-mail z tym szablonem Then do maila zostanie dołączony paragon w formacie PDF And nazwa pliku to numer_paragonu.pdf ``` ## AC-3: Logowanie wysyłek ```gherkin Given użytkownik wysłał e-mail z zamówienia When otworzy zakładkę Dokumenty na widoku zamówienia Then zobaczy wpis z datą, tematem, odbiorcą, statusem (sent/failed) i opcją podglądu ``` ## AC-4: Podgląd przed wysyłką ```gherkin Given użytkownik wybrał szablon w modalu wysyłki When kliknie "Podgląd" Then zobaczy temat i treść z rozwiązanymi zmiennymi (dane z aktualnego zamówienia) And zobaczy informację o załącznikach (jeśli dostępne) ``` ## AC-5: Walidacja i obsługa błędów ```gherkin Given brak skonfigurowanej skrzynki SMTP lub brak aktywnych szablonów When użytkownik kliknie "Wyślij e-mail" Then zobaczy komunikat o braku konfiguracji (nie modal wysyłki) Given wysyłka SMTP nie powiedzie się When błąd zostanie zwrócony przez PHPMailer Then email_logs zapisze status='failed' z error_message And użytkownik zobaczy komunikat o błędzie ``` Task 1: PHPMailer + EmailSendingService z resolwerem zmiennych i załącznikami composer.json, composer.lock, src/Modules/Email/EmailSendingService.php, src/Modules/Email/VariableResolver.php, src/Modules/Email/AttachmentGenerator.php 1. `composer require phpmailer/phpmailer` (v6.x) 2. Utworzyć `src/Modules/Email/VariableResolver.php`: - Metoda `resolve(string $template, array $orderData): string` - Zamienia `{{grupa.zmienna}}` na wartości z danych zamówienia - Mapowanie zmiennych (reuse logiki z EmailTemplateController::VARIABLE_GROUPS): - zamowienie: numer (external_id lub id), numer_zewnetrzny, zrodlo (source), kwota (total_amount), waluta (currency), data (created_at formatted) - kupujacy: imie_nazwisko, email, telefon, login — z buyer_* pól zamówienia - adres: ulica, miasto, kod_pocztowy, kraj — z delivery address - firma: nazwa, nip — z company_settings - Zmienne bez wartości → pusty string (nie zostawiać {{...}}) - Metoda `buildVariableMap(array $order, array $companySettings): array` — buduje płaską mapę 'grupa.zmienna' => wartość 3. Utworzyć `src/Modules/Email/AttachmentGenerator.php`: - Metoda `generate(string $type, array $order): ?array` → ['filename' => '...', 'content' => '...binary...', 'mime' => 'application/pdf'] - Dla type='receipt': - Pobrać paragon z ReceiptRepository::findByOrderId($orderId) - Jeśli brak paragonu → return null (nie blokuje wysyłki, po prostu brak załącznika) - Renderować PDF tym samym wzorcem co ReceiptController::pdf() — Dompdf z widoku receipts/print - Filename: str_replace(['/', '\\'], '_', $receiptNumber) . '.pdf' - Rozszerzalność: switch($type) z case 'receipt', default → null 4. Utworzyć `src/Modules/Email/EmailSendingService.php`: - Konstruktor: Medoo $db, VariableResolver, AttachmentGenerator, IntegrationSecretCipher, TemplateEngine - Metoda `send(int $orderId, int $templateId, ?int $mailboxId = null): array` - Pobrać zamówienie (z buyer, address, items) - Pobrać szablon z EmailTemplateRepository - Pobrać skrzynkę: jeśli mailboxId podany → użyj; jeśli szablon ma mailbox_id → użyj; fallback → domyślna (is_default=1) - Jeśli brak skrzynki → throw RuntimeException('Brak skonfigurowanej skrzynki SMTP') - Pobrać company_settings dla zmiennych firma.* - Rozwiązać zmienne w subject i body_html przez VariableResolver - Wygenerować załączniki przez AttachmentGenerator (jeśli attachment_1 set) - Wysłać przez PHPMailer: - SMTP auth z odszyfrowanym hasłem (IntegrationSecretCipher::decrypt) - SMTPSecure = mailbox.smtp_encryption (tls/ssl/none→'') - Port = mailbox.smtp_port - From = mailbox.sender_email / sender_name - To = order buyer email - Subject = resolved subject - Body = resolved body_html (isHTML = true) - Attachments: addStringAttachment() dla każdego wygenerowanego - Zalogować do email_logs: template_id, mailbox_id, order_id, recipient_email, recipient_name, subject, body_html (resolved), attachments_json, status (sent/failed), error_message, sent_at - Return: ['success' => bool, 'error' => ?string, 'log_id' => int] - Metoda `preview(int $orderId, int $templateId): array` - Rozwiązuje zmienne i zwraca ['subject' => '...', 'body_html' => '...', 'attachments' => [...nazwy]] - NIE wysyła maila Avoid: - Nie używać natywnej funkcji mail() — tylko PHPMailer SMTP - Nie duplikować logiki zmiennych z EmailTemplateController — wydzielić do VariableResolver i docelowo zastąpić SAMPLE_DATA w kontrolerze referencją - Nie tworzyć plików tymczasowych na dysku dla załączników — używać addStringAttachment (in-memory) - composer show phpmailer/phpmailer zwraca wersję 6.x - php -l src/Modules/Email/EmailSendingService.php — brak błędów składni - php -l src/Modules/Email/VariableResolver.php — brak błędów składni - php -l src/Modules/Email/AttachmentGenerator.php — brak błędów składni AC-1 (wysyłka SMTP), AC-2 (załącznik paragon), AC-5 (obsługa błędów) — backend ready Task 2: Modal wysyłki e-mail + endpoint + logowanie + wyświetlanie w Dokumentach src/Modules/Orders/OrdersController.php, resources/views/orders/show.php, resources/views/orders/partials/email-send-modal.php, resources/scss/modules/_email-send.scss, routes/web.php, DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md 1. Dodać route w routes/web.php: - POST /orders/{id}/send-email → [$ordersController, 'sendEmail'] - POST /orders/{id}/email-preview → [$ordersController, 'emailPreview'] 2. W OrdersController dodać metody: - `sendEmail(Request $request)`: - CSRF validation (_token) - Pobrać template_id, mailbox_id (opcjonalny) z POST - Wywołać EmailSendingService::send() - Flash message sukces/błąd - JSON response (AJAX): ['success' => bool, 'message' => '...'] - `emailPreview(Request $request)`: - Pobrać template_id z POST - Wywołać EmailSendingService::preview() - JSON response: ['subject' => '...', 'body_html' => '...', 'attachments' => [...]] 3. Utworzyć `resources/views/orders/partials/email-send-modal.php`: - Modal overlay z klasą .email-send-modal - Formularz: - Select "Szablon" — lista aktywnych szablonów (przekazane z kontrolera) - Select "Skrzynka" — lista aktywnych skrzynek (z opcją "Domyślna z szablonu") - Readonly "Odbiorca" — buyer email z zamówienia - Przycisk "Podgląd" — AJAX POST /orders/{id}/email-preview → wyświetla resolved subject + body w div.preview - Div .email-preview-area — ukryty domyślnie, pokazuje podgląd subject + body + lista załączników - Przycisk "Wyślij" — AJAX POST /orders/{id}/send-email - Przycisk "Anuluj" — zamyka modal - CSRF token hidden input - JS: fetch API do podglądu i wysyłki, loading states, error handling - Po udanej wysyłce: zamknij modal, pokaż OrderProAlerts.success(), odśwież sekcję dokumentów (lub reload) 4. W resources/views/orders/show.php: - Dodać przycisk "Wyślij e-mail" w .order-details-actions (po "Wystaw paragon"): - `` - Przycisk aktywny tylko jeśli: są aktywne szablony + aktywne skrzynki + zamówienie ma buyer email - Jeśli brak konfiguracji → btn--disabled z title="Skonfiguruj skrzynkę i szablony w Ustawieniach" - Include modal partial: `` - W zakładce "documents" dodać sekcję "Wysłane e-maile": - Tabela: Data | Temat | Odbiorca | Status | Akcje - Status: badge sent (zielony) / failed (czerwony) - Akcje: przycisk "Podgląd" otwierający modal z body_html z logu - Dane z email_logs WHERE order_id = current order - W kontrolerze show(): załadować email_logs dla zamówienia + aktywne szablony + aktywne skrzynki 5. Style SCSS w resources/scss/modules/_email-send.scss: - .email-send-modal — overlay + centered card (wzorować na istniejących modalach w projekcie) - .email-preview-area — border, padding, max-height z overflow-y scroll - .email-log-status--sent / --failed badges - Import w głównym SCSS 6. Zaktualizować DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md: - Nowy moduł Email (EmailSendingService, VariableResolver, AttachmentGenerator) - Nowe endpointy /orders/{id}/send-email i /email-preview - PHPMailer jako nowa zależność Avoid: - Nie dodawać natywnych alert()/confirm() — używać OrderProAlerts - Nie trzymać styli w pliku widoku — tylko SCSS - Nie tworzyć nowego taba na widoku zamówienia — wyświetlać emaile w istniejącej zakładce Dokumenty - Nie dodawać bulk email z listy zamówień — tylko z widoku pojedynczego zamówienia - php -l src/Modules/Orders/OrdersController.php — brak błędów składni - Przycisk "Wyślij e-mail" widoczny na /orders/{id} - Modal otwiera się po kliknięciu - Podgląd pokazuje rozwiązane zmienne - Po wysyłce email_logs ma nowy rekord - Zakładka Dokumenty pokazuje historię wysyłek AC-1 (pełny flow UI), AC-3 (logowanie i wyświetlanie), AC-4 (podgląd), AC-5 (walidacja UI) satisfied Pełny flow wysyłki e-mail z widoku zamówienia: wybór szablonu, podgląd ze zmiennymi, wysyłka SMTP z załącznikiem paragon PDF, logowanie i historia w zakładce Dokumenty. 1. Otwórz zamówienie z adresem e-mail kupującego: /orders/{id} 2. Sprawdź przycisk "Wyślij e-mail" w pasku akcji 3. Kliknij — powinien otworzyć się modal z wyborem szablonu i skrzynki 4. Wybierz szablon → kliknij "Podgląd" → sprawdź czy zmienne zostały rozwiązane (numer zamówienia, dane kupującego) 5. Jeśli szablon ma załącznik "Paragon" i zamówienie ma paragon → podgląd powinien pokazać załącznik 6. Kliknij "Wyślij" → sprawdź czy mail dotarł na skrzynkę odbiorcy 7. Sprawdź zakładkę Dokumenty → sekcja "Wysłane e-maile" powinna pokazywać nowy wpis ze statusem "sent" 8. Przetestuj błąd: zmień SMTP na nieprawidłowy → wyślij → status powinien być "failed" z error_message 9. Przetestuj brak konfiguracji: usuń skrzynki → przycisk powinien być nieaktywny z tooltipem Type "approved" to continue, or describe issues to fix ## DO NOT CHANGE - database/migrations/* — tabele email_logs, email_mailboxes, email_templates już istnieją (Phase 13-14) - src/Modules/Settings/EmailMailboxController.php — CRUD skrzynek bez zmian - src/Modules/Settings/EmailTemplateController.php — CRUD szablonów bez zmian (poza ewentualnym reuse VARIABLE_GROUPS) - src/Modules/Accounting/* — moduł paragonów bez zmian (tylko odczyt z ReceiptRepository) ## SCOPE LIMITS - Brak bulk email z listy zamówień — tylko z widoku pojedynczego zamówienia - Brak kolejki/retry — wysyłka synchroniczna (async/cron to przyszły milestone) - Brak edycji treści maila w modalu — tylko wybór szablonu i podgląd - Brak nowych tabel/migracji — wykorzystanie istniejącej email_logs Before declaring plan complete: - [ ] composer show phpmailer/phpmailer zwraca 6.x - [ ] php -l na wszystkich nowych/zmodyfikowanych plikach PHP — brak błędów - [ ] Przycisk "Wyślij e-mail" widoczny na widoku zamówienia - [ ] Modal otwiera się, podgląd rozwiązuje zmienne - [ ] Wysyłka SMTP działa (mail dociera do odbiorcy) - [ ] Załącznik paragon PDF dołączany gdy szablon ma attachment_1='receipt' - [ ] email_logs zapisuje wpis z poprawnym statusem - [ ] Zakładka Dokumenty wyświetla historię emaili - [ ] DOCS zaktualizowane - [ ] Wszystkie acceptance criteria spełnione - All tasks completed - All verification checks pass - No PHP errors or warnings introduced - E-mail z zamówienia działa end-to-end (szablon → podgląd → wysyłka → log → historia) - Paragon PDF jako załącznik działa (gdy dostępny) - Brak regresji w istniejących modułach (zamówienia, paragony, szablony, skrzynki) After completion, create `.paul/phases/15-email-sending/15-01-SUMMARY.md`