import type {ActivityEvent, TimePoint} from '@/features/dashboard/types/dashboard'; import type {Reward, RewardStatus} from '@/features/lyts/types/reward'; import type {RangeKey} from '@/shared/types/api'; import {createRng} from '@/shared/mock/rng'; import {buildTimeseries} from '@/features/dashboard/mock/dashboard.mock'; /** * The reward catalogue. * * `usageRate` is fixed per reward rather than random, because the story this * module has to tell is that redemption rates differ WILDLY by offer type — * Buy-1-Get-1 is claimed constantly and redeemed rarely, a free coffee is * claimed and used almost immediately. A catalogue where everything converts * at ~50% would make "most claimed" and "least used" the same reward and the * whole module pointless. * * 1 LYT = ₹1, so costLyt doubles as the rupee liability per redemption. */ const CATALOGUE: { id: string; name: string; costLyt: number; usageRate: number; status: RewardStatus; /** Days from now; null never expires. */ expiresInDays: number | null; }[] = [ {id: 'r-coffee', name: 'Free Coffee', costLyt: 120, usageRate: 0.72, status: 'active', expiresInDays: 34}, {id: 'r-combo20', name: 'Combo 20% off', costLyt: 200, usageRate: 0.34, status: 'expiring', expiresInDays: 2}, {id: 'r-weekend', name: 'Weekend Bonus', costLyt: 150, usageRate: 0.81, status: 'expiring', expiresInDays: 4}, {id: 'r-birthday', name: 'Birthday Treat', costLyt: 250, usageRate: 0.55, status: 'active', expiresInDays: null}, {id: 'r-bogo', name: 'Buy 1 Get 1', costLyt: 400, usageRate: 0.19, status: 'active', expiresInDays: 61}, {id: 'r-dessert', name: 'Dessert on us', costLyt: 90, usageRate: 0.44, status: 'paused', expiresInDays: 20}, {id: 'r-firstvisit', name: 'First Visit 10%', costLyt: 100, usageRate: 0.63, status: 'active', expiresInDays: 90}, {id: 'r-loyalty500', name: 'Loyalty 500', costLyt: 500, usageRate: 0.28, status: 'expired', expiresInDays: -6}, ]; export function buildRewards( storeId: string, range: RangeKey, endMs: number, ): Reward[] { const scoped = storeId === 'all' ? 1 : 0.28; return CATALOGUE.map((r) => { const rng = createRng('reward', r.id, storeId, range); const claimed = Math.round(rng.int(140, 900) * scoped); const used = Math.round(claimed * r.usageRate * rng.float(0.92, 1.08)); return { id: r.id, name: r.name, costLyt: r.costLyt, claimed, used: Math.min(used, claimed), expiresAt: r.expiresInDays === null ? null : new Date(endMs + r.expiresInDays * 86400000).toISOString(), status: r.status, }; }); } /** * Redemptions over time. * * Derived from the shared timeseries so redemption volume tracks footfall — * an independent generator would produce a redemption curve that contradicts * the store's own traffic, which is immediately obvious to anyone who knows * their business. */ export function buildRedemptions( storeId: string, range: RangeKey, endMs: number, ): TimePoint[] { const points = buildTimeseries(range, storeId, endMs); const rng = createRng('redemptions', storeId, range); return points.map((p) => { // Roughly a third of purchases involve a redemption. const redeemed = Math.round(p.purchases * rng.float(0.24, 0.38)); const issued = Math.round(redeemed * rng.float(1.5, 2.4)); return { t: p.t, // Reusing TimePoint's shape: visitors→issued, purchases→redeemed so the // existing chart primitives apply unchanged. visitors: issued, purchases: redeemed, revenue: redeemed * 140, conversion: issued ? Number(((redeemed / issued) * 100).toFixed(1)) : 0, }; }); } const LYT_ACTIVITY: { kind: ActivityEvent['kind']; title: string; detail: string; }[] = [ {kind: 'reward_redeemed', title: 'Free Coffee redeemed', detail: '120 LYTs · Indiranagar Flagship'}, {kind: 'reward_redeemed', title: 'Weekend Bonus redeemed', detail: '150 LYTs · Koramangala'}, {kind: 'reward_expired', title: 'Loyalty 500 expired', detail: 'Claimed 310 times · used 87'}, {kind: 'reward_redeemed', title: 'Dessert on us redeemed', detail: '90 LYTs · Anna Nagar'}, {kind: 'purchase', title: 'Combo 20% applied at checkout', detail: '₹640 basket · Jubilee Hills'}, ]; export function buildLytActivity( storeId: string, endMs: number, ): ActivityEvent[] { const rng = createRng('lyt-activity', storeId); // Accumulated, not redrawn per index — same ordering bug as buildActivity. let minutesAgo = 0; return Array.from({length: 24}, (_, i) => { const tpl = LYT_ACTIVITY[i % LYT_ACTIVITY.length]; if (i > 0) minutesAgo += rng.int(8, 50); return { id: `l${i}`, at: new Date(endMs - minutesAgo * 60000).toISOString(), kind: tpl.kind, title: tpl.title, detail: tpl.detail, storeId, }; }); }