sms-extension-1777538290/client/src/utils/templateWorkspace.js

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;
}