Skip to content

fix: Humanize Novu notification messages for non-technical users #536

Description

@teetangh

Problem

The current Novu in-app notifications display developer-facing terminology that is confusing and unprofessional for non-technical users. Since Familiarise targets a broad professional audience (management, marketing, sales, mechanical engineers, etc.), notification copy must be human-friendly.

Examples from current notifications

Current message Problem
"Priya Test Consultee rescheduled the subscription for Full-Stack Mentorship Program from to" Missing date values (oldDateTime/newDateTime not populated), raw template leaking through
"consultee cancelled the CONSULTATION session for System Design Deep Dive. Reason: SCHEDULE_CONFLICT" Uses role name "consultee" instead of actual user name, uppercase enum CONSULTATION, raw enum SCHEDULE_CONFLICT
"consultant cancelled the CONSULTATION session for System Design Deep Dive. Reason: SCHEDULE_CONFLICT" Uses role name "consultant" instead of actual user name, uppercase enum values

Root causes

  1. Novu Dashboard templates use raw {{payload.*}} values without transformation (e.g., {{payload.appointmentType}} renders as "CONSULTATION" not "consultation")
  2. Payload construction in API routes passes raw enum values and role labels instead of human-readable strings
  3. Missing payload values (e.g., oldDateTime/newDateTime in reschedule notifications are not populated)
  4. Fallback strings like "Consultant", "Consultee", "N/A", "Unknown" leak into notifications when data is missing

Complete Notification Audit

Codebase architecture

  • Workflow definitions: lib/novu/workflows.ts (35 workflow IDs + typed payloads)
  • Trigger service: lib/novu/service.ts (all notify*() functions)
  • Novu Dashboard template specs: docs/notifications/03-novu-template-specs.md
  • Subscriber sync: lib/novu/subscriber.ts, hooks/useNovuSubscriberSync.ts
  • Frontend inbox: components/notifications/NotificationInbox.tsx, providers/NovuProvider.tsx

All notification trigger points and current payloads

1. Appointment Booked (appointment-booked)

  • File: lib/payments/webhooks/handlers.ts (line ~514)
  • Current payload issues:
    • appointmentType: passes raw metadata.appointmentType (e.g., "CONSULTATION", "SUBSCRIPTION")
    • planTitle: incorrectly passes metadata.planId (a UUID!) as fallback for plan title
    • consulteeName: falls back to "User"
  • Template: Your {{payload.appointmentType}} "{{payload.planTitle}}" has been booked for {{payload.dateTime}}.

