Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 88 additions & 3 deletions src/controllers/llmo/llmo-brand-presence.js
Original file line number Diff line number Diff line change
Expand Up @@ -558,18 +558,26 @@ export function createBrandPresenceWeeksHandler(getOrgAndValidateAccess) {

function parseMarketTrackingTrendsParams(context) {
const q = context.data || {};
const rawNames = q.competitorNames || q.competitor_names;
let competitorNames = null;
if (rawNames) {
competitorNames = Array.isArray(rawNames) ? rawNames : String(rawNames).split(',').map((s) => s.trim()).filter(Boolean);
}
return {
startDate: q.startDate || q.start_date,
endDate: q.endDate || q.end_date,
model: q.model,
siteId: q.siteId || q.site_id,
categoryId: q.categoryId || q.category_id,
regionCode: q.regionCode || q.region_code || q.region,
competitorNames,
};
}

// eslint-disable-next-line max-len
async function callMarketTrackingTrendsRpc(client, organizationId, params, defaults, filterByBrandId, log) {
const competitorNames = params.competitorNames || null;
const rpcName = competitorNames ? 'rpc_market_tracking_filtered' : 'rpc_market_tracking_trends';
const rpcParams = {
p_organization_id: organizationId,
p_start_date: params.startDate || defaults.startDate,
Expand All @@ -582,12 +590,36 @@ async function callMarketTrackingTrendsRpc(client, organizationId, params, defau
p_category_name: shouldApplyFilter(params.categoryId) && !isValidUUID(params.categoryId)
? params.categoryId : null,
p_region_code: shouldApplyFilter(params.regionCode) ? params.regionCode : null,
...(competitorNames && { p_competitor_names: competitorNames }),
};
log.info(`RPC rpc_market_tracking_trends called with: ${JSON.stringify(rpcParams)}`);
log.info(`RPC ${rpcName} called with: ${JSON.stringify(rpcParams)}`);
const start = performance.now();
const result = await client.rpc('rpc_market_tracking_trends', rpcParams);
const result = await client.rpc(rpcName, rpcParams);
const elapsed = (performance.now() - start).toFixed(0);
log.info(`RPC rpc_market_tracking_trends completed in ${elapsed}ms`);
log.info(`RPC ${rpcName} completed in ${elapsed}ms`);
return result;
}

// eslint-disable-next-line max-len
async function callCompetitorSummaryRpc(client, organizationId, params, defaults, filterByBrandId, log) {
const rpcParams = {
p_organization_id: organizationId,
p_start_date: params.startDate || defaults.startDate,
p_end_date: params.endDate || defaults.endDate,
p_model: resolveModelFromRequest(params.model),
p_brand_id: filterByBrandId || null,
p_site_id: shouldApplyFilter(params.siteId) ? params.siteId : null,
p_category_id: shouldApplyFilter(params.categoryId) && isValidUUID(params.categoryId)
? params.categoryId : null,
p_category_name: shouldApplyFilter(params.categoryId) && !isValidUUID(params.categoryId)
? params.categoryId : null,
p_region_code: shouldApplyFilter(params.regionCode) ? params.regionCode : null,
};
log.info(`RPC rpc_market_tracking_competitor_summary called with: ${JSON.stringify(rpcParams)}`);
const start = performance.now();
const result = await client.rpc('rpc_market_tracking_competitor_summary', rpcParams);
const elapsed = (performance.now() - start).toFixed(0);
log.info(`RPC rpc_market_tracking_competitor_summary completed in ${elapsed}ms`);
return result;
}

Expand Down Expand Up @@ -677,6 +709,59 @@ export function createMarketTrackingTrendsHandler(getOrgAndValidateAccess) {
);
}

/**
* Creates the getCompetitorSummary handler.
* Returns aggregate competitor totals (no weekly breakdown) for the competitor picker.
* @param {Function} getOrgAndValidateAccess - Async (context) => { organization }
*/
export function createCompetitorSummaryHandler(getOrgAndValidateAccess) {
return (context) => withBrandPresenceAuth(
context,
getOrgAndValidateAccess,
'competitor-summary',
async (ctx, client) => {
const { spaceCatId, brandId } = ctx.params;
const params = parseMarketTrackingTrendsParams(ctx);
const defaults = defaultDateRange();
const organizationId = spaceCatId;
const filterByBrandId = brandId && brandId !== 'all' ? brandId : null;

if (shouldApplyFilter(params.siteId)) {
const siteBelongsToOrg = await validateSiteBelongsToOrg(
client,
organizationId,
params.siteId,
);
if (!siteBelongsToOrg) {
return forbidden('Site does not belong to the organization');
}
}

const { data, error } = await callCompetitorSummaryRpc(
client,
organizationId,
params,
defaults,
filterByBrandId,
ctx.log,
);

if (error) {
ctx.log.error(`Competitor-summary RPC error: ${error.message}`);
return badRequest(error.message);
}

return ok({
competitors: (data || []).map((r) => ({
name: r.competitor_name,
mentions: r.total_mentions || 0,
citations: r.total_citations || 0,
})),
});
},
);
}

/**
* Converts a YYYY-MM-DD date string to an ISO week object.
* @param {string} dateStr - e.g. "2026-03-11"
Expand Down
4 changes: 3 additions & 1 deletion src/controllers/llmo/llmo-mysticat-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import AccessControlUtil from '../../support/access-control-util.js';
import {
createFilterDimensionsHandler,
createBrandPresenceWeeksHandler, createSentimentOverviewHandler,
createMarketTrackingTrendsHandler, createTopicsHandler,
createMarketTrackingTrendsHandler, createCompetitorSummaryHandler, createTopicsHandler,
createTopicPromptsHandler,
createSearchHandler,
createTopicDetailHandler,
Expand Down Expand Up @@ -84,6 +84,7 @@ function LlmoMysticatController(ctx) {
const getFilterDimensions = createFilterDimensionsHandler(getOrgAndValidateAccess);
const getBrandPresenceWeeks = createBrandPresenceWeeksHandler(getOrgAndValidateAccess);
const getMarketTrackingTrends = createMarketTrackingTrendsHandler(getOrgAndValidateAccess);
const getCompetitorSummary = createCompetitorSummaryHandler(getOrgAndValidateAccess);
const getSentimentOverview = createSentimentOverviewHandler(getOrgAndValidateAccess);
const getTopics = createTopicsHandler(getOrgAndValidateAccess);
const getTopicPrompts = createTopicPromptsHandler(getOrgAndValidateAccess);
Expand All @@ -102,6 +103,7 @@ function LlmoMysticatController(ctx) {
getFilterDimensions,
getBrandPresenceWeeks,
getMarketTrackingTrends,
getCompetitorSummary,
getSentimentOverview,
getTopics,
getTopicPrompts,
Expand Down
2 changes: 2 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ export default function getRouteHandlers(
'GET /org/:spaceCatId/brands/:brandId/brand-presence/sentiment-overview': llmoMysticatController.getSentimentOverview,
'GET /org/:spaceCatId/brands/all/brand-presence/market-tracking-trends': llmoMysticatController.getMarketTrackingTrends,
'GET /org/:spaceCatId/brands/:brandId/brand-presence/market-tracking-trends': llmoMysticatController.getMarketTrackingTrends,
'GET /org/:spaceCatId/brands/all/brand-presence/competitor-summary': llmoMysticatController.getCompetitorSummary,
'GET /org/:spaceCatId/brands/:brandId/brand-presence/competitor-summary': llmoMysticatController.getCompetitorSummary,
'GET /org/:spaceCatId/brands/all/brand-presence/topics': llmoMysticatController.getTopics,
'GET /org/:spaceCatId/brands/:brandId/brand-presence/topics': llmoMysticatController.getTopics,
'GET /org/:spaceCatId/brands/all/brand-presence/topics/:topicId/prompts': llmoMysticatController.getTopicPrompts,
Expand Down
2 changes: 2 additions & 0 deletions src/routes/required-capabilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ const routeRequiredCapabilities = {
'GET /org/:spaceCatId/brands/:brandId/brand-presence/sentiment-overview': 'brand:read',
'GET /org/:spaceCatId/brands/all/brand-presence/market-tracking-trends': 'brand:read',
'GET /org/:spaceCatId/brands/:brandId/brand-presence/market-tracking-trends': 'brand:read',
'GET /org/:spaceCatId/brands/all/brand-presence/competitor-summary': 'brand:read',
'GET /org/:spaceCatId/brands/:brandId/brand-presence/competitor-summary': 'brand:read',
'GET /org/:spaceCatId/brands/all/brand-presence/topics': 'brand:read',
'GET /org/:spaceCatId/brands/:brandId/brand-presence/topics': 'brand:read',
'GET /org/:spaceCatId/brands/all/brand-presence/topics/:topicId/prompts': 'brand:read',
Expand Down
Loading
Loading