Skip to content
Share
Explore

Developer Handoff Prompts — UTM Banner Source Exclusion

Two prompts, one per repo this feature touches:

Backend (nstore-ondc-buyer) — teach the banner query to evaluate UTM exclude mode. CMS Dashboard (ondc-buyer-dashboard) — turn the UTM field into a None/Include/Exclude control and persist the mode. No frontend (mall4all) prompt — the buyer app already sends utm_source to the banner endpoint; include vs exclude is decided entirely server-side, so there is zero frontend change.
Shared Contract — the utm_source scope entry Both repos agree on one new field. A banner/slot’s scope array carries at most one utm_source entry:
None → no utm_source entry in scope at all Include → { type: “utm_source”, mode: “include”, values: [“YoutubeAds”, “Meta”], sequence_number: N } Exclude → { type: “utm_source”, mode: “exclude”, values: [“BFLApp”], sequence_number: N }
New field is mode, value “include” or “exclude”, living on the utm_source scope entry — NOT a banner-level field. It must stay independent of the banner-wide visibilityMode that governs city/pincode. Back-compat: banners created before this feature have a utm_source entry with no mode. A missing mode means “include” everywhere — existing inclusion banners must not change behaviour. Values are stored and compared case-insensitively (unchanged from inclusion). One mode at a time is structural: a banner has a single utm_source entry, so it is either include OR exclude OR absent — never both.

Backend Prompt

Repo: nstore-ondc-buyer (Koa 3 / Node 20 / MongoDB native driver, aggregation pipelines). Baseline: nstore-ondc-buyer @ eba5c45. Re-verify file paths if your checkout differs.
If the PRD is attached, read it; otherwise this prompt is self-sufficient. Propose a plan and wait for approval, then implement fully and self-verify against DONE WHEN.
WHAT WE’RE BUILDING Banners can already be included by utm_source (show only to listed sources). We’re adding the inverse: exclude — show to everyone except listed sources. A banner like “Visit the BFL app” must be hidden from users who arrived with utm_source=BFLApp, and shown to everyone else — including users with no/unknown UTM. This is a pure read-query change: make the banner-matching aggregation honour an exclude mode on the utm_source scope entry. No writes, no new endpoint. DECISIONS (resolved — do not re-litigate) The mode lives on the utm_source scope entry as mode: “include” | “exclude” (see Shared Contract). It is independent of the banner-level visibilityMode field — do not route UTM through visibilityMode. Missing mode means include (back-compat for existing inclusion banners). A user with no/expired utm_source (the param is absent) is not excluded — exclude-mode banners still render for them. UNKNOWN is never an excluded source. Scope is routes/partner.route.js only. routes/cms.route.js does not receive utm_source and is out of scope — do not touch it. HOW THE CODEBASE WORKS (the slice that matters) GET /partners/cms/v2 (routes/partner.route.js) builds a Mongo aggregation that $lookups banners from cms_bannersV2 and filters them by the request’s city, area_code, device, and utm_source. City and pincode filtering uses the inline scopeFilter(type, values) helper, which already branches on $visibilityMode (inclusive vs exclusive). That is the city/pincode mechanism — leave it alone. utm_source is filtered by a separate, hand-rolled $or block inside the $lookup pipeline’s $expr.$and — the block whose conditions reference $$s.type == ‘utm_source’ and utm_source.toLowerCase(). Today this block is inclusion-only; it has no exclude branch. This block is the entire change. The request param is utm_source (a string, optional). utmValues and utm_source are already in scope where the block is built. Pattern to follow for the exclude branch: the else arm of scopeFilter’s $cond — { $not: { $in: [lowerVal, lowerScopeValues] } }. Mirror that style. WHAT TO BUILD Rewrite the utm_source $or block so the banner passes the UTM gate when any of these hold:
No active UTM filter — there is no utm_source scope entry with a non-empty values array. Keep the existing Case-A subexpression exactly as-is. Include pass — a utm_source entry exists with non-empty values, its mode is include (mode == “include” or mode is missing), AND the request utm_source is present and matches a value (case-insensitive). This is today’s behaviour plus a guard that it’s not an exclude entry. Exclude pass — a utm_source entry exists with non-empty values, its mode == “exclude”, AND the user is not excluded: either the request utm_source is absent, OR it does not match any value (case-insensitive). For Case 2, use $ifNull on the mode field with a default of “include”, then confirm it is not “exclude” before checking the values match. For Case 3, use $ifNull on the mode field with a default of “include”, confirm it equals “exclude”, then confirm the request utm_source is either absent or not in the values list — use $not $in with $toLower on each stored value.
VERIFY: confirm the exact variable name for the request param at that point in the file (utm_source vs rest.utm_source) and reuse it verbatim — the absent-param branch depends on it being falsy when unset.
Caching: results are keyed by createCacheKey which already includes utm_source. No cache-key change needed — but confirm utm_source is part of the key so include and exclude results don’t collide.
DO NOT TOUCH scopeFilter() and the city/pincode visibilityMode logic — UTM must not flow through visibilityMode. routes/cms.route.js (/cms/banners, /cms/bannersV2) — no UTM there by design. Coupon UTM logic (routes/offer.route.js, routes/wallet.route.js) — out of scope. The 24h TTL / UTM persistence — that’s frontend, untouched. EDGE CASES & FAILURE MODES Exclude + user matches → banner hidden (no placeholder; just absent from the banners array). Exclude + user does NOT match → banner shown. Exclude + no/expired utm_source → banner shown (UNKNOWN not excluded). Include + no utm_source → banner hidden (unchanged). Legacy entry, no mode → behaves as include (unchanged). Empty values with a mode set → treated as no active filter (Case 1) → banner shown. CMS prevents saving this, but the query must not crash on it. Mixed banner (utm exclude + city include on same banner) → each dimension evaluated independently; banner shows only if it passes both gates. FILES YOU’LL LIKELY TOUCH routes/partner.route.js — the single utm_source $or block inside the /cms/v2 $lookup pipeline. One file, one block. Anything beyond this is a review flag. DATA / MIGRATION No migration. Existing utm_source entries have no mode; the $ifNull defaults handle them. No backfill.
HOW TO VERIFY Run: npm run dev (node index.js). No jest coverage exists for this pipeline — verify by query. Seed or locate a partner with a cms_bannersV2 banner whose scope has { type: ‘utm_source’, mode: ‘exclude’, values: [‘bflapp’] } mapped onto a page (e.g. homePage). Hit the endpoint three ways and confirm the banner’s presence in cms[].banners: GET /partners/cms/v2?id=PARTNER&page=homePage&utm_source=BFLApp → banner absent. GET /partners/cms/v2?id=PARTNER&page=homePage&utm_source=YoutubeAds → banner present. GET /partners/cms/v2?id=PARTNER&page=homePage (no utm) → banner present. Regression: an existing mode-less include banner still hides for non-matching/no-utm users and shows for matching users. Pass no_cache=true between checks to bypass NodeCache. DONE WHEN [ ] Exclude-mode banner does NOT render when request utm_source matches a value (case-insensitive). [ ] Exclude-mode banner renders when utm_source does not match. [ ] Exclude-mode banner renders when utm_source is absent/unknown. [ ] Include-mode and legacy (mode-less) banners behave exactly as before. [ ] Banner with no utm_source scope entry is unaffected. [ ] scopeFilter, city/pincode/visibilityMode, and cms.route.js are untouched.

