450 lines
15 KiB
JavaScript
450 lines
15 KiB
JavaScript
const DLT_PLACEHOLDER_REGEX = /\{#(?:var|numeric|url|urlott|cbn|email|alphanumeric)#\}/g;
|
|
|
|
const PLACEHOLDER_SAMPLE_FIELD_CANDIDATES = {
|
|
'{#var#}': ['firstName', 'customerName', 'fullName', 'brandName', 'eventDisplayName'],
|
|
'{#numeric#}': ['otp', 'amount', 'refundAmount', 'pincode', 'toNumber'],
|
|
'{#url#}': ['trackingUrl', 'url', 'trackUrl', 'trackingLink'],
|
|
'{#urlott#}': ['trackingUrl', 'url', 'trackUrl', 'trackingLink'],
|
|
'{#cbn#}': ['callbackNumber', 'toNumber', 'customerPhone', 'mobile', 'phone'],
|
|
'{#email#}': ['email', 'customerEmail'],
|
|
'{#alphanumeric#}': ['orderId', 'transactionId', 'shipmentId', 'awbNumber', 'awbNo'],
|
|
};
|
|
|
|
const EVENT_SAMPLE_OVERRIDES = {
|
|
payment_failed: {
|
|
shipment: {
|
|
payment_status: 'failed',
|
|
transaction_id: 'TXN9012457812',
|
|
amount: '2499',
|
|
failure_reason: 'UPI mandate expired',
|
|
},
|
|
},
|
|
payment_initiated: {
|
|
shipment: {
|
|
payment_status: 'initiated',
|
|
transaction_id: 'TXN9012457812',
|
|
amount: '2499',
|
|
},
|
|
},
|
|
refund_initiated: {
|
|
shipment: {
|
|
refund_status: 'initiated',
|
|
refund_amount: '2499',
|
|
refund_id: 'RFD1204982',
|
|
},
|
|
},
|
|
refund_completed: {
|
|
shipment: {
|
|
refund_status: 'completed',
|
|
refund_amount: '2499',
|
|
refund_id: 'RFD1204982',
|
|
},
|
|
},
|
|
out_for_delivery: {
|
|
shipment: {
|
|
otp: '482193',
|
|
estimated_delivery_slot: '6:00 PM to 8:00 PM',
|
|
},
|
|
},
|
|
delivery_attempt_failed: {
|
|
shipment: {
|
|
failure_reason: 'Customer unavailable',
|
|
callback_number: '919876543210',
|
|
},
|
|
},
|
|
delivery_done: {
|
|
shipment: {
|
|
delivered_at: '2026-04-06T14:18:00.000Z',
|
|
otp: '482193',
|
|
},
|
|
},
|
|
order_placed: {
|
|
shipment: {
|
|
payment_status: 'paid',
|
|
expected_dispatch_date: '2026-04-07',
|
|
},
|
|
},
|
|
};
|
|
|
|
function normalizeScalarText(value) {
|
|
if (typeof value === 'string') return value.trim();
|
|
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
return '';
|
|
}
|
|
|
|
function firstNonEmptyText(...values) {
|
|
for (const value of values) {
|
|
const normalized = normalizeScalarText(value);
|
|
if (normalized) return normalized;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function toCamelCase(text) {
|
|
return String(text || '')
|
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.map((part, index) => {
|
|
const lower = part.toLowerCase();
|
|
return index === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
function setValueIndexEntry(valueIndex, key, value) {
|
|
if (!key || valueIndex.has(key)) return;
|
|
valueIndex.set(key, value);
|
|
}
|
|
|
|
function normalizeRenderableValue(value) {
|
|
return normalizeScalarText(value).replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function splitFullName(value) {
|
|
const fullName = normalizeRenderableValue(value);
|
|
if (!fullName) return { firstName: '', lastName: '', fullName: '' };
|
|
|
|
const parts = fullName.split(/\s+/).filter(Boolean);
|
|
return {
|
|
firstName: parts[0] || '',
|
|
lastName: parts.length > 1 ? parts.slice(1).join(' ') : '',
|
|
fullName,
|
|
};
|
|
}
|
|
|
|
function indexPayloadValues(value, pathParts = [], valueIndex = new Map()) {
|
|
if (Array.isArray(value)) {
|
|
value.forEach((entry) => indexPayloadValues(entry, pathParts, valueIndex));
|
|
return valueIndex;
|
|
}
|
|
|
|
if (value && typeof value === 'object') {
|
|
Object.entries(value).forEach(([key, entry]) => {
|
|
indexPayloadValues(entry, [...pathParts, key], valueIndex);
|
|
});
|
|
return valueIndex;
|
|
}
|
|
|
|
const normalizedValue = normalizeRenderableValue(value);
|
|
if (!normalizedValue || pathParts.length === 0) return valueIndex;
|
|
|
|
const leafKey = toCamelCase(pathParts[pathParts.length - 1]);
|
|
const fullKey = toCamelCase(pathParts.join(' '));
|
|
setValueIndexEntry(valueIndex, leafKey, normalizedValue);
|
|
setValueIndexEntry(valueIndex, fullKey, normalizedValue);
|
|
return valueIndex;
|
|
}
|
|
|
|
function buildShipmentValueIndex(shipment) {
|
|
const valueIndex = indexPayloadValues(shipment);
|
|
const firstBag = shipment?.bags?.[0] || {};
|
|
const customerName = splitFullName(
|
|
firstNonEmptyText(
|
|
shipment?.user?.first_name && shipment?.user?.last_name
|
|
? `${shipment.user.first_name} ${shipment.user.last_name}`
|
|
: '',
|
|
shipment?.delivery_address?.name,
|
|
shipment?.delivery_address?.contact_person,
|
|
shipment?.billing_address?.name,
|
|
shipment?.billing_address?.contact_person,
|
|
)
|
|
);
|
|
const primaryTrackingUrl = firstNonEmptyText(
|
|
shipment?.delivery_partner_details?.track_url,
|
|
shipment?.meta?.tracking_url,
|
|
firstBag?.meta?.tracking_url,
|
|
shipment?.affiliate_details?.shipment_meta?.tracking_url,
|
|
);
|
|
const primaryAwbNumber = firstNonEmptyText(
|
|
shipment?.delivery_partner_details?.awb_no,
|
|
shipment?.meta?.awb_number,
|
|
firstBag?.meta?.awb_number,
|
|
);
|
|
const primaryCourierName = firstNonEmptyText(
|
|
shipment?.delivery_partner_details?.display_name,
|
|
shipment?.delivery_partner_details?.name,
|
|
shipment?.meta?.courier_partner_name,
|
|
firstBag?.meta?.dp_name,
|
|
);
|
|
const brandName = firstNonEmptyText(
|
|
shipment?.bags?.[0]?.brand?.brand_name,
|
|
shipment?.bags?.[0]?.item?.attributes?.brand_name,
|
|
shipment?.affiliate_details?.company_affiliate_tag,
|
|
);
|
|
const toNumber = firstNonEmptyText(
|
|
shipment?.user?.mobile,
|
|
shipment?.delivery_address?.phone,
|
|
shipment?.billing_address?.phone,
|
|
);
|
|
const emailAddress = firstNonEmptyText(
|
|
shipment?.user?.email,
|
|
shipment?.delivery_address?.email,
|
|
shipment?.billing_address?.email,
|
|
);
|
|
const eventKey = firstNonEmptyText(
|
|
shipment?.status,
|
|
shipment?.shipment_status?.status,
|
|
shipment?.shipment_status?.current_shipment_status,
|
|
);
|
|
const eventDisplayName = firstNonEmptyText(
|
|
shipment?.shipment_status?.display_name,
|
|
shipment?.shipment_status?.current_shipment_status,
|
|
);
|
|
const shipmentId = firstNonEmptyText(
|
|
shipment?.shipment_id,
|
|
shipment?.shipment_status?.shipment_id,
|
|
);
|
|
const resolvedFullName = firstNonEmptyText(
|
|
shipment?.user?.first_name || shipment?.user?.last_name
|
|
? `${normalizeRenderableValue(shipment?.user?.first_name)} ${normalizeRenderableValue(shipment?.user?.last_name)}`.trim()
|
|
: '',
|
|
customerName.fullName,
|
|
);
|
|
const resolvedFirstName = firstNonEmptyText(shipment?.user?.first_name, customerName.firstName);
|
|
const resolvedLastName = firstNonEmptyText(shipment?.user?.last_name, customerName.lastName);
|
|
|
|
setValueIndexEntry(valueIndex, 'firstName', resolvedFirstName);
|
|
setValueIndexEntry(valueIndex, 'lastName', resolvedLastName);
|
|
setValueIndexEntry(valueIndex, 'fullName', resolvedFullName);
|
|
setValueIndexEntry(valueIndex, 'customerFirstName', resolvedFirstName);
|
|
setValueIndexEntry(valueIndex, 'customerLastName', resolvedLastName);
|
|
setValueIndexEntry(valueIndex, 'customerName', resolvedFullName);
|
|
setValueIndexEntry(valueIndex, 'phone', toNumber);
|
|
setValueIndexEntry(valueIndex, 'mobile', toNumber);
|
|
setValueIndexEntry(valueIndex, 'toNumber', toNumber);
|
|
setValueIndexEntry(valueIndex, 'customerPhone', toNumber);
|
|
setValueIndexEntry(valueIndex, 'customerMobile', toNumber);
|
|
setValueIndexEntry(valueIndex, 'email', emailAddress);
|
|
setValueIndexEntry(valueIndex, 'customerEmail', emailAddress);
|
|
setValueIndexEntry(valueIndex, 'orderId', normalizeRenderableValue(shipment?.order_id));
|
|
setValueIndexEntry(valueIndex, 'orderNumber', normalizeRenderableValue(shipment?.order_id));
|
|
setValueIndexEntry(valueIndex, 'shipmentId', shipmentId);
|
|
setValueIndexEntry(valueIndex, 'event', eventKey);
|
|
setValueIndexEntry(valueIndex, 'status', eventKey);
|
|
setValueIndexEntry(valueIndex, 'eventDisplayName', eventDisplayName);
|
|
setValueIndexEntry(valueIndex, 'displayName', eventDisplayName);
|
|
setValueIndexEntry(valueIndex, 'brandName', brandName);
|
|
setValueIndexEntry(valueIndex, 'trackingUrl', primaryTrackingUrl);
|
|
setValueIndexEntry(valueIndex, 'trackUrl', primaryTrackingUrl);
|
|
setValueIndexEntry(valueIndex, 'trackingLink', primaryTrackingUrl);
|
|
setValueIndexEntry(valueIndex, 'url', primaryTrackingUrl);
|
|
setValueIndexEntry(valueIndex, 'awbNo', primaryAwbNumber);
|
|
setValueIndexEntry(valueIndex, 'awbNumber', primaryAwbNumber);
|
|
setValueIndexEntry(valueIndex, 'awb', primaryAwbNumber);
|
|
setValueIndexEntry(valueIndex, 'dpName', primaryCourierName);
|
|
setValueIndexEntry(valueIndex, 'courierName', primaryCourierName);
|
|
setValueIndexEntry(valueIndex, 'deliveryPartnerName', primaryCourierName);
|
|
|
|
return valueIndex;
|
|
}
|
|
|
|
function mergeDeep(baseValue, overrideValue) {
|
|
if (Array.isArray(baseValue) || Array.isArray(overrideValue)) {
|
|
return overrideValue !== undefined ? overrideValue : baseValue;
|
|
}
|
|
|
|
if (baseValue && typeof baseValue === 'object' && overrideValue && typeof overrideValue === 'object') {
|
|
const nextValue = { ...baseValue };
|
|
Object.entries(overrideValue).forEach(([key, value]) => {
|
|
nextValue[key] = key in nextValue ? mergeDeep(nextValue[key], value) : value;
|
|
});
|
|
return nextValue;
|
|
}
|
|
|
|
return overrideValue !== undefined ? overrideValue : baseValue;
|
|
}
|
|
|
|
function titleCaseFromSlug(slug) {
|
|
return String(slug || '')
|
|
.split('_')
|
|
.filter(Boolean)
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(' ');
|
|
}
|
|
|
|
function isRenderablePreviewValueForToken(token, value) {
|
|
const normalizedValue = normalizeRenderableValue(value);
|
|
if (!normalizedValue) return false;
|
|
|
|
switch (token) {
|
|
case '{#numeric#}':
|
|
return /^\d+$/.test(normalizedValue);
|
|
case '{#url#}':
|
|
case '{#urlott#}':
|
|
return /^https?:\/\//i.test(normalizedValue);
|
|
case '{#cbn#}':
|
|
return /^\+?[0-9][0-9\s-]{5,}$/.test(normalizedValue);
|
|
case '{#email#}':
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedValue);
|
|
case '{#alphanumeric#}':
|
|
return /^[A-Za-z0-9]+$/.test(normalizedValue);
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function resolvePlaceholderSampleFallback(token, shipmentValueIndex) {
|
|
const candidateFields = PLACEHOLDER_SAMPLE_FIELD_CANDIDATES[token] || [];
|
|
|
|
for (const fieldName of candidateFields) {
|
|
const resolvedValue = shipmentValueIndex.get(fieldName) || '';
|
|
if (isRenderablePreviewValueForToken(token, resolvedValue)) {
|
|
return {
|
|
fieldName,
|
|
value: resolvedValue,
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function getTemplateSamplePayload(template = {}) {
|
|
const eventSlug = String(template?.eventSlug || '').trim();
|
|
const eventLabel = String(template?.eventLabel || '').trim() || titleCaseFromSlug(eventSlug);
|
|
const brandName = String(template?.brandName || '').trim() || 'Your Brand';
|
|
const override = EVENT_SAMPLE_OVERRIDES[eventSlug] || {};
|
|
|
|
const basePayload = {
|
|
payload: {
|
|
event: eventSlug,
|
|
company_id: 'dev_merchant_001',
|
|
application_id: 'application-demo-001',
|
|
shipment: {
|
|
application_id: 'application-demo-001',
|
|
order_id: 'FY5E53AFAA091115C235',
|
|
shipment_id: 'SHP784512',
|
|
status: eventSlug || 'order_placed',
|
|
shipment_status: {
|
|
status: eventSlug || 'order_placed',
|
|
current_shipment_status: eventSlug || 'order_placed',
|
|
display_name: eventLabel || 'Order Update',
|
|
shipment_id: 'SHP784512',
|
|
},
|
|
user: {
|
|
first_name: 'Aarav',
|
|
last_name: 'Sharma',
|
|
mobile: '919876543210',
|
|
email: '[email protected]',
|
|
},
|
|
delivery_address: {
|
|
name: 'Aarav Sharma',
|
|
phone: '919876543210',
|
|
email: '[email protected]',
|
|
city: 'Bengaluru',
|
|
pincode: '560001',
|
|
},
|
|
billing_address: {
|
|
name: 'Aarav Sharma',
|
|
phone: '919876543210',
|
|
email: '[email protected]',
|
|
},
|
|
delivery_partner_details: {
|
|
display_name: 'Blue Dart',
|
|
track_url: 'https://tracking.example.com/SHP784512',
|
|
awb_no: '78451236985',
|
|
},
|
|
affiliate_details: {
|
|
affiliate_id: 'application-demo-001',
|
|
company_affiliate_tag: brandName,
|
|
shipment_meta: {
|
|
tracking_url: 'https://tracking.example.com/SHP784512',
|
|
courier_partner_name: 'Blue Dart',
|
|
},
|
|
},
|
|
meta: {
|
|
tracking_url: 'https://tracking.example.com/SHP784512',
|
|
awb_number: '78451236985',
|
|
courier_partner_name: 'Blue Dart',
|
|
},
|
|
bags: [
|
|
{
|
|
brand: { brand_name: brandName },
|
|
item: {
|
|
name: 'Midnight Duffle',
|
|
attributes: { brand_name: brandName },
|
|
},
|
|
meta: {
|
|
tracking_url: 'https://tracking.example.com/SHP784512',
|
|
awb_number: '78451236985',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
return mergeDeep(basePayload, override);
|
|
}
|
|
|
|
export function buildTemplateSampleRender(templateText, variableMap = {}, samplePayload = {}) {
|
|
const text = String(templateText || '');
|
|
if (!text) {
|
|
return {
|
|
text: '',
|
|
fallbackPlaceholders: [],
|
|
unresolvedPlaceholders: [],
|
|
};
|
|
}
|
|
|
|
const shipment = samplePayload?.payload?.shipment || samplePayload?.shipment || {};
|
|
const shipmentValueIndex = buildShipmentValueIndex(shipment);
|
|
let placeholderIndex = 0;
|
|
const fallbackPlaceholders = [];
|
|
const unresolvedPlaceholders = [];
|
|
|
|
const renderedText = text.replace(DLT_PLACEHOLDER_REGEX, (token) => {
|
|
const mappingKey = `${token}[${placeholderIndex}]`;
|
|
const mappedFieldName = normalizeScalarText(variableMap?.[mappingKey] || variableMap?.[token]);
|
|
placeholderIndex += 1;
|
|
|
|
const resolvedMappedValue = mappedFieldName
|
|
? shipmentValueIndex.get(toCamelCase(mappedFieldName)) || ''
|
|
: '';
|
|
|
|
if (resolvedMappedValue) return resolvedMappedValue;
|
|
|
|
const fallback = resolvePlaceholderSampleFallback(token, shipmentValueIndex);
|
|
if (fallback) {
|
|
fallbackPlaceholders.push({
|
|
mappingKey,
|
|
token,
|
|
mappedFieldName,
|
|
sampleFieldName: fallback.fieldName,
|
|
});
|
|
return fallback.value;
|
|
}
|
|
|
|
unresolvedPlaceholders.push({
|
|
mappingKey,
|
|
token,
|
|
mappedFieldName,
|
|
});
|
|
return token;
|
|
});
|
|
|
|
return {
|
|
text: renderedText,
|
|
fallbackPlaceholders,
|
|
unresolvedPlaceholders,
|
|
};
|
|
}
|
|
|
|
export function renderTemplateWithSamplePayload(templateText, variableMap = {}, samplePayload = {}) {
|
|
return buildTemplateSampleRender(templateText, variableMap, samplePayload).text;
|
|
}
|
|
|
|
export function getTemplateWorkspaceDescription(template = {}) {
|
|
const eventLabel = String(template?.eventLabel || '').trim() || titleCaseFromSlug(template?.eventSlug);
|
|
if (!eventLabel) return 'Review the current SMS template, sample payload, and delivery details.';
|
|
return `Use this SMS template for ${eventLabel.toLowerCase()} notifications.`;
|
|
}
|
|
|
|
export function getTemplateWorkspaceVariableCount(template = {}) {
|
|
return (String(template?.selectedTemplate || '').match(DLT_PLACEHOLDER_REGEX) || []).length;
|
|
}
|