Comprehensive event-driven notification system with 107 configurable trigger codes across all modules. Supports admin-level toggle, user preferences, email via Brevo, real-time via SignalR, and optional SMS.
The notification system is event-driven, sending both in-app notifications and emails based on configurable triggers. Users can customize their notification preferences.
A system event occurs (e.g., PR approved, PO created, vendor registration). The event is captured with entity details.
public async Task RaiseEventAsync(string eventCode, string entityType, long entityId, object data) { await _notificationService.ProcessEventAsync(new NotificationEvent { EventCode = eventCode, // e.g., "PR_APPROVED" EntityType = entityType, // e.g., "PR" EntityId = entityId, // e.g., 12345 EventData = data, // Additional context data OccurredAt = DateTime.UtcNow }); }
Look up which triggers are configured for this event and are enabled.
SELECT nt.*, t.template_code, t.subject, t.body_html, t.variables FROM config.notification_triggers nt JOIN config.notification_templates t ON nt.template_id = t.id WHERE nt.event_code = 'PR_APPROVED' AND nt.module_type = 'PR' AND nt.is_enabled = true AND t.is_active = true;
Determine who should receive the notification based on recipient_type.
private async Task<List<User>> ResolveRecipientsAsync(string recipientType, NotificationEvent evt) { return recipientType switch { "CREATOR" => await GetEntityCreator(evt.EntityType, evt.EntityId), "APPROVER" => await GetNextApprover(evt.EntityType, evt.EntityId), "ROLE" => await GetUsersByRole(evt.RoleId), "ALL_APPROVERS" => await GetAllPendingApprovers(evt.EntityType, evt.EntityId), "VENDOR" => await GetVendorContacts(evt.VendorId), _ => new List<User>() }; }
Verify if each recipient has opted to receive this type of notification.
SELECT preference_value FROM config.user_preferences WHERE user_id = @userId AND preference_key = 'email_pr_approved'; -- Default to 'true' if no preference is set -- Only skip if explicitly set to 'false'
Replace template variables with actual values from the event data.
private string RenderTemplate(string template, Dictionary<string, string> variables) { foreach (var kvp in variables) { template = template.Replace("{{" + kvp.Key + "}}", kvp.Value); } return template; } // Variables example: // {{pr_number}} -> "PR-2026-00123" // {{approver_name}} -> "John Smith" // {{amount}} -> "$15,000.00"
Insert a notification record for the user's notification bell.
INSERT INTO config.notifications (user_id, title, message, notification_type, entity_type, entity_id, is_read, created_at) VALUES (@userId, 'PR Approved', 'Your PR #PR-2026-00123 has been approved', 'APPROVAL', 'PR', 12345, false, NOW());
Send the email via SMTP/SendGrid and log the result.
// Send email var result = await _emailService.SendAsync(recipient.Email, subject, bodyHtml); // Log result INSERT INTO config.email_logs (template_id, to_email, subject, status, error_message, sent_at) VALUES (@templateId, @email, @subject, result.Success ? 'SENT' : 'FAILED', result.ErrorMessage, NOW());
Comprehensive list of all 107 notification trigger codes across the ReqVise Unified Platform. Admin can enable/disable any trigger. Users can customize preferences except for critical notifications.
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
AUTH_LOGIN_SUCCESS | User logged in | User | In-App | Disabled |
AUTH_LOGIN_FAILED | Failed login attempt | User, Admin | Enabled | |
AUTH_ACCOUNT_LOCKED | Account locked after failed attempts | User, Admin | Enabled | |
AUTH_PASSWORD_RESET_REQUESTED | Password reset initiated | User | Enabled | |
AUTH_PASSWORD_CHANGED | Password successfully changed | User | Enabled | |
AUTH_SESSION_EXPIRED | Session timeout | User | In-App | Disabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
CRM_LEAD_RECEIVED | New lead synced from CRM | Sales Manager | Email In-App | Enabled |
CRM_LEAD_QUALIFIED | Lead marked as qualified | Sales Manager | In-App | Enabled |
CRM_LEAD_NURTURE | Lead moved to nurture | Sales Manager | In-App | Disabled |
CRM_QUOTE_CREATED | New quote created | Customer | Enabled | |
CRM_QUOTE_REVISED | Quote revised | Customer | Enabled | |
CRM_DEAL_WON | Deal marked as won | Sales Mgr, Project Mgr | Email In-App | Enabled |
CRM_DEAL_LOST | Deal marked as lost | Sales Manager | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
PRJ_CREATED | Project created | Project Manager, Team | Email In-App | Enabled |
PRJ_SCHEDULE_UPDATED | Schedule modified | Team Members | In-App | Enabled |
PRJ_MILESTONE_APPROACHING | Milestone due in 7 days | Project Manager | Enabled | |
PRJ_MILESTONE_OVERDUE | Milestone past due | Project Mgr, Escalation | Enabled | |
PRJ_PROGRESS_UPDATED | Activity progress changed | Project Manager | In-App | Disabled |
PRJ_BUDGET_WARNING | Budget at 80% | Project Mgr, Finance | Enabled | |
PRJ_BUDGET_EXCEEDED | Budget exceeded 100% | Project Mgr, Director | Enabled | |
PRJ_TEAM_ASSIGNED | User added to project | User | Email In-App | Enabled |
PRJ_CLOSED | Project closed | Team, Stakeholders | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
ENG_TDS_ALLOCATED | TDS allocated to vendor | Vendor | Enabled | |
ENG_TDS_DUE_REMINDER | TDS due in 3 days | Vendor | Enabled | |
ENG_TDS_OVERDUE | TDS past due date | Vendor, Eng Lead | Enabled | |
ENG_TDS_SUBMITTED | Vendor submitted TDS | Reviewer | Email In-App | Enabled |
ENG_TDS_APPROVED | TDS approved | Vendor, Creator | Enabled | |
ENG_TDS_REJECTED | TDS rejected | Vendor | Enabled | |
ENG_TDS_REVISION_ALLOWED | Revision allowed by reviewer | Vendor | Enabled | |
ENG_DRAWING_ALLOCATED | Drawing allocated to vendor | Vendor | Enabled | |
ENG_DRAWING_DUE_REMINDER | Drawing due in 3 days | Vendor | Enabled | |
ENG_DRAWING_OVERDUE | Drawing past due date | Vendor, Eng Lead | Enabled | |
ENG_DRAWING_SUBMITTED | Vendor submitted drawing | Reviewer | Email In-App | Enabled |
ENG_DRAWING_APPROVED | Drawing approved | Vendor, Creator | Enabled | |
ENG_DRAWING_REJECTED | Drawing rejected | Vendor | Enabled | |
ENG_TRANSMITTAL_SENT | Transmittal sent to recipient | Recipient | Enabled | |
ENG_TRANSMITTAL_ACKNOWLEDGED | Transmittal acknowledged | Sender | In-App | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
PR_CREATED | PR created | Creator | In-App | Disabled |
PR_SUBMITTED | PR submitted for approval | Next Approver | Email In-App | Enabled |
PR_PENDING_APPROVAL | PR awaiting your approval | Approver | Enabled | |
PR_APPROVED_LEVEL | PR approved at current level | Creator, Next Approver | In-App | Enabled |
PR_FULLY_APPROVED | PR fully approved | Creator | Email In-App | Enabled |
PR_REJECTED | PR rejected | Creator | Email In-App | Enabled |
PR_REVISION_REQUESTED | PR needs revision | Creator | Enabled | |
PR_SHORT_CLOSED | PR short closed | Creator | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
STK_CREATED | Stock created from PR | Stock Manager | In-App | Enabled |
STK_LOW_INVENTORY | Stock below threshold | Procurement | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
RFQ_CREATED | RFQ created | Creator | In-App | Disabled |
RFQ_SUBMITTED | RFQ submitted for approval | Approver | Email In-App | Enabled |
RFQ_APPROVED | RFQ approved | Creator | Email In-App | Enabled |
RFQ_PUBLISHED | RFQ published to vendors | Invited Vendors | Enabled | |
RFQ_BID_RECEIVED | Vendor submitted bid | RFQ Creator | Email In-App | Enabled |
RFQ_BID_DEADLINE_REMINDER | Bid deadline in 24 hours | Vendors (no bid yet) | Enabled | |
RFQ_BID_DEADLINE_PASSED | Bid deadline reached | RFQ Creator | In-App | Enabled |
RFQ_EVALUATION_COMPLETE | Bid evaluation completed | Approvers | In-App | Enabled |
RFQ_SHORTLISTED | Vendor shortlisted | Vendor | Enabled | |
RFQ_NOT_SHORTLISTED | Vendor not shortlisted | Vendor | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
RA_CREATED | Auction created | Creator | In-App | Disabled |
RA_INVITATION_SENT | Auction invitation | Shortlisted Vendors | Enabled | |
RA_STARTING_SOON | Auction starts in 1 hour | Participants | Enabled | |
RA_STARTED | Auction started | Participants | Email In-App | Enabled |
RA_NEW_BID | New bid received | Procurement Team | In-App | Enabled |
RA_OUTBID | Your bid is no longer lowest | Vendor | Real-time | Enabled |
RA_EXTENDED | Auction time extended | Participants | Real-time | Enabled |
RA_ENDING_SOON | Auction ends in 5 minutes | Participants | Real-time | Enabled |
RA_ENDED | Auction ended | Participants | Enabled | |
RA_WON | You won the auction | Winning Vendor | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
PO_CREATED | PO created | Creator | In-App | Disabled |
PO_SUBMITTED | PO submitted for approval | Approver | Email In-App | Enabled |
PO_PENDING_APPROVAL | PO awaiting your approval | Approver | Enabled | |
PO_APPROVED | PO fully approved | Creator | Email In-App | Enabled |
PO_REJECTED | PO rejected | Creator | Email In-App | Enabled |
PO_RELEASED | PO released to vendor | Vendor | Enabled | |
PO_ACKNOWLEDGED | PO acknowledged by vendor | Creator | In-App | Enabled |
PO_NOT_ACKNOWLEDGED | PO not acknowledged in 48h | Creator, Vendor | Enabled | |
PO_AMENDMENT_CREATED | PO amendment initiated | Vendor, Approvers | Enabled | |
PO_AMENDMENT_APPROVED | PO amendment approved | Vendor, Creator | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
VND_REGISTRATION_RECEIVED | New vendor registration | Procurement Admin | Enabled | |
VND_REGISTRATION_APPROVED | Vendor approved | Vendor | Enabled | |
VND_REGISTRATION_REJECTED | Vendor rejected | Vendor | Enabled | |
VND_COMPLIANCE_DUE | Compliance document expiring | Vendor | Enabled | |
VND_COMPLIANCE_EXPIRED | Compliance document expired | Vendor, Admin | Enabled | |
VND_COMPLIANCE_SUBMITTED | Compliance uploaded | Admin | In-App | Enabled |
VND_COMPLIANCE_APPROVED | Compliance approved | Vendor | Enabled | |
VND_COMPLIANCE_REJECTED | Compliance rejected | Vendor | Enabled | |
VND_PROFILE_INCOMPLETE | Profile incomplete warning | Vendor | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
VDW_SUBMISSION_REQUIRED | Drawings required for PO | Vendor | Enabled | |
VDW_SUBMITTED | Vendor submitted drawings | Engineering Reviewer | Email In-App | Enabled |
VDW_APPROVED | Drawings approved | Vendor | Enabled | |
VDW_REVISE_RESUBMIT | Drawings need revision | Vendor | Enabled | |
VDW_REJECTED | Drawings rejected | Vendor | Enabled | |
VDW_REVIEW_OVERDUE | Review overdue (7 days) | Reviewer, Manager | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
ASN_CREATED | ASN created by vendor | Receiving Team | Enabled | |
ASN_SHIPMENT_DISPATCHED | Shipment dispatched | Receiving Team | Enabled | |
ASN_DELIVERY_DATE | Expected delivery today | Receiving Team | Enabled | |
GRN_CREATED | GRN created | Vendor, Procurement | In-App | Enabled |
GRN_QC_PASSED | QC passed | Vendor | In-App | Enabled |
GRN_QC_FAILED | QC failed | Vendor, Procurement | Enabled | |
GRN_PARTIAL_RECEIPT | Partial goods received | Vendor, Procurement | Enabled | |
INV_SUBMITTED | Invoice submitted by vendor | Finance | Email In-App | Enabled |
INV_THREE_WAY_MATCH_OK | 3-way match successful | Finance | In-App | Enabled |
INV_THREE_WAY_MISMATCH | 3-way match discrepancy | Finance, Procurement | Enabled | |
INV_APPROVED | Invoice approved | Vendor | Enabled | |
INV_REJECTED | Invoice rejected | Vendor | Enabled | |
INV_PAYMENT_DUE | Invoice due in 3 days | Finance | Enabled | |
INV_PAYMENT_OVERDUE | Invoice overdue | Finance, Vendor | Enabled | |
PAY_PROCESSED | Payment processed | Vendor | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
RPT_SCHEDULED_READY | Scheduled report generated | Subscribers | Enabled | |
RPT_EXPORT_COMPLETE | Large export complete | Requester | In-App | Enabled |
KPI_THRESHOLD_BREACH | KPI threshold exceeded | Dashboard Owner | Enabled | |
CRM_SYNC_COMPLETE | CRM sync completed | Admin | In-App | Disabled |
CRM_SYNC_FAILED | CRM sync failed | Admin | Enabled |
| Event Code | Description | Recipients | Channel | Default |
|---|---|---|---|---|
SYS_MAINTENANCE_SCHEDULED | Maintenance scheduled | All Users | Enabled | |
SYS_MAINTENANCE_STARTING | Maintenance starting in 1h | All Users | In-App | Enabled |
SYS_MAINTENANCE_COMPLETE | Maintenance complete | All Users | In-App | Enabled |
SYS_NEW_VERSION | New version deployed | All Users | In-App | Disabled |
SYS_BACKUP_FAILED | Backup job failed | Admin | Enabled |
AUTH_ACCOUNT_LOCKED, AUTH_PASSWORD_RESET_REQUESTED, AUTH_PASSWORD_CHANGED, VND_COMPLIANCE_EXPIRED, PO_RELEASED, INV_REJECTED, SYS_BACKUP_FAILED
Example email template for PR Approval notification:
Dear {{requester_name}},
Your Purchase Request {{pr_number}} has been approved.
Details:
You can view the PR details by clicking here.
Best regards,
ProKure System
-- Template stored in database INSERT INTO config.notification_templates (template_code, template_name, subject, body_html, body_text, variables, is_active) VALUES ( 'PR_APPROVED', 'PR Approval Notification', 'Purchase Request {{pr_number}} has been Approved', '<html>...template HTML...</html>', 'Plain text version...', '["pr_number", "requester_name", "total_amount", "approver_name", "approved_date"]'::jsonb, true );
Users can customize their notification preferences through the settings page:
| Preference Key | Description | Default | Type |
|---|---|---|---|
email_pr_submitted |
Email when PR needs your approval | true | |
email_pr_approved |
Email when your PR is approved | true | |
inapp_pr_approved |
In-app notification when PR approved | true | In-App |
email_po_created |
Email when new PO created (vendors) | true | |
email_bid_deadline |
Reminder before RFQ deadline | true | |
email_digest |
Daily digest instead of individual emails | false |
If a user has not set a preference, the system defaults to sending the notification. Only explicit opt-outs (preference_value = 'false') will suppress notifications.
For real-time delivery, the system uses SignalR (WebSocket) to push notifications to the Angular frontend:
// .NET Hub public class NotificationHub : Hub { public async Task SendNotification(long userId, NotificationDto notification) { await Clients.User(userId.ToString()) .SendAsync("ReceiveNotification", notification); } } // Angular Service this.hubConnection.on('ReceiveNotification', (notification) => { this.notifications.unshift(notification); this.unreadCount++; this.playNotificationSound(); });