i male base url issue resolved

This commit is contained in:
Laxman 2026-05-22 20:40:24 +05:30
parent 061dc8e260
commit ac31b9ba57
26 changed files with 101 additions and 67 deletions

View File

@ -11,6 +11,9 @@ import {
getSmtpConfig,
isSmtpEnabled
} from '../../services/smtpConfig.service.js';
import { getFrontendBaseUrl } from './frontendUrl.js';
export { getFrontendBaseUrl } from './frontendUrl.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -208,7 +211,7 @@ export const sendOpportunityEmail = async (
location: string,
applicationId: string
) => {
const link = `http://localhost:5173/questionnaire/${applicationId}`;
const link = `${getFrontendBaseUrl()}/questionnaire/${applicationId}`;
await sendEmail(to, 'Action Required: Royal Enfield Dealership Opportunity', 'OPPORTUNITY', {
applicantName,
location,
@ -286,7 +289,7 @@ export const sendShortlistedEmail = async (
location: string,
applicationId: string
) => {
const portalLink = 'http://localhost:5173/login';
const portalLink = `${getFrontendBaseUrl()}/login`;
await sendEmail(to, `Congratulations! You are Shortlisted: ${applicationId}`, 'APPLICANT_SHORTLISTED', {
applicantName,
location,

View File

@ -0,0 +1,7 @@
/**
* Portal base URL for emails, WhatsApp, SLA alerts, and notifications.
* Set `FRONTEND_URL` in backend `.env` (e.g. https://dealeronboarding-uat.royalenfield.com).
*/
export function getFrontendBaseUrl(): string {
return (process.env.FRONTEND_URL || 'http://localhost:5173').replace(/\/$/, '');
}

View File

@ -1,6 +1,7 @@
import { Server as SocketServer } from 'socket.io';
import { Server as HTTPServer } from 'http';
import logger from './logger.js';
import { getFrontendBaseUrl } from './frontendUrl.js';
let io: SocketServer | null = null;
@ -11,7 +12,7 @@ let io: SocketServer | null = null;
export const initSocket = (httpServer: HTTPServer) => {
io = new SocketServer(httpServer, {
cors: {
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
origin: getFrontendBaseUrl(),
methods: ['GET', 'POST', 'PUT'],
credentials: true
}

View File

@ -3,6 +3,7 @@ import { Op } from 'sequelize';
import { sendEmail } from './email.service.js';
import { NotificationService } from '../../services/NotificationService.js';
import {
import { getFrontendBaseUrl } from './frontendUrl.js';
APPLICATION_STAGES,
TERMINATION_STAGES,
CONSTITUTIONAL_STAGES,
@ -13,8 +14,6 @@ import {
const { RequestParticipant, User, Outlet, District, Dealer } = db;
const frontendBase = () => process.env.FRONTEND_URL || 'http://localhost:5173';
/** Dealer acknowledgement + internal reviewers after resignation is created. */
export async function notifyResignationSubmittedEmails(resignation: any): Promise<void> {
const dealerUser = await User.findByPk(resignation.dealerId, {
@ -22,7 +21,7 @@ export async function notifyResignationSubmittedEmails(resignation: any): Promis
});
if (!dealerUser?.email) return;
const base = frontendBase();
const base = getFrontendBaseUrl();
const resignationCode = resignation.resignationId || resignation.id;
const lwd =
resignation.lastOperationalDateSales ||
@ -99,7 +98,7 @@ export async function notifyConstitutionalSubmittedEmails(request: any, dealerDi
include: [{ model: User, as: 'user', attributes: ['id', 'email', 'fullName', 'mobileNumber'] }]
});
const base = frontendBase();
const base = getFrontendBaseUrl();
const link = `${base}/constitutional-change/${request.id}`;
for (const p of participants) {
@ -128,7 +127,7 @@ export async function notifyRelocationSubmittedEmails(
request: any,
submitter: { email: string; fullName?: string | null }
): Promise<void> {
const base = frontendBase();
const base = getFrontendBaseUrl();
const code = request.requestId || request.id;
const dealerName = submitter.fullName?.trim() || 'Dealer';

View File

@ -339,7 +339,7 @@ const processStageDecision = async (params: {
// we still need to trigger notifications for the NEXT person in the sequence.
try {
const { notifyStakeholdersOnTransition } = await import('../../common/utils/workflow-email-notifications.js');
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
await notifyStakeholdersOnTransition(
application.id,
@ -583,7 +583,7 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
type,
scheduledAt: formatIST(scheduledDateObj),
meetLink,
appLink: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
appLink: `${getFrontendBaseUrl()}/applications/${application.id}`,
phone: applicantPhone,
ctaLabel: 'View application'
}
@ -710,7 +710,7 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
type,
scheduledAt: formatIST(scheduledDateObj),
meetLink,
appLink: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
appLink: `${getFrontendBaseUrl()}/applications/${application.id}`,
phone: pPhone || '',
ctaLabel: 'View application'
}
@ -813,7 +813,7 @@ export const updateInterview = async (req: AuthRequest, res: Response) => {
type,
scheduledAt: formatIST(interview.scheduleDate),
meetLink,
appLink: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
appLink: `${getFrontendBaseUrl()}/applications/${application.id}`,
phone: application.mobileNumber || '',
ctaLabel: 'View Schedule'
}
@ -840,7 +840,7 @@ export const updateInterview = async (req: AuthRequest, res: Response) => {
type,
scheduledAt: formatIST(interview.scheduleDate),
meetLink,
appLink: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
appLink: `${getFrontendBaseUrl()}/applications/${application.id}`,
phone: panelist.mobileNumber || '',
ctaLabel: 'Open Assessment'
}
@ -1043,6 +1043,7 @@ export const submitLevel2Feedback = async (req: AuthRequest, res: Response) => {
// --- AI Summary ---
import { ExternalMocksService } from '../../common/utils/externalMocks.service.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
export const generateAiSummary = async (req: AuthRequest, res: Response) => {
try {

View File

@ -16,6 +16,7 @@ import { getIO } from '../../common/utils/socket.js';
import * as InAppPushNotificationService from '../../common/utils/notification.service.js';
import { NotificationService as EmailNotificationService } from '../../services/NotificationService.js';
import logger from '../../common/utils/logger.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
// --- Helpers ---
const getDocumentModel = (requestType: string) => {
@ -190,7 +191,7 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
});
}
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
let worknoteLink = `${portalBase}/dashboard`;
let applicationIdLabel = String(resolvedId).slice(0, 12);
let dealerNameLabel = 'Record';

View File

@ -5,6 +5,7 @@ const { EorChecklist, EorChecklistItem, OnboardingDocument, RelocationDocument }
import { AuthRequest } from '../../types/express.types.js';
import { NotificationService } from '../../services/NotificationService.js';
import { ROLES } from '../../common/config/constants.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
/** Default EOR rows for relocation (SRS 12.2.8) — must stay aligned with relocation required-doc labels. */
export const RELOCATION_EOR_DEFAULT_ITEMS = [
@ -355,7 +356,7 @@ export const submitAudit = async (req: AuthRequest, res: Response) => {
placeholders: {
applicantName: app?.applicantName || '',
applicationId: app?.applicationId || '',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${checklist.applicationId}`,
link: `${getFrontendBaseUrl()}/applications/${checklist.applicationId}`,
ctaLabel: 'View Application'
}
}).catch((e: any) => console.error('[EOR] Completion notify failed:', e));
@ -379,7 +380,7 @@ export const submitAudit = async (req: AuthRequest, res: Response) => {
placeholders: {
applicantName: '',
applicationId: checklist.relocationId,
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/relocation-requests/${checklist.relocationId}`,
link: `${getFrontendBaseUrl()}/relocation-requests/${checklist.relocationId}`,
ctaLabel: 'View Request'
}
}).catch((e: any) => console.error('[EOR] Relocation notify failed:', e));

View File

@ -8,6 +8,7 @@ import { WorkflowService } from '../../services/WorkflowService.js';
import { pickApplicationAuditContext, safeAuditLogCreate } from '../../services/applicationAuditLog.service.js';
import { NotificationService } from '../../services/NotificationService.js';
import { formatDueDateDaysFromNow, DEFAULT_FDD_DOCUMENT_CHECKLIST } from '../../constants/onboarding-email-defaults.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
export const getAssignment = async (req: Request, res: Response) => {
try {
@ -105,14 +106,14 @@ export const assignAgency = async (req: AuthRequest, res: Response) => {
applicationId: application.applicationId,
dealerName: application.applicantName || '',
participantType: 'FDD Partner',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
link: `${getFrontendBaseUrl()}/applications/${application.id}`,
ctaLabel: 'View Assignment',
phone: phone || ''
}
}).catch((e: any) => console.error('[FDD] Agency notify failed:', e));
}
const portalBaseFdd = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBaseFdd = getFrontendBaseUrl();
if (application.email) {
const applicantAcct = await db.User.findOne({
where: { email: application.email },
@ -221,7 +222,7 @@ export const uploadReport = async (req: AuthRequest, res: Response) => {
dealerName: application.applicantName || '',
requestId: application.applicationId,
targetStage: 'FDD Review',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
link: `${getFrontendBaseUrl()}/applications/${application.id}`,
phone: ''
}
}).catch((e: any) => console.error('[FDD] Finance/Admin notify failed:', e));

View File

@ -6,6 +6,7 @@ import { AuthRequest } from '../../types/express.types.js';
import { AUDIT_ACTIONS, APPLICATION_STATUS, APPLICATION_STAGES, ROLES } from '../../common/config/constants.js';
import { WorkflowService } from '../../services/WorkflowService.js';
import { NotificationService } from '../../services/NotificationService.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
const LOA_STAGE_CODE = 'LOA_APPROVAL';
@ -101,7 +102,7 @@ export const createRequest = async (req: AuthRequest, res: Response) => {
dealerName: application.applicantName || application.applicationId,
requestId: application.applicationId,
targetStage: 'LOA Approval',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
link: `${getFrontendBaseUrl()}/applications/${application.id}`,
phone: phone || ''
}
}).catch((e: any) => console.error('[LOA] DD-Head notify failed:', e));
@ -243,7 +244,7 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
placeholders: {
applicantName: app?.applicantName || '',
applicationId: app?.applicationId || '',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${request.applicationId}`,
link: `${getFrontendBaseUrl()}/applications/${request.applicationId}`,
ctaLabel: 'View Application'
}
}).catch((e: any) => console.error('[LOA] team notify failed:', e));
@ -254,7 +255,7 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
include: [{ model: DealerCode, as: 'dealerCode', required: false }]
});
if (appFull?.email) {
const portalBaseLoa = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBaseLoa = getFrontendBaseUrl();
const dc = (appFull as any).dealerCode;
const dealerCodeStr = dc?.salesCode || dc?.serviceCode || 'Available on portal after SAP sync';
const applicantAcct = await User.findOne({
@ -324,7 +325,7 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
dealerName: '',
requestId,
targetStage: 'NBH LOA Approval',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${request.applicationId}`,
link: `${getFrontendBaseUrl()}/applications/${request.applicationId}`,
phone: phone || ''
}
}).catch((e: any) => console.error('[LOA] NBH notify failed:', e));

View File

@ -7,6 +7,7 @@ import { WorkflowService } from '../../services/WorkflowService.js';
import { NotificationService } from '../../services/NotificationService.js';
import { sendEmail } from '../../common/utils/email.service.js';
import { formatDueDateDaysFromNow, DEFAULT_STATUTORY_DOCUMENT_CHECKLIST } from '../../constants/onboarding-email-defaults.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
const LOI_STAGE_CODE = 'LOI_APPROVAL';
@ -96,7 +97,7 @@ export const acknowledgeRequest = async (req: AuthRequest, res: Response) => {
progressPercentage: 90
});
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const deposit =
(await SecurityDeposit.findOne({
where: { applicationId: application.id, depositType: 'SECURITY_DEPOSIT' }
@ -182,7 +183,7 @@ export const createRequest = async (req: AuthRequest, res: Response) => {
dealerName: application.applicantName || application.applicationId,
requestId: application.applicationId,
targetStage: 'LOI Approval',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
link: `${getFrontendBaseUrl()}/applications/${application.id}`,
phone: phone || ''
}
}).catch((e: any) => console.error('[LOI] DD-Head notify failed:', e));
@ -336,7 +337,7 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
placeholders: {
dealerName: application2?.applicantName || '',
requestId: application2?.applicationId || '',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${request.applicationId}`,
link: `${getFrontendBaseUrl()}/applications/${request.applicationId}`,
ctaLabel: 'View Application'
}
}).catch((e: any) => console.error('[LOI] finance/admin notify failed:', e));
@ -358,7 +359,7 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
dealerName: '',
requestId: String(request.applicationId),
targetStage: 'NBH LOI Approval',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${request.applicationId}`,
link: `${getFrontendBaseUrl()}/applications/${request.applicationId}`,
phone: phone || ''
}
}).catch((e: any) => console.error('[LOI] NBH notify failed:', e));
@ -474,7 +475,7 @@ export const generateDocument = async (req: AuthRequest, res: Response) => {
{
applicantName: application.applicantName || 'Applicant',
applicationId: application.applicationId,
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/prospect-login`,
link: `${getFrontendBaseUrl()}/prospect-login`,
ctaLabel: 'View Your Application'
}
).catch((e: any) => console.error('[LOI] Applicant email failed:', e));
@ -493,7 +494,7 @@ export const generateDocument = async (req: AuthRequest, res: Response) => {
placeholders: {
applicantName: application.applicantName || '',
applicationId: application.applicationId,
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
link: `${getFrontendBaseUrl()}/applications/${application.id}`,
ctaLabel: 'View Application'
}
}).catch((e: any) => console.error('[LOI] stakeholder notify failed:', e));
@ -501,7 +502,7 @@ export const generateDocument = async (req: AuthRequest, res: Response) => {
}
if (application.email) {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
await NotificationService.notify(null, application.email, {
title: `Statutory documents — ${application.applicationId}`,
message: 'Please upload statutory and compliance documents on the Dealer Portal.',

View File

@ -217,6 +217,10 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
applicantName: displayApplicantName,
location: displayLocation,
applicationId,
link: isOpportunityAvailable
? `${getFrontendBaseUrl()}/questionnaire/${applicationId}`
: undefined,
ctaLabel: isOpportunityAvailable ? 'Complete Questionnaire' : undefined,
phone
}
}).catch((err: any) => console.error('[Onboarding] WhatsApp ack failed:', err));
@ -722,7 +726,7 @@ export const uploadDocuments = async (req: any, res: Response) => {
});
if (application.email) {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
await NotificationService.notify(null, application.email, {
title: `Document received — ${application.applicationId}`,
message: `We received your upload: ${documentType}.`,
@ -846,7 +850,7 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
application.applicationId
).catch(err => console.error('Failed to send shortlist email:', err));
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const applicantUser = await User.findOne({
where: { email: application.email },
attributes: ['id', 'mobileNumber']
@ -1149,7 +1153,7 @@ export const assignArchitectureTeam = async (req: AuthRequest, res: Response) =>
});
const architect = await User.findByPk(targetUserId, { attributes: ['fullName'] });
const portalBaseArch = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBaseArch = getFrontendBaseUrl();
if (application.email) {
await NotificationService.notify(null, application.email, {
title: `Architecture & site inputs — ${application.applicationId}`,
@ -1215,6 +1219,7 @@ export const updateArchitectureStatus = async (req: AuthRequest, res: Response)
};
import { ExternalMocksService } from '../../common/utils/externalMocks.service.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
try {
@ -1746,7 +1751,7 @@ export const sendBulkDocumentReminders = async (req: AuthRequest, res: Response)
applicationId: app.applicationId,
pendingDocuments: pendingDocuments || undefined,
dueDate: dueDate || formatDueDateDaysFromNow(7),
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${app.id}`
link: `${getFrontendBaseUrl()}/applications/${app.id}`
});
await safeAuditLogCreate({
userId: req.user?.id || null,
@ -1798,7 +1803,7 @@ export const sendBulkLoiAckReminders = async (req: AuthRequest, res: Response) =
await NotificationService.sendLoiAcknowledgementReminder(app.email, app.phone, app.applicantName, {
applicationId: app.applicationId,
dueDate: dueDate || formatDueDateDaysFromNow(7),
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/prospect-login`
link: `${getFrontendBaseUrl()}/prospect-login`
});
sent++;
}
@ -1842,7 +1847,7 @@ export const rejectOnboardingDocument = async (req: AuthRequest, res: Response)
await doc.update({ status: 'rejected' });
if (application.email) {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
await NotificationService.notify(null, application.email, {
title: `Document re-upload required — ${application.applicationId}`,
message: `A document was not accepted: ${doc.documentType}.`,

View File

@ -23,6 +23,7 @@ import { OFFBOARDING_ACTIONS, REQUEST_TYPES } from '../../common/config/constant
import { validateOffboardingAction, getPreviousStage } from '../../common/utils/offboardingWorkflow.utils.js';
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
import { notifyConstitutionalSubmittedEmails } from '../../common/utils/workflow-email-notifications.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
const STRUCTURE_TARGET_VALUES = new Set<string>(
CONSTITUTIONAL_STRUCTURE_TARGET_OPTIONS.map((o) => o.value as string)
@ -674,7 +675,7 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
}]
});
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
for (const p of remainingParticipants) {
const u = (p as any).user;

View File

@ -23,6 +23,7 @@ import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
import { OFFBOARDING_ACTIONS } from '../../common/config/constants.js';
import { validateOffboardingAction, getPreviousStage } from '../../common/utils/offboardingWorkflow.utils.js';
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
// Removed generateResignationId and moved to NomenclatureService
const resolveResignationUuid = async (id: string) => {
@ -330,7 +331,7 @@ export const uploadResignationDocument = async (req: AuthRequest, res: Response,
// SRS §7.5.2 — When Legal uploads the acceptance letter, notify DD-Admin and ASM
// so they can communicate official closure to the dealer (field hierarchy).
if (stage === RESIGNATION_STAGES.LEGAL && resignation.currentStage === RESIGNATION_STAGES.LEGAL) {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const resignationCode = resignation.resignationId || resignation.id;
setImmediate(() =>
notifyStakeholdersOnTransition(

View File

@ -11,8 +11,9 @@ import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorkno
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
import { NotificationService } from '../../services/NotificationService.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const LINE_ITEM_DESCRIPTION_PREFIX = {
DEPARTMENT_CLAIM: '[DEPARTMENT_CLAIM]',

View File

@ -26,6 +26,7 @@ import { OFFBOARDING_ACTIONS, REQUEST_TYPES } from '../../common/config/constant
import { validateOffboardingAction, getPreviousStage } from '../../common/utils/offboardingWorkflow.utils.js';
import { NotificationService } from '../../services/NotificationService.js';
import { sendEmail } from '../../common/utils/email.service.js';
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
const resolveTerminationUuid = async (id: string) => {
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'termination');
@ -101,7 +102,7 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N
dealerName: '',
requestId: termination.requestId,
reason: reason || '',
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/termination/${termination.id}`,
link: `${getFrontendBaseUrl()}/termination/${termination.id}`,
ctaLabel: 'Review Request',
phone: phone || ''
}
@ -758,7 +759,7 @@ export const issueScn = async (req: AuthRequest, res: Response, next: NextFuncti
{
dealerName: dealerUser.fullName || 'Dealer',
requestId: termination.requestId,
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/dealer-termination/${termination.id}`,
link: `${getFrontendBaseUrl()}/dealer-termination/${termination.id}`,
ctaLabel: 'View Notice'
}
).catch((e: any) => logger.error('[Termination] SCN email to dealer failed:', e));
@ -777,7 +778,7 @@ export const issueScn = async (req: AuthRequest, res: Response, next: NextFuncti
placeholders: {
dealerName: dealerUser?.fullName || '',
requestId: termination.requestId,
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/termination/${termination.id}`,
link: `${getFrontendBaseUrl()}/termination/${termination.id}`,
ctaLabel: 'View Case'
}
}).catch((e: any) => logger.error('[Termination] SCN admin/legal notify failed:', e));

View File

@ -44,6 +44,7 @@ import terminationRoutes from './modules/termination/termination.routes.js';
// Import common middleware & utils
import errorHandler from './common/middleware/errorHandler.js';
import logger from './common/utils/logger.js';
import { getFrontendBaseUrl } from './common/utils/frontendUrl.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -63,14 +64,14 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
"frame-ancestors": ["'self'", process.env.FRONTEND_URL || 'http://localhost:5173'],
"frame-ancestors": ["'self'", getFrontendBaseUrl()],
},
},
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: { policy: "cross-origin" }
}));
const allowedOrigins = new Set<string>([
process.env.FRONTEND_URL || 'http://localhost:5173',
getFrontendBaseUrl(),
'http://localhost:5173'
]);
app.use(cors({

View File

@ -5,6 +5,7 @@ import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.
import { getOffboardingAuditAction, formatOffboardingAction } from '../common/utils/offboardingWorkflow.utils.js';
import { mapConstitutionalChangeTypeToDealerProfile } from '../common/utils/constitutionalNormalize.js';
import { syncSlaOnStageTransition } from '../common/utils/slaWorkflowSync.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
export class ConstitutionalWorkflowService {
private static normalizeDocLabel(input: string): string {
@ -197,7 +198,7 @@ export class ConstitutionalWorkflowService {
const dealerUser = await db.User.findByPk(request.dealerId, { attributes: ['id', 'email', 'fullName'] });
if (dealerUser?.email) {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const remarkText = String(remarks ?? '').trim() || 'N/A';
const { notifyStakeholdersOnTransition } = await import('../common/utils/workflow-email-notifications.js');

View File

@ -1,4 +1,5 @@
import { sendEmail } from '../common/utils/email.service.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
import db from '../database/models/index.js';
const { Notification, PushSubscription } = db;
@ -147,7 +148,6 @@ export class NotificationService {
applicantName: string,
options?: { location?: string; link?: string }
) {
const base = process.env.FRONTEND_URL || 'http://localhost:5173';
await this.notify(null, email, {
title: 'Action Required: Complete your Dealership Questionnaire',
message: `Hi ${applicantName}, please complete the questionnaire to proceed with your application.`,
@ -157,8 +157,8 @@ export class NotificationService {
applicantName,
phone,
location: options?.location ?? '',
link: options?.link ?? `${base}/login`,
ctaLabel: 'Complete Now'
link: options?.link ?? `${getFrontendBaseUrl()}/prospect-login`,
ctaLabel: 'Complete Questionnaire'
}
});
}
@ -172,7 +172,6 @@ export class NotificationService {
applicantName: string,
options: { applicationId: string; pendingDocuments?: string; dueDate?: string; link?: string }
) {
const base = process.env.FRONTEND_URL || 'http://localhost:5173';
const channels: ('email' | 'whatsapp')[] = ['email'];
if (phone) channels.push('whatsapp');
await this.notify(null, email, {
@ -187,7 +186,7 @@ export class NotificationService {
options.pendingDocuments ||
'Please sign in to the Dealer Portal to view your personalised document checklist.',
dueDate: options.dueDate || 'within seven calendar days',
link: options.link || `${base}/applications`,
link: options.link || `${getFrontendBaseUrl()}/applications`,
ctaLabel: 'Upload documents',
phone: phone || ''
}
@ -203,7 +202,6 @@ export class NotificationService {
applicantName: string,
options: { applicationId: string; link?: string; dueDate?: string }
) {
const base = process.env.FRONTEND_URL || 'http://localhost:5173';
const channels: ('email' | 'whatsapp')[] = ['email'];
if (phone) channels.push('whatsapp');
await this.notify(null, email, {
@ -215,7 +213,7 @@ export class NotificationService {
applicantName,
applicationId: options.applicationId,
dueDate: options.dueDate || 'within seven calendar days',
link: options.link || `${base}/prospect-login`,
link: options.link || `${getFrontendBaseUrl()}/prospect-login`,
ctaLabel: 'Acknowledge LOI',
phone: phone || ''
}

View File

@ -13,6 +13,7 @@ import {
msUntilLwdMorning
} from '../common/utils/offboardingLwd.js';
import logger from '../common/utils/logger.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
const FNF_PUSH_ROLES = [ROLES.DD_LEAD, ROLES.DD_HEAD, ROLES.NBH, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN];
@ -45,7 +46,7 @@ export class OffboardingLwdReminderService {
lwd: Date | string;
detailPath: string;
}): Promise<void> {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const lwdStr = formatLwdDisplay(params.lwd);
const link = `${portalBase}${params.detailPath}`;

View File

@ -6,6 +6,7 @@ import { safeAuditLogCreate } from './applicationAuditLog.service.js';
import { pickApplicationAuditContext } from './applicationAuditLog.service.js';
import logger from '../common/utils/logger.js';
import { getQuestionnaireReminderSettings } from './questionnaireReminderSettings.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
const TEMPLATE = 'QUESTIONNAIRE_REMINDER';
@ -67,8 +68,7 @@ export class QuestionnaireReminderService {
}
static async sendReminderForApplication(application: any, source: 'scheduled' | 'manual' = 'scheduled') {
const base = process.env.FRONTEND_URL || 'http://localhost:5173';
const link = `${base}/questionnaire/${application.applicationId}`;
const link = `${getFrontendBaseUrl()}/questionnaire/${application.applicationId}`;
await NotificationService.sendQuestionnaireReminder(
application.email,

View File

@ -6,6 +6,7 @@ import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.
import logger from '../common/utils/logger.js';
import { NotificationService } from './NotificationService.js';
import { syncSlaOnStageTransition } from '../common/utils/slaWorkflowSync.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
export class RelocationWorkflowService {
/**
@ -102,7 +103,7 @@ export class RelocationWorkflowService {
await request.reload();
const dealerUser = await User.findByPk(request.dealerId, { attributes: ['id', 'email', 'fullName'] });
if (dealerUser?.email) {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const stageLabel = request.currentStage || request.status || targetStatus;
const { notifyStakeholdersOnTransition } = await import('../common/utils/workflow-email-notifications.js');

View File

@ -9,6 +9,7 @@ import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.
import { getOffboardingAuditAction, formatOffboardingAction } from '../common/utils/offboardingWorkflow.utils.js';
import { NomenclatureService } from '../common/utils/nomenclature.js';
import { syncSlaOnStageTransition } from '../common/utils/slaWorkflowSync.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
export class ResignationWorkflowService {
@ -96,7 +97,7 @@ export class ResignationWorkflowService {
});
if (user) {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const { notifyStakeholdersOnTransition } = await import('../common/utils/workflow-email-notifications.js');

View File

@ -8,6 +8,7 @@ import { resolveRecipientsForRoles } from './slaGeographyResolver.js';
import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.js';
import type { WorkflowActivityRequestType } from '../common/utils/workflowWorknote.js';
import {
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
claimSlaNotificationDispatch,
SlaDispatchKeys,
updateSlaDispatchRecipientCount,
@ -496,7 +497,7 @@ export class SLAService {
}
private static linkForEntity(entityType: string, entityId: string): string {
const base = process.env.FRONTEND_URL || 'http://localhost:5173';
const base = getFrontendBaseUrl();
switch (entityType) {
case 'application':
return `${base}/applications/${entityId}`;

View File

@ -4,6 +4,7 @@ import { Op } from 'sequelize';
import { slaConfigLookupNames } from '../common/config/slaStageCatalog.js';
import { computeSlaTrackView, formatSlaDuration, type SlaBucket } from '../common/utils/slaMetrics.js';
import { SlaStatusService } from './SlaStatusService.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
export type { SlaBucket };
@ -20,7 +21,7 @@ function moduleFromEntityType(entityType: string): string {
}
function linkForEntity(entityType: string, entityId: string): string {
const base = process.env.FRONTEND_URL || 'http://localhost:5173';
const base = getFrontendBaseUrl();
switch (entityType) {
case 'application':
return `${base}/applications/${entityId}`;

View File

@ -11,6 +11,7 @@ import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.
import { getOffboardingAuditAction, formatOffboardingAction } from '../common/utils/offboardingWorkflow.utils.js';
import { ParticipantService } from './ParticipantService.js';
import { syncSlaOnStageTransition } from '../common/utils/slaWorkflowSync.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
export class TerminationWorkflowService {
/**
@ -103,7 +104,7 @@ export class TerminationWorkflowService {
});
if (user) {
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
const dealerPortalLink = `${portalBase}/termination/${termination.id}`;
const isScnIssued = targetStage === TERMINATION_STAGES.SCN_ISSUED;
@ -263,7 +264,7 @@ export class TerminationWorkflowService {
attributes: ['id', 'email', 'fullName', 'mobileNumber']
});
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
const portalBase = getFrontendBaseUrl();
for (const u of adminUsers) {
const phone = u.mobileNumber || null;
await NotificationService.notify(u.id, u.email, {

View File

@ -10,6 +10,7 @@ import {
} from '../common/config/constants.js';
import { NotificationService } from './NotificationService.js';
import { pickApplicationAuditContext, safeAuditLogCreate } from './applicationAuditLog.service.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
export class WorkflowService {
/**
@ -150,11 +151,13 @@ export class WorkflowService {
if (targetStatus === 'LOA Issued') templateCode = 'LOA_ISSUED';
if (targetStatus === 'Dealer Code Generated') templateCode = 'DEALER_CODE_READY';
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
let ctaLabel = 'View application';
if (templateCode === 'LOI_ISSUED') ctaLabel = 'View LOI';
else if (templateCode === 'LOA_ISSUED') ctaLabel = 'View LOA';
if (['Rejected', 'LOI Rejected', 'LOA Rejected', 'Disqualified'].includes(targetStatus)) {
templateCode = 'APPLICANT_REJECTED';
ctaLabel = 'View application';
}
await NotificationService.notify(targetUserId, application.email, {
title: `Onboarding Update: ${targetStatus}`,
@ -165,10 +168,12 @@ export class WorkflowService {
status: targetStatus,
applicantName: application.applicantName,
applicationId: application.applicationId,
location: application.preferredLocation || application.city || '',
rejectionReason: reason || 'N/A',
reason: reason || 'N/A',
salesCode: application.dealerCode?.salesCode || 'N/A',
serviceCode: application.dealerCode?.serviceCode || 'N/A',
link: `${portalBase}/applications/${application.id}`,
link: `${getFrontendBaseUrl()}/applications/${application.id}`,
ctaLabel,
phone: user?.mobileNumber || user?.phone || application?.mobileNumber || application?.phone || ''
},
@ -179,8 +184,6 @@ export class WorkflowService {
// Stakeholder Notifications
tasks.push((async () => {
const { notifyStakeholdersOnTransition } = await import('../common/utils/workflow-email-notifications.js');
const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173';
let actionUserFullName = 'System';
if (userId) {
const actionUser = await User.findByPk(userId, { attributes: ['fullName'] });
@ -198,7 +201,7 @@ export class WorkflowService {
actionUserFullName,
action: reason || `Transitioned to ${targetStatus}`,
remarks: reason || 'N/A',
link: `${portalBase}/applications/${application.id}`
link: `${getFrontendBaseUrl()}/applications/${application.id}`
}
);
})().catch(e => console.error('[WorkflowService] stakeholder notify failed:', e)));