Skip to content

SRS — Messaging & Notifications

Module owner: Domain 7 — Communication. Two shared engines: (1) Messaging — customer↔vendor conversations always scoped to a business context (event, RFQ or booking); (2) the Notification engine — one catalog-driven service that every other module publishes events into. Engines are shared services: no module implements its own sending, ever.

Related: Quotations · Booking · Admin & CMS

Purpose

Keep every negotiation, question and file exchange inside Yam3at — organized per event, moderated for safety, and admissible when disputes arise — and deliver the right notification to the right person on the right channel without spamming anyone.

Actors

Actor Role
Customer Chats with vendors within their events' contexts; manages notification preferences
Vendor Chats with customers on received RFQs/bookings; manages preferences
Admin (support) Reads conversations only under an open dispute or abuse report; handles reports
System Delivers messages (WebSocket + push), scans content, renders and sends notifications

User stories

  • As a customer, I want one conversation per vendor per event, so that my wedding chats never mix with my office party chats.
  • As a customer, I want to send the venue floor plan as a PDF, so that the decorator quotes accurately.
  • As a vendor, I want read receipts, so that I know my quote clarification was seen.
  • As a vendor, I want a push the instant a new RFQ arrives, so that I'm first to respond.
  • As a user, I want to report an abusive message, so that the platform stays professional.
  • As a support admin, I want conversation access when a dispute is open, so that I can judge "the vendor promised X" claims — and only then.
  • As a user, I want to mute marketing notifications but keep booking alerts, so that I control my attention.

Functional requirements

Conversations — MVP

ID Requirement Phase
SRS-MSG-001 A Conversation is between exactly one customer and one vendor, created lazily on first message, and carries a mandatory context: context_type (rfq | booking | event_vendor) + context_id. One conversation per (customer, vendor, event) — RFQs and bookings of the same pair on the same event thread into it with context chips inline; a different event → a separate conversation. MVP
SRS-MSG-002 Message types: text (≤ 5,000 chars), image (jpg/png/webp ≤ 10 MB), file (PDF only ≤ 20 MB, MVP). Up to 5 attachments per message. System messages (quote sent, booking confirmed…) render inline as non-editable context cards. Voice notes = Scale. MVP
SRS-MSG-003 Real-time delivery over WebSockets (Laravel Reverb/Echo) with FCM push fallback when the recipient has no live connection. Messages persist first, then fan out — no at-most-once delivery. MVP
SRS-MSG-004 Read receipts: per-message delivered_at, read_at; sender sees single/double state. Conversation-level unread counters power badges and the event dashboard. MVP
SRS-MSG-005 Typing indicator over the socket channel, throttled to 1 signal per 3 s, auto-clears after 6 s. Never persisted. MVP
SRS-MSG-006 Sender can delete own message within 15 minutes ("deleted" placeholder remains; original retained internally 90 days for moderation). No editing in MVP. MVP
SRS-MSG-007 Conversation list per user: sorted by last activity, with search by vendor/customer name and event filter, archive (hide) without deleting. MVP

Safety & moderation — MVP

ID Requirement Phase
SRS-MSG-030 Blocked-words engine (shared with RFQ/quote text per SRS-RFQ business rule 8): admin-managed AR/EN term lists in two tiers — warn (profanity: message sent, flagged for moderation) and contact-leak patterns (phone regex [\d٠-٩]{8,}, emails, instagram/wa.me handles: sender warned inline "keep contact in Yam3at to stay protected", message delivered but flagged; 3 flags in 7 days → account review task). Blocking is deterrence + signal, not censorship theater. MVP
SRS-MSG-031 Report abuse: any participant reports a specific message or the whole conversation (reason enum: harassment, scam, spam, inappropriate, off-platform pressure + note). Creates a moderation case in the admin queue; reporter can also block the counterpart (blocks new messages both ways; existing history untouched; active bookings unaffected and flagged to support). MVP
SRS-MSG-032 Admin read access is dispute-gated: support/ops admins can open a conversation transcript only when it is attached to an open dispute or abuse report. Every access is audit-logged (admin, case ref, timestamp) and both participants' privacy notice discloses this policy. Super admin override exists but is logged identically. MVP

Notification engine — MVP core

