Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Reward engine: - Recurring milestones (every 3 → +7d, every 5 → +14d) instead of one-time tiers capped at 5; idempotent + transactional grants serialised per referrer so concurrent qualifications can't double-count. - Rewards now gated on the referred user's email verification (afterEmailVerification hook); already-verified referees (OAuth) qualify at apply time. - Reward days banked as users.referral_credit_days when the referrer has no live subscription, consumed on next trial start / activation (no more silently lost rewards). - Accurate cumulative rewardDays in stats; getMyReferrals returns referee name/masked email/status. Hardening / cleanup: - onConflictDoNothing makes apply idempotent (no unhandled unique violation). - Anti-fraud: normalizeEmail blocks self-referral via gmail dot/+tag aliases. - Collision-safe referral code generation at signup. - Single apply path (welcome onboarding modal); removed duplicate calls in register + subscription pages. Input validation on the apply code. Email verification UX: - Verification link now lands on a dedicated /email-verified confirmation page instead of the deep-linked VIN/search page. Schema: referrals.status + qualified_at, users.referral_credit_days (0009). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
64 lines
1.7 KiB
TypeScript
64 lines
1.7 KiB
TypeScript
export const PLANS = {
|
|
SINGLE: {
|
|
name: "1 Marka",
|
|
brandCount: 1,
|
|
priceMonthly: 200_00,
|
|
priceYearly: 2000_00,
|
|
},
|
|
DOUBLE: {
|
|
name: "2 Marka",
|
|
brandCount: 2,
|
|
priceMonthly: 350_00,
|
|
priceYearly: 3500_00,
|
|
},
|
|
TRIPLE: {
|
|
name: "3 Marka",
|
|
brandCount: 3,
|
|
priceMonthly: 500_00,
|
|
priceYearly: 5000_00,
|
|
},
|
|
FULL: {
|
|
name: "Full Paket",
|
|
brandCount: 0,
|
|
priceMonthly: 999_00,
|
|
priceYearly: 9990_00,
|
|
},
|
|
} as const;
|
|
|
|
/** Sentinel value representing unlimited brand access for display purposes. */
|
|
export const FULL_PLAN_BRAND_LIMIT = 999;
|
|
|
|
/**
|
|
* Referrer reward milestones. Each time the referrer's *qualified* referral
|
|
* count reaches a multiple of `every`, they earn `days` of subscription time.
|
|
* Milestones are independent and recurring — they stack on a shared multiple
|
|
* (e.g. count 15 fires both the every-3 and every-5 milestone). A referral
|
|
* qualifies only once the referred user verifies their email.
|
|
*/
|
|
export const REFERRAL_REWARDS = {
|
|
MILESTONES: [
|
|
{ every: 3, days: 7 },
|
|
{ every: 5, days: 14 },
|
|
],
|
|
} as const;
|
|
|
|
/** Reward days earned exactly when the qualified count reaches `count`. */
|
|
export function referralRewardForCount(count: number): number {
|
|
if (count <= 0) return 0;
|
|
return REFERRAL_REWARDS.MILESTONES.reduce(
|
|
(sum, m) => (count % m.every === 0 ? sum + m.days : sum),
|
|
0,
|
|
);
|
|
}
|
|
|
|
/** Cumulative reward days earned across all referrals up to `count`. */
|
|
export function referralTotalRewardDays(count: number): number {
|
|
if (count <= 0) return 0;
|
|
return REFERRAL_REWARDS.MILESTONES.reduce(
|
|
(sum, m) => sum + Math.floor(count / m.every) * m.days,
|
|
0,
|
|
);
|
|
}
|
|
|
|
export const CURRENCY = "TRY" as const;
|