diff --git a/client/src/pages/Events.jsx b/client/src/pages/Events.jsx
index 120e795..710be90 100644
--- a/client/src/pages/Events.jsx
+++ b/client/src/pages/Events.jsx
@@ -13,6 +13,120 @@ const DLT_VARIABLE_OPTIONS = [
const DLT_TOKEN_SET = new Set(DLT_VARIABLE_OPTIONS.map((option) => option.token));
const DLT_TOKEN_REGEX = /\{#(?:var|numeric|url|cbn)#\}/g;
const DLT_TOKEN_LIKE_REGEX = /\{#[^{}]*#\}/g;
+const DELIVERY_EVENT_SLUGS = new Set([
+ 'out_for_pickup',
+ 'bag_picked',
+ 'bag_reached_drop_point',
+ 'in_transit',
+ 'out_for_delivery',
+ 'delivery_attempt_failed',
+ 'delivery_done',
+ 'handed_over_to_customer',
+ 'bag_lost',
+]);
+const CANCELLATION_EVENT_SLUGS = new Set([
+ 'bag_not_confirmed',
+ 'cancelled_at_dp',
+ 'cancelled_customer',
+ 'cancelled_failed_at_dp',
+ 'cancelled_fynd',
+ 'rejected_by_customer',
+]);
+const REFUND_EVENT_SLUGS = new Set([
+ 'credit_note_generated',
+ 'partial_refund_completed',
+ 'refund_acknowledged',
+ 'refund_approved',
+ 'refund_completed',
+ 'refund_failed',
+ 'refund_initiated',
+ 'refund_on_hold',
+ 'refund_pending',
+ 'refund_pending_for_approval',
+ 'refund_retry',
+]);
+const RETURN_EVENT_SLUGS = new Set([
+ 'assigning_return_dp',
+ 'internal_return_dp_reassign',
+ 'deadstock_defective',
+ 'deadstock_defective_lost',
+]);
+const EVENT_GROUPS = [
+ {
+ id: 'fulfillment',
+ label: 'Order & Fulfillment',
+ description: 'Core order confirmation, allocation, packing, and dispatch readiness stages.',
+ defaultExpanded: true,
+ },
+ {
+ id: 'delivery',
+ label: 'Delivery Journey',
+ description: 'Courier pickup, in-transit updates, and final handover milestones.',
+ defaultExpanded: true,
+ },
+ {
+ id: 'cancellations',
+ label: 'Cancellations & Rejections',
+ description: 'Customer, merchant, and delivery-partner driven cancellations and rejections.',
+ defaultExpanded: false,
+ },
+ {
+ id: 'returns',
+ label: 'Returns',
+ description: 'Return initiation, pickup, transit, and merchant-side return handling.',
+ defaultExpanded: false,
+ },
+ {
+ id: 'refunds',
+ label: 'Refunds',
+ description: 'Refund processing and credit-note states across payment flows.',
+ defaultExpanded: false,
+ },
+ {
+ id: 'rto',
+ label: 'RTO',
+ description: 'Return-to-origin movement and completion states after failed delivery.',
+ defaultExpanded: false,
+ },
+ {
+ id: 'custom',
+ label: 'Custom Events',
+ description: 'Business-specific events you added manually for your own messaging flows.',
+ defaultExpanded: false,
+ },
+];
+const DEFAULT_EXPANDED_GROUPS = EVENT_GROUPS.reduce((acc, group) => {
+ acc[group.id] = group.defaultExpanded;
+ return acc;
+}, {});
+
+function getEventGroupId(event) {
+ const slug = String(event?.slug || '');
+
+ if (!event?.isDefault) return 'custom';
+ if (slug.startsWith('rto_') || slug === 'return_to_origin') return 'rto';
+ if (slug.startsWith('return_') || RETURN_EVENT_SLUGS.has(slug)) return 'returns';
+ if (slug.startsWith('refund_') || REFUND_EVENT_SLUGS.has(slug)) return 'refunds';
+ if (CANCELLATION_EVENT_SLUGS.has(slug)) return 'cancellations';
+ if (DELIVERY_EVENT_SLUGS.has(slug)) return 'delivery';
+ return 'fulfillment';
+}
+
+function matchesEventSearch(event, searchTerm) {
+ const query = String(searchTerm || '').trim().toLowerCase();
+ if (!query) return true;
+
+ return [event?.label, event?.slug]
+ .filter(Boolean)
+ .some((value) => String(value).toLowerCase().includes(query));
+}
+
+function buildGroupedEvents(events, searchTerm) {
+ return EVENT_GROUPS.map((group) => ({
+ ...group,
+ events: events.filter((event) => getEventGroupId(event) === group.id && matchesEventSearch(event, searchTerm)),
+ })).filter((group) => group.events.length > 0);
+}
function getVariantKey(slug, index) {
return `${slug}:${index}`;
@@ -105,8 +219,10 @@ export default function Events() {
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [newLabel, setNewLabel] = useState('');
+ const [searchTerm, setSearchTerm] = useState('');
const [addingEvent, setAddingEvent] = useState(false);
const [showAddForm, setShowAddForm] = useState(false);
+ const [expandedGroups, setExpandedGroups] = useState(DEFAULT_EXPANDED_GROUPS);
const [genState, setGenState] = useState({});
const [variants, setVariants] = useState({});
const [variantDrafts, setVariantDrafts] = useState({});
@@ -350,6 +466,13 @@ export default function Events() {
});
}
+ function toggleGroup(groupId) {
+ setExpandedGroups((current) => ({
+ ...current,
+ [groupId]: !current[groupId],
+ }));
+ }
+
if (loading) {
return (
@@ -358,19 +481,55 @@ export default function Events() {
);
}
+ const groupedEvents = buildGroupedEvents(events, searchTerm);
+ const totalVisibleEvents = groupedEvents.reduce((count, group) => count + group.events.length, 0);
+
return (
-
+
Events
Generate SMS templates for each order event.
-
+
+
+
+
+
+
setSearchTerm(e.target.value)}
+ placeholder="Search events"
+ className="w-full rounded-xl border border-gray-300 bg-white py-3 pl-11 pr-10 text-sm font-medium text-gray-900 placeholder-gray-400 shadow-sm transition focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100"
+ />
+ {searchTerm && (
+
+ )}
+
+
+
+
+ {totalVisibleEvents} visible
+
+
+
+
{!readyToGenerate && (
@@ -406,8 +565,56 @@ export default function Events() {
)}
-
- {events.map((event) => {
+ {groupedEvents.length === 0 ? (
+
+
No events match your search.
+
Try a different keyword or clear the search to see the full lifecycle list.
+
+ ) : (
+
+ {groupedEvents.map((group) => {
+ const isExpanded = searchTerm.trim() ? true : !!expandedGroups[group.id];
+
+ return (
+
+
+
+ {isExpanded && (
+
+
+ {group.events.map((event) => {
const state = genState[event.slug] || 'idle';
const eventVariants = variants[event.slug] || [];
@@ -430,7 +637,6 @@ export default function Events() {
)}
{event.label}
-
{event.slug}
@@ -695,7 +901,14 @@ export default function Events() {
);
})}
-
+
+
+ )}
+
+ );
+ })}
+
+ )}
);
}
diff --git a/server/config/defaultEvents.js b/server/config/defaultEvents.js
index 1c688d6..5b9bf99 100644
--- a/server/config/defaultEvents.js
+++ b/server/config/defaultEvents.js
@@ -1,10 +1,102 @@
-const DEFAULT_EVENTS = [
- { slug: 'placed', label: 'Placed', isDefault: true },
- { slug: 'confirmed', label: 'Confirmed', isDefault: true },
- { slug: 'dp_assigned', label: 'DP Assigned', isDefault: true },
- { slug: 'pack', label: 'Pack', isDefault: true },
- { slug: 'cancelled', label: 'Cancelled', isDefault: true },
- { slug: 'delivery_done', label: 'Delivery Done', isDefault: true },
+const OMS_EVENT_SLUGS = [
+ 'assigning_dp',
+ 'assigning_return_dp',
+ 'bag_confirmed',
+ 'bag_invoiced',
+ 'bag_lost',
+ 'bag_not_confirmed',
+ 'bag_not_handed_over_to_dg',
+ 'bag_not_picked',
+ 'bag_packed',
+ 'bag_picked',
+ 'bag_reached_drop_point',
+ 'cancelled_at_dp',
+ 'cancelled_customer',
+ 'cancelled_failed_at_dp',
+ 'cancelled_fynd',
+ 'credit_note_generated',
+ 'deadstock_defective',
+ 'deadstock_defective_lost',
+ 'delivery_attempt_failed',
+ 'delivery_done',
+ 'dp_assigned',
+ 'dp_not_assigned',
+ 'handed_over_to_customer',
+ 'handed_over_to_dg',
+ 'internal_dp_reassign',
+ 'internal_return_dp_reassign',
+ 'in_transit',
+ 'out_for_delivery',
+ 'out_for_pickup',
+ 'partial_refund_completed',
+ 'payment_failed',
+ 'payment_initiated',
+ 'pending',
+ 'placed',
+ 'ready_for_dp_assignment',
+ 'refund_acknowledged',
+ 'refund_approved',
+ 'refund_completed',
+ 'refund_failed',
+ 'refund_initiated',
+ 'refund_on_hold',
+ 'refund_pending',
+ 'refund_pending_for_approval',
+ 'refund_retry',
+ 'rejected_by_customer',
+ 'return_accepted',
+ 'return_assigning_dp',
+ 'return_bag_delivered',
+ 'return_bag_in_transit',
+ 'return_bag_lost',
+ 'return_bag_not_delivered',
+ 'return_bag_not_picked',
+ 'return_bag_out_for_delivery',
+ 'return_bag_picked',
+ 'return_cancelled_at_dp',
+ 'return_cancelled_failed_at_dp',
+ 'return_dp_assigned',
+ 'return_dp_assignment_requested',
+ 'return_dp_cancelled',
+ 'return_dp_not_assigned',
+ 'return_dp_out_for_pickup',
+ 'return_dp_qc_failed',
+ 'return_dp_qc_passed',
+ 'return_initiated',
+ 'return_not_accepted',
+ 'return_pre_qc',
+ 'return_rejected_by_dp',
+ 'return_request_cancelled',
+ 'return_request_rejected_by_operation',
+ 'return_to_origin',
+ 'rto_bag_accepted',
+ 'rto_bag_delivered',
+ 'rto_bag_out_for_delivery',
+ 'rto_initiated',
+ 'rto_in_transit',
+ 'store_reassigned',
+ 'upcoming',
];
+const WORD_OVERRIDES = {
+ dp: 'DP',
+ dg: 'DG',
+ qc: 'QC',
+ rto: 'RTO',
+};
+
+function humanizeEventSlug(slug) {
+ return String(slug || '')
+ .split('_')
+ .filter(Boolean)
+ .map((part) => WORD_OVERRIDES[part] || part.charAt(0).toUpperCase() + part.slice(1))
+ .join(' ');
+}
+
+const DEFAULT_EVENTS = OMS_EVENT_SLUGS.map((slug) => ({
+ slug,
+ label: humanizeEventSlug(slug),
+ isDefault: true,
+}));
+
module.exports = DEFAULT_EVENTS;
diff --git a/server/routes/businesses.js b/server/routes/businesses.js
index 952f012..01de111 100644
--- a/server/routes/businesses.js
+++ b/server/routes/businesses.js
@@ -155,6 +155,21 @@ function normalizeText(value) {
return typeof value === 'string' ? value.trim() : '';
}
+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 normalizeSenderId(value) {
return normalizeText(value).toUpperCase();
}
@@ -182,6 +197,42 @@ function normalizeProvider(provider = {}, fallbackUpdatedAt = null) {
};
}
+const LEGACY_DEFAULT_EVENT_SLUGS = new Set(['confirmed', 'pack', 'cancelled']);
+const EVENT_TEMPLATE_FALLBACKS = {
+ bag_confirmed: ['confirmed'],
+ bag_packed: ['pack'],
+ bag_not_confirmed: ['cancelled'],
+ cancelled_customer: ['cancelled'],
+ cancelled_fynd: ['cancelled'],
+ cancelled_at_dp: ['cancelled'],
+ cancelled_failed_at_dp: ['cancelled'],
+};
+
+function mergeDefaultEvents(data = {}) {
+ const existingEvents = Array.isArray(data?.events) ? data.events : [];
+ const defaultEventBySlug = new Map(DEFAULT_EVENTS.map((event) => [event.slug, event]));
+ const existingEventBySlug = new Map(
+ existingEvents
+ .map((event) => ({ ...event, slug: normalizeText(event?.slug) }))
+ .filter((event) => event.slug)
+ .map((event) => [event.slug, event])
+ );
+
+ const mergedDefaults = DEFAULT_EVENTS.map((event) => {
+ const existing = existingEventBySlug.get(event.slug);
+ return existing
+ ? { ...event, ...existing, slug: event.slug, label: existing.label || event.label, isDefault: true }
+ : { ...event };
+ });
+
+ const customEvents = existingEvents
+ .map((event) => ({ ...event, slug: normalizeText(event?.slug), label: normalizeText(event?.label) }))
+ .filter((event) => event.slug && !defaultEventBySlug.has(event.slug) && !LEGACY_DEFAULT_EVENT_SLUGS.has(event.slug))
+ .map((event) => ({ ...event, isDefault: false }));
+
+ return { events: [...mergedDefaults, ...customEvents] };
+}
+
function getShipmentPayload(body) {
return body?.payload?.shipment && typeof body.payload.shipment === 'object'
? body.payload.shipment
@@ -189,24 +240,69 @@ function getShipmentPayload(body) {
}
function getShipmentBrandName(body) {
- return normalizeText(body?.payload?.shipment?.bags?.[0]?.brand?.brand_name);
+ const shipment = getShipmentPayload(body);
+ return firstNonEmptyText(
+ shipment?.bags?.[0]?.brand?.brand_name,
+ shipment?.bags?.[0]?.item?.attributes?.brand_name,
+ shipment?.affiliate_details?.company_affiliate_tag
+ );
}
function getShipmentEventKey(body) {
- return normalizeText(body?.payload?.shipment?.status);
+ const shipment = getShipmentPayload(body);
+ return firstNonEmptyText(
+ shipment?.status,
+ shipment?.shipment_status?.status,
+ shipment?.shipment_status?.current_shipment_status
+ );
}
function getShipmentToNumber(body) {
- return normalizeText(body?.payload?.shipment?.user?.mobile);
+ const shipment = getShipmentPayload(body);
+ return firstNonEmptyText(
+ shipment?.user?.mobile,
+ shipment?.delivery_address?.phone,
+ shipment?.billing_address?.phone
+ );
}
const DLT_PLACEHOLDER_REGEX = /\{#(?:var|numeric|url|cbn)#\}/g;
function normalizeRenderableValue(value) {
- if (typeof value === 'string') return value.replace(/\s+/g, ' ').trim();
- if (typeof value === 'number' && Number.isFinite(value)) return String(value);
- if (typeof value === 'boolean') return value ? 'true' : 'false';
- return '';
+ 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 getEventLookupCandidates(eventSlug) {
+ const normalizedEventSlug = slugify(eventSlug || '');
+ const candidates = [
+ normalizedEventSlug,
+ ...(EVENT_TEMPLATE_FALLBACKS[normalizedEventSlug] || []),
+ ];
+
+ return [...new Set(candidates.filter(Boolean))];
+}
+
+async function resolveWhitelistedTemplate(folder, eventSlug) {
+ for (const candidate of getEventLookupCandidates(eventSlug)) {
+ const template = await fetchJSON(folder, candidate);
+ if (template && template.status === 'whitelisted' && normalizeText(template.selectedTemplate)) {
+ return { template, matchedSlug: candidate };
+ }
+ }
+
+ return { template: null, matchedSlug: '' };
}
function toCamelCase(text) {
@@ -255,51 +351,97 @@ function indexShipmentValues(value, pathParts = [], valueIndex = new Map()) {
function buildShipmentValueIndex(shipment) {
const valueIndex = indexShipmentValues(shipment);
const firstBag = shipment?.bags?.[0] || {};
- const primaryTrackingUrl = normalizeRenderableValue(
- shipment?.delivery_partner_details?.track_url
- || shipment?.meta?.tracking_url
- || firstBag?.meta?.tracking_url
- || shipment?.affiliate_details?.shipment_meta?.tracking_url
- || shipment?.article_details?.dp_details?.track_url
+ 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 primaryAwbNumber = normalizeRenderableValue(
- shipment?.delivery_partner_details?.awb_no
- || shipment?.meta?.awb_number
- || shipment?.article_details?.dp_details?.awb_no
+ const primaryTrackingUrl = firstNonEmptyText(
+ shipment?.delivery_partner_details?.track_url,
+ shipment?.meta?.tracking_url,
+ firstBag?.meta?.tracking_url,
+ shipment?.affiliate_details?.shipment_meta?.tracking_url,
+ shipment?.article_details?.dp_details?.track_url
);
- const primaryCourierName = normalizeRenderableValue(
- shipment?.delivery_partner_details?.display_name
- || shipment?.delivery_partner_details?.name
- || shipment?.meta?.courier_partner_name
- || shipment?.meta?.dp_name
- || firstBag?.meta?.dp_name
+ const primaryAwbNumber = firstNonEmptyText(
+ shipment?.delivery_partner_details?.awb_no,
+ shipment?.meta?.awb_number,
+ shipment?.article_details?.dp_details?.awb_no,
+ firstBag?.meta?.awb_number,
+ firstBag?.current_operational_status?.delivery_awb_number
);
- const brandName = normalizeRenderableValue(
- shipment?.bags?.[0]?.brand?.brand_name
- || shipment?.bags?.[0]?.item?.attributes?.brand_name
- || shipment?.affiliate_details?.company_affiliate_tag
+ const primaryCourierName = firstNonEmptyText(
+ shipment?.delivery_partner_details?.display_name,
+ shipment?.delivery_partner_details?.name,
+ shipment?.meta?.courier_partner_name,
+ shipment?.meta?.dp_name,
+ firstBag?.meta?.dp_name,
+ shipment?.affiliate_details?.shipment_meta?.courier_partner_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 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', normalizeRenderableValue(shipment?.user?.first_name));
- setValueIndexEntry(valueIndex, 'lastName', normalizeRenderableValue(shipment?.user?.last_name));
- setValueIndexEntry(valueIndex, 'fullName', normalizeRenderableValue(
- `${normalizeRenderableValue(shipment?.user?.first_name)} ${normalizeRenderableValue(shipment?.user?.last_name)}`.trim()
- ));
- setValueIndexEntry(valueIndex, 'phone', normalizeRenderableValue(shipment?.user?.mobile || shipment?.delivery_address?.phone));
- setValueIndexEntry(valueIndex, 'mobile', normalizeRenderableValue(shipment?.user?.mobile || shipment?.delivery_address?.phone));
- setValueIndexEntry(valueIndex, 'toNumber', normalizeRenderableValue(shipment?.user?.mobile || shipment?.delivery_address?.phone));
+ 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, 'orderId', normalizeRenderableValue(shipment?.order_id));
- setValueIndexEntry(valueIndex, 'shipmentId', normalizeRenderableValue(shipment?.shipment_id));
- setValueIndexEntry(valueIndex, 'event', normalizeRenderableValue(shipment?.status));
- setValueIndexEntry(valueIndex, 'status', normalizeRenderableValue(shipment?.status));
- setValueIndexEntry(valueIndex, 'eventDisplayName', normalizeRenderableValue(shipment?.shipment_status?.display_name));
- setValueIndexEntry(valueIndex, 'displayName', normalizeRenderableValue(shipment?.shipment_status?.display_name));
+ 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);
@@ -729,9 +871,42 @@ router.post('/resolve-template', async (req, res) => {
if (!companyId) return res.status(400).json({ error: 'companyId is required' });
if (!shipment) return res.status(400).json({ error: 'payload.shipment is required' });
- if (!brandName) return res.status(400).json({ error: 'payload.shipment.bags[0].brand.brand_name is required' });
- if (!event) return res.status(400).json({ error: 'payload.shipment.status is required' });
- if (!toNumber) return res.status(400).json({ error: 'payload.shipment.user.mobile is required' });
+ if (!brandName) {
+ return res.status(400).json({
+ error: 'A shipment brand name is required',
+ details: {
+ acceptedPaths: [
+ 'payload.shipment.bags[0].brand.brand_name',
+ 'payload.shipment.bags[0].item.attributes.brand_name',
+ 'payload.shipment.affiliate_details.company_affiliate_tag',
+ ],
+ },
+ });
+ }
+ if (!event) {
+ return res.status(400).json({
+ error: 'A shipment event status is required',
+ details: {
+ acceptedPaths: [
+ 'payload.shipment.status',
+ 'payload.shipment.shipment_status.status',
+ 'payload.shipment.shipment_status.current_shipment_status',
+ ],
+ },
+ });
+ }
+ if (!toNumber) {
+ return res.status(400).json({
+ error: 'A shipment phone number is required',
+ details: {
+ acceptedPaths: [
+ 'payload.shipment.user.mobile',
+ 'payload.shipment.delivery_address.phone',
+ 'payload.shipment.billing_address.phone',
+ ],
+ },
+ });
+ }
const business = await findBusinessByBrandName(companyId, brandName);
if (!business) {
@@ -740,7 +915,7 @@ router.post('/resolve-template', async (req, res) => {
const eventSlug = slugify(event);
const folder = `${businessRoot(companyId, business.businessId)}/templates`;
- const tmpl = await fetchJSON(folder, eventSlug);
+ const { template: tmpl, matchedSlug } = await resolveWhitelistedTemplate(folder, eventSlug);
if (!tmpl || tmpl.status !== 'whitelisted' || !normalizeText(tmpl.selectedTemplate)) {
return res.status(404).json({ error: 'Whitelisted template not found' });
@@ -763,6 +938,7 @@ router.post('/resolve-template', async (req, res) => {
businessId: business.businessId,
brandName,
event: eventSlug,
+ matchedTemplateEvent: matchedSlug || eventSlug,
templateId: normalizeText(tmpl.templateId),
template: tmpl.selectedTemplate,
content: resolvedTemplate,
@@ -1030,7 +1206,7 @@ router.get('/:businessId/global-sms/active', async (req, res) => {
router.get('/:businessId/events', async (req, res) => {
try {
const data = await fetchJSON(businessRoot(getCompanyId(req), req.params.businessId), 'events');
- res.json(data || { events: DEFAULT_EVENTS });
+ res.json(mergeDefaultEvents(data || {}));
} catch (err) {
res.status(500).json({ error: err.message });
}
@@ -1043,7 +1219,7 @@ router.post('/:businessId/events', async (req, res) => {
if (!label) return res.status(400).json({ error: 'label is required' });
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
- const data = await fetchJSON(bizRoot, 'events') || { events: [...DEFAULT_EVENTS] };
+ const data = mergeDefaultEvents(await fetchJSON(bizRoot, 'events') || {});
const slug = slugify(label);
if (data.events.some(e => e.slug === slug)) {
@@ -1064,7 +1240,7 @@ router.delete('/:businessId/events/:slug', async (req, res) => {
try {
const { businessId, slug } = req.params;
const bizRoot = businessRoot(getCompanyId(req), businessId);
- const data = await fetchJSON(bizRoot, 'events') || { events: [...DEFAULT_EVENTS] };
+ const data = mergeDefaultEvents(await fetchJSON(bizRoot, 'events') || {});
const event = data.events.find(e => e.slug === slug);
if (!event) return res.status(404).json({ error: 'Event not found' });
@@ -1092,7 +1268,7 @@ router.post('/:businessId/events/:slug/generate', async (req, res) => {
return res.status(400).json({ error: 'A cURL profile must be configured and active before generating templates.' });
}
- const eventsData = await fetchJSON(bizRoot, 'events') || { events: DEFAULT_EVENTS };
+ const eventsData = mergeDefaultEvents(await fetchJSON(bizRoot, 'events') || {});
const event = eventsData.events.find(e => e.slug === slug);
if (!event) return res.status(404).json({ error: 'Event not found' });