ID Requirement Phase
SRS-MSG-040 Central engine: modules publish domain events; the engine maps event → catalog entry → audience → channels → localized template (AR/EN by recipient's locale) → delivery jobs on Redis queues, with per-channel providers (Mailgun/SES email, FCM push; SMS & WhatsApp Business API = Scale behind the same channel abstraction). MVP
SRS-MSG-041 Every notification also writes an in-app notification-center entry (bell icon, read/unread) regardless of channel outcomes. MVP
SRS-MSG-042 Delivery rules: retries with exponential backoff (3 attempts/channel); email respects suppression lists (bounces/complaints auto-suppress); push tokens pruned on FCM invalid-token responses; message-received pushes are collapsed per conversation (max 1 push/conversation/5 min, latest content). MVP
SRS-MSG-043 User preferences: per category (transactional, reminders, marketing) × per channel toggle. Transactional (payments, booking state, security) cannot be fully disabled — email stays locked on. Marketing is opt-in-first. Quiet hours (default 22:00–08:00 Asia/Kuwait) hold non-critical pushes. MVP
SRS-MSG-044 Template management: template_key + channel + locale versions with variables (e.g. {{vendor_name}}, {{amount}}); admin-editable via CMS with preview + test-send; missing-locale falls back EN with an alert. MVP

Notification-event catalog

Trigger Audience Channels (MVP) Template key Category
RFQ received Vendor Push, Email rfq.received Transactional
Quote received Customer Push, Email quote.received Transactional
Quote revised / counter answered Customer Push quote.revised Transactional
Negotiation counter received Vendor Push, Email quote.counter Transactional
Quote accepted (won) Vendor Push, Email quote.accepted Transactional
Quote not selected Vendor Push quote.not_selected Transactional
Quote/RFQ expiring in 24h Customer / Vendor Push rfq.expiring Reminder
RFQ/quote expired Both parties Push rfq.expired Transactional
Deposit due (booking created) Customer Push, Email booking.deposit_due Transactional
Deposit deadline in 12h Customer Push, Email booking.deposit_reminder Reminder
Deposit paid Vendor Push, Email booking.deposit_paid Transactional
Booking confirmed Customer Push, Email booking.confirmed Transactional
Booking cancelled / declined Both parties Push, Email booking.cancelled Transactional
Reschedule proposed / accepted Counterpart Push, Email booking.reschedule Transactional
Event date changed (has bookings) Booked vendors Push, Email event.date_changed Transactional
Completion check ("did vendor deliver?") Customer Push, Email booking.completion_check Transactional
Booking completed → review invite Customer Push, Email review.invite Transactional
Review published Vendor Push, Email review.published Transactional
Vendor replied to your review Customer Push review.replied Transactional
Payment succeeded (receipt) Payer Email (locked), Push payment.receipt Transactional
Payment failed Payer Push, Email payment.failed Transactional
Refund approved / processed / denied Customer Push, Email refund.status Transactional
Subscription renewal upcoming (D−7/−3/−1) Vendor Email, Push subscription.renewal_reminder Reminder
Subscription payment failed / grace Vendor Email, Push subscription.payment_failed Transactional
Subscription downgraded to Free Vendor Email, Push subscription.downgraded Transactional
Vendor application approved / rejected Vendor Email, Push vendor.approval_result Transactional
New message Recipient Push (collapsed) message.received Transactional
Abuse report resolved Reporter Push moderation.report_resolved Transactional
Checklist item due (Scale) Customer Push checklist.due Reminder
Event in 7 days summary Customer Push, Email event.week_out Reminder
Promos / announcements Opted-in users Email, Push (Scale: SMS/WhatsApp) marketing.campaign Marketing

Business rules

  1. No conversation without a business context — cold vendor-to-customer outreach does not exist.
  2. Vendors can initiate messages only on RFQs they received or bookings they hold; customers can initiate with any vendor via an RFQ or from a listing's "ask a question" (which creates an event_vendor context tied to a chosen event).
  3. Attachments live on private S3 with short-lived signed URLs; direct object URLs never exposed.
  4. Conversation history is retained while either party's account exists; account deletion anonymizes their side (name → "Deleted user") without destroying the counterpart's record.
  5. One notification per (event, recipient, template) occurrence — the engine deduplicates on an idempotency hash so job retries never double-send.
  6. Channel failures never fail business transactions: notification dispatch is strictly async post-commit.
  7. All templates exist in AR and EN before a catalog entry can be activated.

Validation rules

Field Rule
Message text 1–5,000 chars after trim; not only whitespace
Attachments ≤ 5 per message; image ≤ 10 MB (jpg/png/webp), PDF ≤ 20 MB; MIME sniffed server-side (not extension-trusted); images EXIF-stripped; all virus-scanned
Report reason enum required; note ≤ 1,000 chars
Preferences valid category × channel matrix; transactional email cannot be set off
Template valid variable set for its template_key; both locales present to activate
Push token FCM format, one active per device, refreshed on app launch

Permissions matrix

Action Participant (customer/vendor) Non-participant user Admin support Admin ops Admin super
Send/read messages in conversation
Delete own message (≤15 min)
Report / block
Read transcript (dispute/report open) ✔ (logged) ✔ (logged) ✔ (logged, incl. override)
Manage blocked-word lists
Manage templates & catalog
Send marketing campaign ✔ (approval by super)
Edit own notification preferences

Status / state machine

Message delivery lifecycle:

stateDiagram-v2
    [*] --> Sent : persisted
    Sent --> Delivered : recipient device/socket ack
    Delivered --> Read : recipient opened conversation
    Sent --> Deleted : sender deletes ≤ 15 min
    Delivered --> Deleted : sender deletes ≤ 15 min
    Read --> [*]
    Deleted --> [*]

Notification job: queued → sent → delivered | bounced | failed(retry ×3) → suppressed? per channel; in-app entry: unread → read.

Edge cases

  • Recipient blocked the sender: send returns 403 with a neutral "cannot message this user" (no block disclosure); system messages on active bookings still deliver.
  • Vendor at plan limit gets a message on an existing conversation: messaging is never plan-limited — only RFQ receipt is.
  • Attachment scan finds malware: message delivered without the attachment + "attachment removed" placeholder; sender notified; repeat → account review.
  • Push token invalid mid-fanout: token pruned silently; in-app entry still created; email fallback only if the catalog entry includes email.
  • Clock-skewed client timestamps: server time is authoritative; client times only for optimistic rendering.
  • Dispute closed while admin has transcript open: access re-checked per request; closed case → 403 on next page.
  • User disables push at OS level: engine still records in-app; a periodic "you're missing updates" email nudge (max 1/month) if transactional pushes consistently undeliverable.
  • Two devices, same user: read state syncs across devices via socket events; unread counters converge.

Empty / loading / error states

Surface Empty Loading Error
Conversation list "Conversations start from a quote request" + CTA to browse vendors Skeleton rows Cached list + retry banner
Conversation view Context card only ("You're talking about: {RFQ title}") Paginated history spinner (reverse infinite scroll, 50/page) Failed sends queue locally with retry/discard per message; offline banner
Notification center "You're all caught up" Skeleton rows Retry; badge count from cache
Preferences Toggle-level optimistic update Revert toggle + toast on save failure

Acceptance criteria

  • Message from customer reaches vendor's open socket in < 1 s p95 on staging; with app closed, FCM push arrives with collapsed conversation behavior verified (5 rapid messages → ≤ 1 push per 5 min window).
  • Read receipts and unread counters converge across two simultaneously connected devices.
  • A Kuwaiti phone number in Arabic-Indic digits (٩٩٨٨٧٧٦٦) triggers the contact-leak warning.
  • Support admin without an open dispute on a conversation receives 403 on transcript access; with one, access succeeds and the audit log shows the entry.
  • Every catalog row above has AR + EN templates and fires exactly once per trigger in the end-to-end suite (idempotency verified by replaying each domain event twice).
  • Quiet hours hold a reminder push at 23:00 and release it at 08:00 Asia/Kuwait; a payment receipt email at 23:00 sends immediately.
  • PDF attachment round-trip: upload → scan → signed URL renders in web (RTL layout intact) and Flutter.

Data entities touched

conversations, conversation_participants, messages, message_attachments, message_receipts, message_flags (blocked-word hits), abuse_reports, user_blocks, notification_catalog, notification_templates, notifications (in-app), notification_deliveries (per-channel attempts), notification_preferences, push_tokens, email_suppressions, admin_transcript_access_log; reads on events, quotation_requests, bookings, disputes.

GET    /api/v1/conversations?event_id=&archived=
POST   /api/v1/conversations                   # context_type + context_id (+ event) required
GET    /api/v1/conversations/{id}/messages?before=
POST   /api/v1/conversations/{id}/messages
DELETE /api/v1/messages/{id}                   # ≤ 15 min own message
POST   /api/v1/conversations/{id}/read         # mark read up to message
POST   /api/v1/conversations/{id}/archive
POST   /api/v1/messages/{id}/report
POST   /api/v1/users/{id}/block  |  DELETE /api/v1/users/{id}/block
WS     /ws  (channels: conversation.{id}, user.{id})
# notifications
GET    /api/v1/notifications?unread=1
POST   /api/v1/notifications/read-all
GET|PUT /api/v1/me/notification-preferences
POST   /api/v1/me/push-tokens  |  DELETE /api/v1/me/push-tokens/{token}
# admin
GET    /api/v1/admin/moderation/reports?status=open
GET    /api/v1/admin/conversations/{id}/transcript   # dispute-gated, logged
GET|PUT /api/v1/admin/notification-templates/{key}
GET|PUT /api/v1/admin/blocked-words