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