/opt/canhelp/apps/api/src/lib
NameSizeModeActions
activity.ts13240644editdlrm
apple-client-secret.ts40730644editdlrm
balance.ts35060644editdlrm
daily-limits.ts42820644editdlrm
email.ts527080644editdlrm
fomo.ts113660644editdlrm
google-oauth.ts105260644editdlrm
helper-capability.ts42070644editdlrm
messaging-rules.ts31230644editdlrm
nameUtils.ts7090644editdlrm
notif.ts189290644editdlrm
plan-history.ts10200644editdlrm
push.ts63860644editdlrm
referral.ts61100644editdlrm
telegram.ts62070644editdlrm
turnstile.ts7410644editdlrm
Edit: /opt/canhelp/apps/api/src/lib/referral.ts (6110B)
import { eq, gte } from 'drizzle-orm' import { db } from '../db.js' import { users, plans, settings as settingsTable, referralRewards } from '@canhelp/db' import { notifyReferralReward } from './notif.js' import { emailReferralReward } from './email.js' import { logPlanHistory } from './plan-history.js' const TIER_ORDER: Record = { free: 0, pro: 1, ultimate: 2 } /** Generate a short unique referral code for a user */ export function generateReferralCode(): string { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' let code = '' for (let i = 0; i < 8; i++) { code += chars[Math.floor(Math.random() * chars.length)] } return code } /** Get current referral program settings from DB */ export async function getReferralSettings(): Promise<{ enabled: boolean reward: { planId: string; days: number } | null } | null> { const rows = await db .select() .from(settingsTable) .where(gte(settingsTable.key, 'referral_')) const map: Record = {} for (const row of rows) { if (row.key.startsWith('referral_')) map[row.key] = row.value } if (map['referral_enabled'] !== 'true') return null const planId = map['referral_plan_id'] || map['referral_customer_plan_id'] || map['referral_specialist_plan_id'] || '' const days = Number(map['referral_days'] || map['referral_customer_days'] || map['referral_specialist_days'] || '0') return { enabled: true, reward: planId && days > 0 ? { planId, days } : null, } } /** * Apply referral reward to the referrer after a new user registers. * - Looks up referrer by referral code * - Ensures this referee hasn't been used before * - Assigns / extends planExpiresAt on the referrer * - Logs to referral_rewards */ export async function applyReferral(refereeId: string, refCode: string): Promise { const cfg = await getReferralSettings() if (!cfg) return // Find referrer const [referrer] = await db .select({ user: users, planRole: plans.role, }) .from(users) .leftJoin(plans, eq(users.planId, plans.id)) .where(eq(users.referralCode, refCode)) .limit(1) const referrerUser = referrer?.user if (!referrerUser || referrerUser.id === refereeId) return // Prevent duplicate reward for same referee const existing = await db .select({ id: referralRewards.id }) .from(referralRewards) .where(eq(referralRewards.refereeId, refereeId)) .limit(1) if (existing.length) return const rewardCfg = cfg.reward if (!rewardCfg) return // Verify reward plan exists const [rewardPlan] = await db.select().from(plans).where(eq(plans.id, rewardCfg.planId)).limit(1) if (!rewardPlan) return // If referrer's current plan tier >= reward plan tier — keep their plan, only extend days let assignPlanId = rewardCfg.planId if (referrerUser.planId) { const [currentPlan] = await db .select({ tier: plans.tier }) .from(plans) .where(eq(plans.id, referrerUser.planId)) .limit(1) if (currentPlan) { const currentTier = TIER_ORDER[currentPlan.tier] ?? 0 const rewardTier = TIER_ORDER[rewardPlan.tier] ?? 0 if (currentTier >= rewardTier) { assignPlanId = referrerUser.planId // don't downgrade — keep current plan } } } // Calculate new expiry (extend from current expiry or now) const now = new Date() const base = referrerUser.planExpiresAt && referrerUser.planExpiresAt > now ? referrerUser.planExpiresAt : now const newExpiry = new Date(base.getTime() + rewardCfg.days * 24 * 60 * 60 * 1000) // Update referrer await db .update(users) .set({ planId: assignPlanId, planExpiresAt: newExpiry, updatedAt: new Date() }) .where(eq(users.id, referrerUser.id)) // Update referee's referredBy (store referrer userId now) await db .update(users) .set({ referredBy: referrerUser.id, updatedAt: new Date() }) .where(eq(users.id, refereeId)) // Log reward const [reward] = await db.insert(referralRewards).values({ referrerId: referrerUser.id, refereeId, planId: assignPlanId, daysAdded: rewardCfg.days, expiresAt: newExpiry, }).returning() // Send in-app notification + email to referrer // Use the name of the actually assigned plan (may differ from reward plan if referrer has higher tier) let planName = rewardPlan.name if (assignPlanId !== rewardCfg.planId) { const [assignedPlan] = await db.select({ name: plans.name }).from(plans).where(eq(plans.id, assignPlanId)).limit(1) if (assignedPlan) planName = assignedPlan.name } // Log plan history logPlanHistory({ userId: referrerUser.id, event: 'referral_reward', planId: assignPlanId, planName, previousPlanId: referrerUser.planId, previousPlanName: referrerUser.planId === assignPlanId ? planName : rewardPlan.name, expiresAt: newExpiry, daysAdded: rewardCfg.days, referralRewardId: reward.id, }) const loc = (referrerUser.locale === 'en' || referrerUser.locale === 'ru' ? referrerUser.locale : 'el') as 'el' | 'en' | 'ru' await Promise.all([ notifyReferralReward(referrerUser.id, rewardCfg.days, planName, reward.id), emailReferralReward({ to: referrerUser.email, name: referrerUser.name, days: rewardCfg.days, planName, locale: loc }), ]) } /** * Called after email verification. * Reads the pending referral code stored in referredBy and applies the reward. * referredBy at this point contains a raw referral code (before applyReferral resolves it to userId). */ export async function applyPendingReferral(userId: string): Promise { const [referee] = await db .select({ id: users.id, referredBy: users.referredBy }) .from(users) .where(eq(users.id, userId)) .limit(1) if (!referee?.referredBy) return // If referredBy looks like a userId (already resolved), skip // Raw ref codes are 8 chars uppercase alphanumeric; userIDs are longer const pending = referee.referredBy if (pending.length > 12) return // already a userId — reward already applied await applyReferral(userId, pending) }