diff --git a/components/department-dashboard/performance-conversion.test.ts b/components/department-dashboard/performance-conversion.test.ts
index e3b54d9..95aa98d 100644
--- a/components/department-dashboard/performance-conversion.test.ts
+++ b/components/department-dashboard/performance-conversion.test.ts
@@ -1,6 +1,8 @@
import {
BUSINESS_TREND_BRANDS,
addCalendarMonths,
+ buildIntentRateWindowSummary,
+ calculateIntentRate,
daysBetweenInclusive,
extractValidProductIds,
isValidDateKey,
@@ -35,4 +37,64 @@ describe('performance conversion helpers', () => {
expect(isValidDateKey('2026-02-29')).toBe(false)
expect(isValidDateKey('2026-9-17')).toBe(false)
})
+
+ it('calculates intent rate from daily interest and conversion metrics', () => {
+ expect(calculateIntentRate({ favorites: 10, cartUsers: 5, visitors: 100, paymentConversionRate: 0.03 })).toBe(0.18)
+ expect(calculateIntentRate({ favorites: 10, cartUsers: 5, visitors: 0, paymentConversionRate: 0.03 })).toBeNull()
+ expect(calculateIntentRate({ favorites: 10, cartUsers: null, visitors: 100, paymentConversionRate: 0.03 })).toBeNull()
+ })
+
+ it('calculates complete 7-day and 15-day averages with intent-rate change', () => {
+ const daily = Array.from({ length: 15 }, (_, index) => ({
+ date: `2026-07-${String(index + 1).padStart(2, '0')}`,
+ impressions: 1000,
+ visitors: 100,
+ clickRate: 0.1,
+ favorites: index < 8 ? 10 : 20,
+ cartUsers: 10,
+ paymentConversionRate: 0.03,
+ paymentAmount: 100,
+ roi: 1,
+ }))
+
+ const summary = buildIntentRateWindowSummary(daily)
+ expect(summary.sevenDayIntentRate).toBeCloseTo(0.23, 12)
+ expect(summary.fifteenDayIntentRate).toBeCloseTo(0.27666666666666667, 12)
+ expect(summary.intentRateChange).toBeCloseTo(0.20289855072463767, 12)
+ })
+
+ it('returns unavailable values when a window is incomplete or its 7-day rate is zero', () => {
+ const fiveDays = Array.from({ length: 5 }, (_, index) => ({
+ date: `2026-07-${String(index + 1).padStart(2, '0')}`,
+ impressions: 1000,
+ visitors: 100,
+ clickRate: 0.1,
+ favorites: 10,
+ cartUsers: 10,
+ paymentConversionRate: 0.03,
+ paymentAmount: 100,
+ roi: 1,
+ }))
+ expect(buildIntentRateWindowSummary(fiveDays)).toEqual({
+ sevenDayIntentRate: null,
+ fifteenDayIntentRate: null,
+ intentRateChange: null,
+ })
+
+ const zeroSevenDayRate = Array.from({ length: 15 }, (_, index) => ({
+ date: `2026-07-${String(index + 1).padStart(2, '0')}`,
+ impressions: 1000,
+ visitors: 100,
+ clickRate: 0.1,
+ favorites: index < 7 ? 0 : 10,
+ cartUsers: index < 7 ? 0 : 10,
+ paymentConversionRate: index < 7 ? 0 : 0.03,
+ paymentAmount: 100,
+ roi: 1,
+ }))
+ expect(buildIntentRateWindowSummary(zeroSevenDayRate)).toMatchObject({
+ sevenDayIntentRate: 0,
+ intentRateChange: null,
+ })
+ })
})
diff --git a/components/department-dashboard/performance-conversion.ts b/components/department-dashboard/performance-conversion.ts
index fb25f60..94abe51 100644
--- a/components/department-dashboard/performance-conversion.ts
+++ b/components/department-dashboard/performance-conversion.ts
@@ -66,6 +66,12 @@ export type PerformanceConversionDailyMetric = {
roi: number | null
}
+export type PerformanceConversionWindowSummary = {
+ sevenDayIntentRate: number | null
+ fifteenDayIntentRate: number | null
+ intentRateChange: number | null
+}
+
export type PerformanceConversionTimeseries = {
workflow: PerformanceConversionWorkflowItem
product: {
@@ -87,6 +93,36 @@ export type PerformanceConversionTimeseries = {
}
}
+export function calculateIntentRate(day: Pick
): number | null {
+ const { favorites, cartUsers, visitors, paymentConversionRate } = day
+ if (favorites === null || cartUsers === null || visitors === null || paymentConversionRate === null
+ || ![favorites, cartUsers, visitors, paymentConversionRate].every((value) => Number.isFinite(value))) {
+ return null
+ }
+ if (visitors <= 0) return null
+ return (favorites + cartUsers) / visitors + paymentConversionRate
+}
+
+function averageIntentRate(daily: PerformanceConversionDailyMetric[], windowSize: number) {
+ const window = daily.slice(0, windowSize)
+ if (window.length !== windowSize) return null
+ const values = window.map(calculateIntentRate)
+ if (values.some((value) => value === null)) return null
+ return values.reduce((total, value) => total + (value ?? 0), 0) / windowSize
+}
+
+export function buildIntentRateWindowSummary(daily: PerformanceConversionDailyMetric[]): PerformanceConversionWindowSummary {
+ const sevenDayIntentRate = averageIntentRate(daily, 7)
+ const fifteenDayIntentRate = averageIntentRate(daily, 15)
+ const intentRateChange = sevenDayIntentRate !== null
+ && sevenDayIntentRate !== 0
+ && fifteenDayIntentRate !== null
+ ? (fifteenDayIntentRate - sevenDayIntentRate) / sevenDayIntentRate
+ : null
+
+ return { sevenDayIntentRate, fifteenDayIntentRate, intentRateChange }
+}
+
export function extractValidProductIds(...values: unknown[]): string[] {
const candidates = values.flatMap((value) => String(value || '')
.split(/[\s,,;;/、|]+/)