CMS Dashboard Prompt

Repo: ondc-buyer-dashboard (Nuxt 3 / Vue 3 / NuxtUI — U* components). Baseline: ondc-buyer-dashboard @ a19b0cb. Re-verify file paths if your checkout differs.
If the PRD is attached, read it; otherwise this prompt is self-sufficient. Propose a plan and wait for approval, then implement fully and self-verify against DONE WHEN.
WHAT WE’RE BUILDING Today the banner form has a single free-text “UTM Source” input that always means include. Replace it with a three-state control: None / Include / Exclude. None (default) → no UTM filtering, no source input shown, no utm_source scope entry saved. Include → show the source input; banner shows only to listed sources (today’s behaviour). Exclude → show the source input; banner shows to everyone except listed sources. Persist the choice as mode on the utm_source scope entry (see Shared Contract). Add an info popover so operators understand Include vs Exclude. DECISIONS (resolved — do not re-litigate) Mode is stored on the utm_source scope entry as mode: “include” | “exclude”. None means do not push a utm_source entry at all (and on edit, ensure any prior entry is dropped). This is separate from the existing “Banner visibility mode” (Inclusive/Exclusive) radio that controls city/pincode — leave that control alone; do not reuse it for UTM. Edit-load of a legacy banner (a utm_source entry with values but no mode) → preselect Include. Apply to the four banner templates the ops team actually uses: pages/dashboard/category/[id].vue, pages/dashboard/v2/[id].vue, pages/dashboard/allCategoriesV2.vue, pages/dashboard/homePageV2.vue. VERIFY with the lead whether fintasticsHome.vue, demoPages/[id].vue, or EMIV2.vue also need it before widening. HOW THE CODEBASE WORKS (the slice that matters) Each template is large (~130KB+) and the banner form logic is duplicated twice: an edit-existing-slot path (state slotData, save fn handleSave, hydrate fn handleEdit) and an add-new-slot path (state newSlot, save fn handleAddSlot). Every change below must be made in BOTH paths in EACH template. The current UTM input is a UInput bound to utmSource (edit) and newSlot.utmSource (add). Values are comma-separated free text, split and whitespace-stripped on save. On save, the utm_source scope entry is pushed as { type: ‘utm_source’, values: utmSourceData.filter(x=>x!=‘’), sequence_number }. On edit, handleEdit reads it back via data.scope[i].type == ‘utm_source’ into utmSource.value. Patterns to imitate (both already in the file): The “Banner visibility mode” URadio group (URadio bound to slotData.visibilityMode with values ‘inclusive’ / ‘exclusive’) — copy this exact radio pattern for the new None/Include/Exclude control. The existing UTooltip usage (e.g. UTooltip with text “Add new slots”) — use the same for the info popover. WHAT TO BUILD (apply in BOTH the add and edit paths, in EACH of the 4 templates) State — add a UTM mode field defaulting to “none”: Edit path: a utmMode ref alongside the existing utmSource ref. Add path: newSlot.utmMode = “none” alongside newSlot.utmSource. Template — replace the bare UTM input with: A URadio group bound to the mode: None / Include / Exclude (values “none” / “include” / “exclude”). An info icon (ⓘ) beside the “UTM source” label using UTooltip with copy: Include — Banner shows only to users arriving from sources you list. Exclude — Banner shows to everyone except users arriving from sources you list. Use this to hide the banner from a specific channel. The existing source UInput, now shown only when mode is not “none”. An inline error shown when utmError is truthy: “Add at least one source or set UTM filter to None.” Switching behaviour — watch the mode: when it changes, clear the source value (utmSource / newSlot.utmSource = “”). Selecting None also hides the input. Validation on save (in handleSave and handleAddSlot) — if mode is “include” or “exclude” and the parsed source list is empty → set the inline error and return (block save). None saves with no validation. Build the scope entry on save: Mode “none” → do NOT push a utm_source entry. On the edit path, also ensure any existing utm_source entry is not carried over (the entry must be absent in the saved scope). Mode “include” or “exclude” → push { type: ‘utm_source’, mode: MODE, values: PARSED_NON_EMPTY_DEDUPED_LIST, sequence_number: as today }. Keep the existing split-and-filter logic that strips empties. Hydrate on edit (handleEdit) — find the utm_source scope entry: Present → set utmMode = entry.mode (default to “include” if missing), set utmSource.value = entry.values.toString(). Absent → utmMode = “none”, utmSource.value = “”. Reset paths — wherever utmSource is reset to “” today (cancel/close/reset), also reset the mode to “none”. DO NOT TOUCH The existing “Banner visibility mode” radio and its visibilityMode plumbing (city/pincode). City / pincode inputs and their scope entries. The backend — it reads mode off the entry; the CMS only has to write it. EDGE CASES & FAILURE MODES Switch Include to Exclude → previous source list cleared; operator re-enters sources for the new mode. Switch to None from Include/Exclude → input hidden, list cleared, no utm_source entry saved (banner becomes unrestricted on next publish). Include/Exclude selected, empty list, Save → blocked, inline error shown. Edit a legacy include banner (entry has values, no mode) → radio preselects Include; saving writes mode: “include” going forward. Edit a None banner (no entry) → radio shows None, input hidden. Duplicate / empty / whitespace sources → stripped before save (keep existing filter behaviour); no duplicates persisted. FILES YOU’LL LIKELY TOUCH pages/dashboard/category/[id].vue pages/dashboard/v2/[id].vue pages/dashboard/allCategoriesV2.vue pages/dashboard/homePageV2.vue Per file: template block ×2 for add/edit, plus handleSave, handleAddSlot, handleEdit, and the reset/cancel + reactive-state declarations. New files: none.
DATA / MIGRATION No migration. Legacy entries (no mode) load as Include and gain mode: “include” only when re-saved. None-state banners simply have no utm_source entry.
HOW TO VERIFY Run: npm run dev (nuxt dev). No automated coverage — verify in the running dashboard. For each of the 4 templates, in both Add slot and Edit slot: Default radio is None, no source input visible. Select Include → input appears; save with sources → reopen → Include and sources persisted. Select Exclude → input appears; save with sources → reopen → Exclude and sources persisted; inspect the saved banner’s scope has { type: ‘utm_source’, mode: ‘exclude’, values: […] }. Select Include or Exclude, leave empty, Save → blocked with inline error. Switch Include to Exclude → source list clears. Switch to None → input hides, list clears; save → banner has no utm_source entry. Open an existing legacy include banner → radio shows Include, sources intact. Cross-check with the backend: a banner saved as Exclude [‘BFLApp’] is hidden in the buyer app for utm_source=BFLApp and shown otherwise. DONE WHEN [ ] UTM control defaults to None on new banner creation; no source input shown. [ ] Three-state None/Include/Exclude radio; only one selectable. [ ] Selecting Include/Exclude reveals the source input; None hides it and clears the list. [ ] Info popover explains Include vs Exclude in plain language. [ ] Switching Include to/from Exclude clears the previous source list. [ ] Saving Include/Exclude with an empty list is blocked with the inline error. [ ] None always saves without error and persists no utm_source entry. [ ] Saved utm_source entry carries mode = include or exclude; values lowercase-insensitive, no dupes/empties. [ ] Edit-load preselects the correct mode (legacy → Include; absent → None). [ ] All four templates updated, both add and edit paths; “Banner visibility mode” radio untouched. Generated from PRD/utm-banner-exclusion.requirements.md (FINALIZED). Baselines: backend eba5c45, dashboard a19b0cb, frontend 4b3ac14 (no FE change). Zero open decisions.
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.