2. Appointment Cancelled (appointment-cancelled)

  • File: app/api/appointments/[appointmentId]/cancel/route.ts (line ~258)
  • Current payload issues:
    • appointmentType: passes raw DB enum value (e.g., "CONSULTATION", "SUBSCRIPTION")
    • reason: passes raw CancellationReason enum (e.g., "SCHEDULE_CONFLICT", "CONSULTANT_UNAVAILABLE")
    • cancelledBy: passes "consultant" or "consultee" (role labels, not names)
    • consulteeName: falls back to "Consultee"
    • consultantName: falls back to "Consultant"
    • No webinar/class participant names extracted
  • Template: Your {{payload.appointmentType}} "{{payload.planTitle}}" has been cancelled{{#if payload.reason}}: {{payload.reason}}{{/if}}.
  • Also triggered from: actions/maintenance/freeze-appointments.ts (with cancelledBy: "system", reason: "Scheduled platform maintenance")

3. Appointment Rescheduled (appointment-rescheduled)

  • File: app/api/appointments/[appointmentId]/reschedule/route.ts (line ~399)
  • Current payload issues:
    • oldDateTime and newDateTime are NOT populated at all (missing from payload construction)
    • appointmentType: passes lowercase "consultation" or "subscription" (inconsistent with cancel route which passes uppercase)
    • consulteeName: falls back to "Consultee"
    • consultantName: falls back to "Consultant"

4. Appointment Reminder (appointment-reminder)

  • File: scripts/appointments/send-appointment-reminders.ts (line ~212)
  • Current payload issues:
    • appointmentType: passes lowercase ("consultation", "subscription", "webinar", "class")
    • consulteeName: falls back to "Consultee" (inappropriate for webinar/class where there may be many participants)
  • Template: Reminder: Your {{payload.appointmentType}} "{{payload.planTitle}}" is coming up on {{payload.dateTime}}.

5. Appointment Completed (appointment-completed)

  • File: scripts/appointments/auto-complete-appointments.ts (lines ~284, ~405)
  • Current payload issues:
    • appointmentType: passes lowercase ("consultation", "subscription")
    • consulteeName: falls back to "Consultee"
    • consultantName: falls back to "Consultant"

6. Payment Success (payment-success)

  • File: lib/payments/webhooks/handlers.ts (line ~499)
  • Current payload issues:
    • appointmentType: passes raw metadata.appointmentType
    • planTitle: passes metadata.planId (UUID!) as fallback -- critical bug
    • consultantName: falls back to "Consultant"

7. Payment Failed (payment-failed)

  • File: lib/payments/webhooks/handlers.ts (referenced in service, may be in Stripe/Razorpay webhook handlers)
  • Payload issues:
    • failureReason: likely passes raw gateway error strings
    • appointmentType: raw enum value

8. Refund Processed (refund-processed)

  • File: app/api/webhooks/utils.ts (line ~212)
  • Current payload: minimal -- only amount, currency, dashboardUrl
  • Issues: No human-friendly context about what was refunded

9. Refund Requested (refund-requested)

  • Defined in service but no active call site found -- may not be triggered yet

10. Support Ticket Created (support-ticket-created)

  • File: app/api/user/support-tickets/route.ts (line ~188)
  • Payload: ticketId, ticketTitle, dashboardUrl
  • Issues: Relatively clean, but ticket ID is raw UUID

11. Support Ticket Update (support-ticket-update)

  • File: app/api/staff/support-tickets/[ticketId]/route.ts (line ~272)
  • Payload issues:
    • status: passes raw DB enum (e.g., "IN_PROGRESS", "RESOLVED")

12. Support Ticket Response (support-ticket-response)

  • File: app/api/staff/support-tickets/[ticketId]/responses/route.ts (line ~93)
  • Payload: Clean -- ticketTitle, message, dashboardUrl

13. Feedback Received (feedback-received)

  • File: app/api/user/feedbacks/route.ts (line ~82)
  • Payload issues:
    • category: passes raw DB enum value

14. New Review Received (new-review-received)

  • File: app/api/user/reviews/route.ts (line ~135)
  • Payload: Relatively clean -- reviewerName, rating, comment

15. Trial Session Requested (trial-session-requested)

  • File: app/api/trials/route.ts (line ~349)
  • Payload issues:
    • status: passes raw enum "PENDING"

16. Trial Session Scheduled (trial-session-scheduled)

  • File: app/api/trials/[trialId]/route.ts (line ~419)
  • Payload issues:
    • status: passes raw enum "SCHEDULED"

17. Trial Session Completed (trial-session-completed)

  • File: app/api/trials/[trialId]/route.ts (line ~458)
  • Payload issues:
    • status: passes raw enum "COMPLETED"

18. Trial Session Cancelled (trial-session-cancelled)

  • File: app/api/trials/[trialId]/route.ts (line ~479)
  • Payload issues:
    • status: passes raw enum value ("CANCELLED" or "REJECTED")

19. Subscription Started (subscription-started)

  • Files: app/api/events/subscriptions/route.ts (line ~255), app/api/events/subscriptions/[subscriptionId]/route.ts
  • Payload: Relatively clean

20. Subscription Cancelled (subscription-cancelled)

  • Files: app/api/events/subscriptions/route.ts (line ~276), app/api/events/subscriptions/[subscriptionId]/route.ts
  • Payload: Relatively clean

21. Subscription Renewed (subscription-renewed)

  • Defined in service but no active call site found

22. New Booking Request (new-booking-request)

  • File: app/api/slots/request-for-approval/route.ts (line ~226)
  • Payload issues:
    • appointmentType: passes raw "CONSULTATION"
    • consulteeName: falls back to "A consultee"

23. Verification Status Changed (verification-status-changed)

  • Files: app/api/staff/moderation/profiles/[verificationId]/route.ts (line ~214), app/api/admin/verification/[verificationId]/route.ts (line ~219)
  • Payload issues:
    • status: passes raw enum ("VERIFIED", "REJECTED", "PENDING_VERIFICATION")

24. Payout Processed (payout-processed)

  • File: lib/payments/payouts/payout-service.ts (line ~805)
  • Payload: Clean -- amount, currency, payoutId

25. General Announcement (general-announcement)

  • File: app/api/announcements/route.ts (line ~96)
  • Payload: Clean -- admin-authored content

26. New Consultant Application (new-consultant-application)

  • Files: app/api/verification/submit/route.ts (line ~126), utils/onboarding-server.ts
  • Payload: Clean -- applicantName, applicantEmail

27. Waitlist Spot Available (waitlist-spot-available)

  • Defined in service, triggered from waitlist handlers
  • Payload: Clean

28. Dispute Created (dispute-created)

  • File: app/api/webhooks/utils.ts (line ~330)
  • Payload issues:
    • reason: passes raw Stripe/Razorpay dispute reason strings
    • status: passes raw mapped enum ("NEEDS_RESPONSE", "WARNING_NEEDS_RESPONSE", etc.)

29. Dispute Resolved (dispute-resolved)

  • File: app/api/webhooks/utils.ts (line ~383)
  • Payload issues:
    • status: passes raw enum ("WON", "LOST", "CHARGE_REFUNDED")

30. Recording Available (recording-available)

  • File: lib/stream/recording-handlers.ts (line ~354)
  • Payload issues:
    • appointmentType: passes lowercase ("consultation", "subscription", etc.)
    • consultantName: falls back to "Unknown Consultant"

31. Recording Failed (recording-failed)

  • File: lib/stream/recording-handlers.ts (line ~442)
  • Payload issues:
    • errorMessage: passes raw Stream.io error strings

32. Referral Bonus Earned / Referee Welcome Bonus / Referral Credits Applied

  • Defined in service, triggered from lib/referrals/service.ts
  • Payload issues:
    • appointmentType (in credits applied): likely raw enum

33. Collaborator Invited (collaborator-invited)

  • File: lib/collaborators/service.ts (line ~166)
  • Payload issues:
    • planType: passes raw "webinar" or "class"
    • role: passes raw DB enum (e.g., "CO_HOST", "GUEST_SPEAKER", "MODERATOR")

34. Collaborator Accepted (collaborator-accepted)

  • File: lib/collaborators/service.ts (lines ~232, ~285)
  • Payload issues:
    • planType: raw "webinar" or "class"
    • role: raw DB enum

35. Collaborator Removed (collaborator-removed)

  • File: lib/collaborators/service.ts (lines ~338, ~376)
  • Payload issues:
    • planType: raw "webinar" or "class"

36-38. Maintenance Scheduled/Started/Ended

  • Files: actions/maintenance/drain-sessions.ts (line ~171), actions/maintenance/post-recovery.ts (line ~117), actions/maintenance/freeze-appointments.ts
  • Payload issues:
    • phase: passes raw "OFFLINE", "OFF" strings
  • Note: These are system broadcasts; less critical for user-facing copy

Enum-to-Human-Readable Mapping Tables

AppointmentsType / appointmentType

Raw value Human-friendly
CONSULTATION Consultation
SUBSCRIPTION Mentorship
WEBINAR Webinar
CLASS Class
TRIAL Trial session

CancellationReason

Raw value Human-friendly
SCHEDULE_CONFLICT Scheduling conflict
FOUND_ALTERNATIVE Found an alternative
FINANCIAL_REASONS Financial reasons
PERSONAL_EMERGENCY Personal emergency
NO_LONGER_NEEDED No longer needed
CONSULTANT_UNAVAILABLE Expert unavailable
CONSULTANT_EMERGENCY Expert emergency
PAYMENT_FAILED Payment issue
EXPIRED Booking expired
CONSULTANT_ISSUE Issue with expert
TECHNICAL_ISSUE Technical issue

TrialSessionStatus

Raw value Human-friendly
PENDING Pending review
SCHEDULED Scheduled
COMPLETED Completed
CONVERTED Converted to subscription
CANCELLED Cancelled
REJECTED Declined

ConsultantVerificationStatus

Raw value Human-friendly
PENDING_VERIFICATION Pending verification
UNDER_REVIEW Under review
VERIFIED Verified
REJECTED Needs resubmission

Support Ticket Status

Raw value Human-friendly
OPEN Open
IN_PROGRESS In progress
RESOLVED Resolved
CLOSED Closed

Dispute Status

Raw value Human-friendly
NEEDS_RESPONSE Needs your response
UNDER_REVIEW Under review
WON Resolved in your favor
LOST Resolved against you
CHARGE_REFUNDED Charge refunded
WARNING_NEEDS_RESPONSE Warning: needs response
WARNING_UNDER_REVIEW Warning: under review
WARNING_CLOSED Warning resolved

Collaborator Roles

Raw value Human-friendly
CO_HOST Co-host
GUEST_SPEAKER Guest speaker
MODERATOR Moderator
PANELIST Panelist
ASSISTANT Assistant

cancelledBy

Raw value Human-friendly (for notification copy)
consultant "your expert" / the actual consultant name
consultee "the client" / the actual consultee name
system "the system due to maintenance"

Implementation Plan

Phase 1: Create a shared humanization utility (new file)

Create lib/novu/humanize.ts with:

// Maps raw enum values to human-friendly strings
export function humanizeAppointmentType(raw: string): string { ... }
export function humanizeCancellationReason(raw: string): string { ... }
export function humanizeTrialStatus(raw: string): string { ... }
export function humanizeVerificationStatus(raw: string): string { ... }
export function humanizeSupportTicketStatus(raw: string): string { ... }
export function humanizeDisputeStatus(raw: string): string { ... }
export function humanizeCollaboratorRole(raw: string): string { ... }
export function humanizeCancelledBy(raw: string, name?: string): string { ... }

// Formats ISO datetime to user-friendly string
export function humanizeDateTime(iso: string | undefined): string { ... }

// Ensures names are never raw role labels
export function humanizeName(name: string | null | undefined, role: 'consultant' | 'consultee'): string { ... }

Phase 2: Fix payload construction at each call site

For every notification trigger point listed above, update the payload to:

  1. Use humanizeAppointmentType() for all appointmentType fields
  2. Use humanizeCancellationReason() for all reason fields
  3. Use humanizeDateTime() for all date/time fields
  4. Use actual user names (never "Consultant", "Consultee", "User")
  5. Use humanize*() for all status/enum fields
  6. Fix the reschedule route to actually populate oldDateTime and newDateTime
  7. Fix the payment success route to not pass metadata.planId (UUID) as planTitle

Phase 3: Update Novu Dashboard templates

For templates that use text-transform: capitalize or similar CSS tricks, these can be removed once payloads are pre-humanized. Update in-app notification copy in the Novu Dashboard to be complete, well-formatted sentences.

Phase 4: Handle edge cases

  1. Missing data fallbacks: Instead of "Consultant" / "Consultee", use "your expert" / "the client" (role-appropriate language without exposing internal terminology)
  2. Multi-participant events (webinars/classes): Don't show consulteeName -- use "your upcoming webinar" style language
  3. Date formatting: Use Intl.DateTimeFormat with user's locale for all dates; provide a sensible fallback if timezone unknown
  4. Reason codes from payment gateways: Map Stripe/Razorpay raw failure codes to user-friendly messages

Specific Bug Fixes Required

Bug File Fix
Reschedule notification missing dates app/api/appointments/[appointmentId]/reschedule/route.ts Populate oldDateTime and newDateTime from the slot data before marking tentative
Payment success planTitle shows UUID lib/payments/webhooks/handlers.ts (~line 504) Fetch actual plan title instead of using metadata.planId as fallback
Cancel route only extracts names for consultation/subscription app/api/appointments/[appointmentId]/cancel/route.ts Add webinar/class consultant name extraction
Inconsistent appointmentType casing Multiple files Standardize: always pass through humanizeAppointmentType()
cancelledBy shows role not name Cancel route Pass the cancelling user's actual name, not the role string

Testing Plan

  • Trigger each of the 35+ notification types in a dev/staging environment
  • Verify in-app notifications in the Novu Inbox show human-friendly copy
  • Verify email notifications render correctly with humanized values
  • Test with missing data (null names, missing dates) -- confirm graceful fallbacks
  • Test all CancellationReason enum values render as human-readable strings
  • Test reschedule notifications show both old and new date/time
  • Test payment success does not show UUIDs in plan title
  • Test webinar/class notifications don't reference "consultee" by name (many participants)
  • Verify no regression in notification delivery (fire-and-forget patterns preserved)
  • Cross-browser test notification inbox rendering

Files to modify

New file:

  • lib/novu/humanize.ts

Core payload construction (all trigger points):

  • lib/payments/webhooks/handlers.ts
  • app/api/appointments/[appointmentId]/cancel/route.ts
  • app/api/appointments/[appointmentId]/reschedule/route.ts
  • app/api/events/subscriptions/route.ts
  • app/api/events/subscriptions/[subscriptionId]/route.ts
  • app/api/trials/route.ts
  • app/api/trials/[trialId]/route.ts
  • app/api/user/support-tickets/route.ts
  • app/api/user/reviews/route.ts
  • app/api/user/feedbacks/route.ts
  • app/api/slots/request-for-approval/route.ts
  • app/api/verification/submit/route.ts
  • app/api/staff/support-tickets/[ticketId]/route.ts
  • app/api/staff/support-tickets/[ticketId]/responses/route.ts
  • app/api/staff/moderation/profiles/[verificationId]/route.ts
  • app/api/admin/verification/[verificationId]/route.ts
  • app/api/announcements/route.ts
  • app/api/webhooks/utils.ts
  • lib/stream/recording-handlers.ts
  • lib/payments/payouts/payout-service.ts
  • lib/collaborators/service.ts
  • scripts/appointments/send-appointment-reminders.ts
  • scripts/appointments/auto-complete-appointments.ts
  • actions/maintenance/freeze-appointments.ts
  • actions/maintenance/drain-sessions.ts
  • actions/maintenance/post-recovery.ts
  • utils/onboarding-server.ts

Novu Dashboard templates (external):

  • All 16 Tier-1 workflow templates listed in docs/notifications/03-novu-template-specs.md

Documentation update:

  • docs/notifications/03-novu-template-specs.md (update template specs to reflect humanized payloads)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingfixBug fixes and targeted improvementslaunch: pre-mvpGates launch — money, data, or a failure we would not detectnotificationsNotification and email system

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions