feat(email): per-typ preferencje powiadomień e-mail (D.1 dopełnienie)
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Symetria z push — panel /konto/prywatnosc rozszerzony o 3 dodatkowe
toggle w karcie "Powiadomienia e-mail":
- Pytanie pod moim ogłoszeniem B2B (notify_email_classified_question)
- Odpowiedź pod moim pytaniem B2B (notify_email_classified_answer)
- Ogłoszenie wygasa za 3 dni (notify_email_classified_expiry)

Migracja 102 dodaje kolumny (default TRUE — nie zmienia zachowania
istniejących userów). Endpointy ask_question / answer_question teraz
czytają dedykowaną flagę zamiast notify_email_messages (która zostaje
tylko dla wiadomości prywatnych). Skrypt classified_expiry_notifier.py
pomija userów z wyłączonym notify_email_classified_expiry.

W kolejnych sub-fazach D.2/D.3 symetrycznie dojdą triggery e-mail +
toggle dla forum/broadcast/wydarzeń — z defaults dobranymi tak, by
nie zalać inbox użytkowników (broadcast OFF, personalne ON).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-14 17:50:41 +02:00
parent 46adb0ece7
commit 3f1e66d3ca
6 changed files with 58 additions and 4 deletions

View File

@ -914,6 +914,9 @@ def konto_prywatnosc():
user.contact_prefer_phone = request.form.get('prefer_phone') == 'on'
user.contact_prefer_portal = request.form.get('prefer_portal') == 'on'
user.notify_email_messages = request.form.get('notify_email_messages') == 'on'
user.notify_email_classified_question = request.form.get('notify_email_classified_question') == 'on'
user.notify_email_classified_answer = request.form.get('notify_email_classified_answer') == 'on'
user.notify_email_classified_expiry = request.form.get('notify_email_classified_expiry') == 'on'
# Web Push preferences per event type
user.notify_push_messages = request.form.get('notify_push_messages') == 'on'

View File

@ -635,7 +635,7 @@ def ask_question(classified_id):
create_classified_question_notification(
classified_id, classified.title, questioner_name, classified.author_id)
author = db.query(User).filter(User.id == classified.author_id).first()
if author and author.email and author.notify_email_messages != False:
if author and author.email and author.notify_email_classified_question is not False:
send_classified_question_email(
classified_id, classified.title, questioner_name, content,
author.email, author.name or author.email.split('@')[0])
@ -720,7 +720,7 @@ def answer_question(classified_id, question_id):
create_classified_answer_notification(
classified_id, classified.title, answerer_name, question.author_id)
q_author = db.query(User).filter(User.id == question.author_id).first()
if q_author and q_author.email and q_author.notify_email_messages != False:
if q_author and q_author.email and q_author.notify_email_classified_answer is not False:
send_classified_answer_email(
classified_id, classified.title, answerer_name, answer,
q_author.email, q_author.name or q_author.email.split('@')[0])

View File

@ -337,8 +337,11 @@ class User(Base, UserMixin):
contact_prefer_portal = Column(Boolean, default=True) # User prefers portal messages
contact_note = Column(Text, nullable=True) # Additional note (e.g. best hours)
# Email notification preferences
notify_email_messages = Column(Boolean, default=True) # Email when receiving private message
# Email notification preferences (per event type)
notify_email_messages = Column(Boolean, default=True) # Prywatna wiadomość
notify_email_classified_question = Column(Boolean, default=True) # Pytanie pod moim ogłoszeniem B2B
notify_email_classified_answer = Column(Boolean, default=True) # Odpowiedź pod moim pytaniem B2B
notify_email_classified_expiry = Column(Boolean, default=True) # Moje ogłoszenie wygasa za 3 dni
# Web Push notification preferences (per event type)
notify_push_messages = Column(Boolean, default=True) # Prywatna wiadomość

View File

@ -0,0 +1,10 @@
-- Migration 102: per-event e-mail notification preferences
--
-- Dotąd użytkownik miał jedną flagę notify_email_messages (używana także przez
-- classified question/answer). Rozbijamy na dedykowane flagi per typ e-maila.
-- Wszystkie default TRUE — zachowujemy obecne zachowanie dla istniejących userów.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS notify_email_classified_question BOOLEAN DEFAULT TRUE,
ADD COLUMN IF NOT EXISTS notify_email_classified_answer BOOLEAN DEFAULT TRUE,
ADD COLUMN IF NOT EXISTS notify_email_classified_expiry BOOLEAN DEFAULT TRUE;

View File

@ -50,6 +50,11 @@ def main():
if not author or not author.email:
continue
# Respect user preference
if getattr(author, 'notify_email_classified_expiry', True) is False:
print(f" [pomijam] {classified.title} -> {author.email} (wyłączył powiadomienia e-mail o wygasaniu)")
continue
author_name = author.name or author.email.split('@')[0]
expire_date = classified.expires_at.strftime('%d.%m.%Y')
extend_url = f"https://nordabiznes.pl/tablica/{classified.id}"

View File

@ -367,6 +367,39 @@
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Pytanie do Twojego ogłoszenia B2B</div>
<div class="setting-description">E-mail gdy ktoś zada publiczne pytanie pod Twoim ogłoszeniem</div>
</div>
<label class="toggle-switch">
<input type="checkbox" name="notify_email_classified_question" {% if user.notify_email_classified_question != False %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Odpowiedź na Twoje pytanie B2B</div>
<div class="setting-description">E-mail gdy autor ogłoszenia odpowie na Twoje pytanie</div>
</div>
<label class="toggle-switch">
<input type="checkbox" name="notify_email_classified_answer" {% if user.notify_email_classified_answer != False %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Twoje ogłoszenie wygasa za 3 dni</div>
<div class="setting-description">Przypomnienie e-mailem gdy Twoje ogłoszenie B2B zbliża się do daty wygaśnięcia (abyś mógł je przedłużyć)</div>
</div>
<label class="toggle-switch">
<input type="checkbox" name="notify_email_classified_expiry" {% if user.notify_email_classified_expiry != False %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="settings-card">