Files
pfm/PostFundManagement.PartnerUI/src/services/notificationRulesService.ts
T
2026-10-23 00:58:47 +02:00

282 lines
9.1 KiB
TypeScript

import {
NotificationPlatform,
NotificationStatus,
ApprovalStatus,
ActivityStatus,
ContractStatus,
ComplianceStatus,
User,
Invite,
CreateNotificationRequest,
} from '../types';
import { apiClient } from './httpClient';
import {
getCurrentUserSub,
getCurrentUserClaims,
getCurrentUserTenantId,
} from '../auth/AuthProvider';
export const CONSOLE_PORTAL_URL = 'https://console.ifri.khongisa.co.za/';
export const PARTNER_PORTAL_URL = 'https://partner.ifri.khongisa.co.za/';
export async function dispatchNotification(payload: CreateNotificationRequest): Promise<void> {
const tenantId = getCurrentUserTenantId();
const orgId =
payload.organisationId !== undefined && payload.organisationId !== '' && payload.organisationId !== null
? Number(payload.organisationId)
: (tenantId !== null ? tenantId : 1);
await apiClient.post('/v1/api/communications/notifications', {
organisationId: orgId,
recipient: payload.recipient,
platform: payload.platform,
subject: payload.subject || null,
destination: payload.destination || 'In-App User Inbox',
message: payload.message,
status: payload.status !== undefined ? payload.status : undefined,
});
}
export async function fetchDirectoryUsers(page = 1, pageSize = 200): Promise<User[]> {
try {
const res = await apiClient.post<User[]>(`/v1/api/users/${page}/${pageSize}`, {});
return res.data || [];
} catch (err) {
console.warn('[NotificationRules] Failed to fetch users directory:', err);
return [];
}
}
export function isApprovedOrActiveStatus(status: number | string | undefined | null): boolean {
if (status === undefined || status === null) return false;
const numStatus = Number(status);
if (
numStatus === ApprovalStatus.Approved ||
numStatus === ActivityStatus.Active ||
numStatus === ContractStatus.Active ||
numStatus === ContractStatus.Executed ||
numStatus === ComplianceStatus.Met ||
numStatus === ComplianceStatus.Exceeded
) {
return true;
}
const strStatus = String(status).toLowerCase().trim();
return (
strStatus === 'approved' ||
strStatus === 'active' ||
strStatus === 'executed' ||
strStatus === 'met' ||
strStatus === 'exceeded' ||
strStatus === 'verified'
);
}
export function getApprovedOrActiveStatusLabel(status: number | string | undefined | null): string {
if (status === undefined || status === null) return 'Active';
const numStatus = Number(status);
if (numStatus === ApprovalStatus.Approved) return 'Approved';
if (numStatus === ActivityStatus.Active) return 'Active';
if (numStatus === ContractStatus.Active) return 'Active';
if (numStatus === ComplianceStatus.Met) return 'Compliant (Met)';
const str = String(status);
return str.charAt(0).toUpperCase() + str.slice(1);
}
export async function getTenantManagersAndExecutives(): Promise<User[]> {
try {
const tenantId = getCurrentUserTenantId();
const users = await fetchDirectoryUsers(1, 200);
const managersAndExecutives = users.filter((u) => {
const roleStr = String(u.role || '').toLowerCase();
const isManagerOrExec =
roleStr.includes('manager') ||
roleStr.includes('executive') ||
roleStr.includes('director') ||
roleStr === '2' ||
roleStr === '4';
const matchesTenant =
!tenantId || !u.tenant_id || Number(u.tenant_id) === Number(tenantId);
return isManagerOrExec && matchesTenant;
});
if (managersAndExecutives.length > 0) {
return managersAndExecutives;
}
const elevatedUsers = users.filter((u) => {
const roleStr = String(u.role || '').toLowerCase();
return (
roleStr.includes('admin') ||
roleStr.includes('supervisor') ||
roleStr === '1' ||
roleStr === '3'
);
});
if (elevatedUsers.length > 0) {
return elevatedUsers;
}
} catch (err) {
console.warn('[NotificationRules] Directory lookup failed, using authenticated identity fallback:', err);
}
const currentSub = getCurrentUserSub();
const currentClaims = getCurrentUserClaims();
if (currentSub) {
return [
{
id: currentSub,
email: (currentClaims.email as string) || (currentClaims.preferred_username as string) || 'manager@pfm.org.za',
status: ActivityStatus.Active,
lastLoginAt: null,
role: 'Manager',
name: (currentClaims.name as string) || 'Tenant Manager',
},
];
}
return [];
}
export interface RecordStatusChangeNotificationParams {
recordType: string;
recordId: string | number;
newStatus: number | string;
title?: string;
previousStatus?: number | string;
}
export async function notifyTenantManagersAndExecutivesOnRecordStatusChange(
params: RecordStatusChangeNotificationParams
): Promise<void> {
const { recordType, recordId, newStatus, title } = params;
if (recordType.toLowerCase() === 'user' || recordType.toLowerCase() === 'users') {
return;
}
if (!isApprovedOrActiveStatus(newStatus)) {
return;
}
const statusLabel = getApprovedOrActiveStatusLabel(newStatus);
const orgId = getCurrentUserTenantId() || 1;
const managersAndExecutives = await getTenantManagersAndExecutives();
if (managersAndExecutives.length === 0) {
return;
}
const recordDescriptor = title
? `${recordType} "${title}" (#${recordId})`
: `${recordType} #${recordId}`;
const inAppSubject = `[${statusLabel}] ${recordType} #${recordId} is now ${statusLabel}`;
const inAppMessage = `### Statutory Record Update\n\nThe record **${recordDescriptor}** has officially transitioned to **${statusLabel}** status.\n\n- **Record Category:** ${recordType}\n- **Identifier:** #${recordId}\n- **Updated Status:** ${statusLabel}\n- **Timestamp:** ${new Date().toLocaleString()}`;
const emailSubject = `[PFM Governance Alert] ${recordType} #${recordId} Status Transition: ${statusLabel}`;
const emailMessage = `### Formal Statutory Notification\n\nThis is an automated notification to Tenant Managers and Executives regarding an approved record status update.\n\n**Record Details:**\n- **Category:** ${recordType}\n- **Reference Identifier:** #${recordId}\n${title ? `- **Title:** ${title}\n` : ''}- **New Lifecycle Status:** **${statusLabel}**\n- **Effective Timestamp:** ${new Date().toUTCString()}`;
for (const recipient of managersAndExecutives) {
if (!recipient.id) continue;
const recipientEmail = recipient.email || 'manager@pfm.org.za';
try {
await dispatchNotification({
organisationId: orgId,
recipient: recipient.id,
platform: NotificationPlatform.InApp,
subject: inAppSubject,
destination: recipientEmail,
message: inAppMessage,
status: NotificationStatus.Delivered,
});
} catch {}
try {
await dispatchNotification({
organisationId: orgId,
recipient: recipient.id,
platform: NotificationPlatform.Email,
subject: emailSubject,
destination: `To: ${recipientEmail} | From: PFM Automated Governance System`,
message: emailMessage,
status: NotificationStatus.Queued,
});
} catch {}
}
}
export interface InviteApprovedNotificationParams {
invite: Partial<Invite> & {
id: number | string;
recipient: string;
invitedOrganisationId?: number | string;
organisationId?: number | string;
awardId?: number | string;
contractId?: number | string;
message?: string;
};
partnerUser?: User | null;
partnerEmail?: string;
}
export async function notifyInviteApproved(
params: InviteApprovedNotificationParams
): Promise<void> {
const { invite, partnerUser, partnerEmail } = params;
if (!invite.recipient) return;
const orgId = invite.invitedOrganisationId || invite.organisationId || getCurrentUserTenantId() || 1;
const targetEmail = partnerEmail || partnerUser?.email || invite.recipient;
try {
await dispatchNotification({
organisationId: orgId,
recipient: invite.recipient,
platform: NotificationPlatform.InApp,
subject: 'Onboarding Invitation Approved: Access to Partner Portal Granted',
destination: targetEmail,
message: `Your statutory onboarding invitation has been formally Approved. Access the Partner Portal at ${PARTNER_PORTAL_URL}.`,
status: NotificationStatus.Delivered,
});
} catch {}
}
export interface NewUserCreatedNotificationParams {
userId: string;
email: string;
isPartner: boolean;
roleName?: string;
tenantId?: number | string | null;
}
export async function notifyNewUserCreated(
params: NewUserCreatedNotificationParams
): Promise<void> {
const { userId, email, isPartner, tenantId } = params;
if (!userId || !email) return;
const orgId = tenantId ? Number(tenantId) : (getCurrentUserTenantId() || 1);
const targetUrl = isPartner ? PARTNER_PORTAL_URL : CONSOLE_PORTAL_URL;
try {
await dispatchNotification({
organisationId: orgId,
recipient: userId,
platform: NotificationPlatform.InApp,
subject: 'Welcome to PFM - Your Account Has Been Created',
destination: email,
message: `Your user account has been created. Please sign in at ${targetUrl}.`,
status: NotificationStatus.Delivered,
});
} catch {}
}