171 lines
6.2 KiB
TypeScript
171 lines
6.2 KiB
TypeScript
import {
|
||
BUSINESS_TREND_BRANDS,
|
||
addCalendarMonths,
|
||
buildIntentRateWindowSummary,
|
||
calculateIntentRate,
|
||
daysBetweenInclusive,
|
||
extractValidProductIds,
|
||
isValidDateKey,
|
||
toShanghaiDateKey,
|
||
type PerformanceConversionDailyMetric,
|
||
} from './performance-conversion'
|
||
|
||
describe('performance conversion helpers', () => {
|
||
it('keeps the requested six brands in the agreed order', () => {
|
||
expect(BUSINESS_TREND_BRANDS).toEqual(['亿家达', '卓禾', '欧意朗', '顾家', '索菲亚', '朴美'])
|
||
})
|
||
|
||
it('extracts and de-duplicates numeric product ids while rejecting placeholders', () => {
|
||
expect(extractValidProductIds(
|
||
'1077434823611 / 1077434823611',
|
||
'970584740986,新品暂无;0',
|
||
null,
|
||
)).toEqual(['1077434823611', '970584740986'])
|
||
})
|
||
|
||
it('adds six calendar months with month-end clamping', () => {
|
||
expect(addCalendarMonths('2026-03-31', 6)).toBe('2026-09-30')
|
||
expect(addCalendarMonths('2024-08-31', 6)).toBe('2025-02-28')
|
||
})
|
||
|
||
it('uses Shanghai dates and inclusive coverage days', () => {
|
||
expect(toShanghaiDateKey('2026-03-31T16:00:00.000Z')).toBe('2026-04-01')
|
||
expect(daysBetweenInclusive('2026-04-01', '2026-04-03')).toBe(3)
|
||
})
|
||
|
||
it('validates real calendar date keys', () => {
|
||
expect(isValidDateKey('2026-09-17')).toBe(true)
|
||
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)
|
||
})
|
||
|
||
function dailyMetrics(length: number): PerformanceConversionDailyMetric[] {
|
||
return Array.from({ length }, (_, index) => ({
|
||
date: `2026-10-${String(index + 7).padStart(2, '0')}`,
|
||
impressions: null,
|
||
visitors: 100,
|
||
clickRate: null,
|
||
favorites: 10,
|
||
cartUsers: 10,
|
||
paymentConversionRate: 0.03,
|
||
paymentAmount: null,
|
||
roi: null,
|
||
}))
|
||
}
|
||
|
||
it('counts missing dates as zero and divides by the full calendar window', () => {
|
||
const daily = dailyMetrics(15)
|
||
// The October 7 completion has gaps on October 10, 17 and 18.
|
||
for (const index of [3, 10, 11]) {
|
||
daily[index] = { ...daily[index], favorites: null, cartUsers: null, visitors: null, paymentConversionRate: null }
|
||
}
|
||
|
||
const summary = buildIntentRateWindowSummary(daily)
|
||
const sevenDayAverage = 0.23 * 6 / 7
|
||
const fifteenDayAverage = 0.23 * 12 / 15
|
||
expect(summary.sevenDayIntentRate).toBeCloseTo(sevenDayAverage, 12)
|
||
expect(summary.fifteenDayIntentRate).toBeCloseTo(fifteenDayAverage, 12)
|
||
expect(summary.intentRateChange).toBeCloseTo((fifteenDayAverage - sevenDayAverage) / sevenDayAverage, 12)
|
||
expect(daily[3].visitors).toBeNull()
|
||
expect(daily[10].favorites).toBeNull()
|
||
})
|
||
|
||
it('counts an unavailable daily rate as zero when fields are missing or visitors are zero', () => {
|
||
const daily = dailyMetrics(7)
|
||
daily[2].favorites = null
|
||
daily[4].visitors = 0
|
||
|
||
expect(buildIntentRateWindowSummary(daily)).toEqual({
|
||
sevenDayIntentRate: expect.closeTo(0.23 * 5 / 7, 12),
|
||
fifteenDayIntentRate: null,
|
||
intentRateChange: null,
|
||
})
|
||
})
|
||
|
||
it('keeps unavailable windows until 7 or 15 calendar days have elapsed', () => {
|
||
expect(buildIntentRateWindowSummary(dailyMetrics(6)).sevenDayIntentRate).toBeNull()
|
||
for (const length of [7, 14]) {
|
||
const summary = buildIntentRateWindowSummary(dailyMetrics(length))
|
||
expect(summary.sevenDayIntentRate).toBeCloseTo(0.23, 12)
|
||
expect(summary.fifteenDayIntentRate).toBeNull()
|
||
expect(summary.intentRateChange).toBeNull()
|
||
}
|
||
expect(buildIntentRateWindowSummary([])).toEqual({
|
||
sevenDayIntentRate: null,
|
||
fifteenDayIntentRate: null,
|
||
intentRateChange: null,
|
||
})
|
||
})
|
||
|
||
it('returns zero averages but no change when all elapsed dates are missing', () => {
|
||
const daily = dailyMetrics(15).map((day) => ({
|
||
...day, favorites: null, cartUsers: null, visitors: null, paymentConversionRate: null,
|
||
}))
|
||
expect(buildIntentRateWindowSummary(daily)).toEqual({
|
||
sevenDayIntentRate: 0,
|
||
fifteenDayIntentRate: 0,
|
||
intentRateChange: null,
|
||
})
|
||
})
|
||
|
||
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,
|
||
})
|
||
})
|
||
})
|