Skip to content

Instantly share code, notes, and snippets.

@charanjit-singh
Created September 24, 2025 10:32
Show Gist options
  • Save charanjit-singh/b183df63c9830dd0f943f7399b65631c to your computer and use it in GitHub Desktop.
Save charanjit-singh/b183df63c9830dd0f943f7399b65631c to your computer and use it in GitHub Desktop.

Revisions

  1. charanjit-singh created this gist Sep 24, 2025.
    152 changes: 152 additions & 0 deletions refHandler.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,152 @@
    import Cookies from 'js-cookie';

    // Referral cookie name
    const REFERRAL_COOKIE_NAME = 'indiekit_referrals';
    // Cookie expiration days (45 days)
    const COOKIE_EXPIRY_DAYS = 45;
    // Max referrals to store
    const MAX_REFERRALS = 5;

    interface ReferralEntry {
    code: string;
    timestamp: number;
    }

    /**
    * Get all stored referrals from the cookie
    */
    export function getStoredReferrals(): ReferralEntry[] {
    try {
    const cookieValue = Cookies.get(REFERRAL_COOKIE_NAME);
    if (!cookieValue) return [];

    return JSON.parse(cookieValue);
    } catch (error) {
    console.error('Error parsing referral cookie:', error);
    // If the cookie is corrupt, reset it
    Cookies.remove(REFERRAL_COOKIE_NAME);
    return [];
    }
    }

    /**
    * Add a new referral code to the cookie
    */
    export function addReferral(refCode: string): void {
    if (!refCode) return;

    try {
    const referrals = getStoredReferrals();

    // Check if this referral code already exists
    const existingReferralIndex = referrals.findIndex(ref => ref.code === refCode);

    if (existingReferralIndex >= 0) {
    // Code already exists, do nothing to preserve the original timestamp
    return;
    }

    // Add new referral with current timestamp
    const newReferral: ReferralEntry = {
    code: refCode,
    timestamp: Date.now()
    };

    // Add to the beginning, limit to max number of referrals
    const updatedReferrals = [newReferral, ...referrals].slice(0, MAX_REFERRALS);

    // Save back to cookie
    Cookies.set(REFERRAL_COOKIE_NAME, JSON.stringify(updatedReferrals), {
    expires: COOKIE_EXPIRY_DAYS,
    sameSite: 'lax',
    path: '/'
    });
    } catch (error) {
    console.error('Error updating referral cookie:', error);
    }
    }

    /**
    * Clean up expired referrals (older than 45 days)
    */
    export function cleanupExpiredReferrals(): void {
    try {
    const referrals = getStoredReferrals();
    const now = Date.now();
    const expiryTime = COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000; // 45 days in milliseconds

    // Filter out expired referrals
    const validReferrals = referrals.filter(ref => {
    return (now - ref.timestamp) < expiryTime;
    });

    // Only update if we removed some expired referrals
    if (validReferrals.length < referrals.length) {
    Cookies.set(REFERRAL_COOKIE_NAME, JSON.stringify(validReferrals), {
    expires: COOKIE_EXPIRY_DAYS,
    sameSite: 'lax',
    path: '/'
    });
    }
    } catch (error) {
    console.error('Error cleaning up referrals:', error);
    }
    }

    /**
    * Get the first/oldest valid referral within the attribution window
    */
    export function getApplicableReferral(): string | null {
    try {
    cleanupExpiredReferrals();

    const referrals = getStoredReferrals();

    if (referrals.length === 0) return null;

    // Return the oldest (last in array) valid referral code
    // This implements "first touch" attribution
    return referrals[referrals.length - 1].code;

    } catch (error) {
    console.error('Error getting applicable referral:', error);
    return null;
    }
    }

    /**
    * Check if a URL search param has a referral code and add it to the cookie
    */
    export function handleReferralFromURL(): void {
    if (typeof window === 'undefined') return;

    try {
    const urlParams = new URLSearchParams(window.location.search);
    const refCode = urlParams.get('ref');

    if (refCode) {
    addReferral(refCode);
    }
    } catch (error) {
    console.error('Error handling referral from URL:', error);
    }
    }

    /**
    * Add referral code to payment link
    */
    export function addReferralToPaymentLink(baseLink: string): string {
    try {
    const refCode = getApplicableReferral();

    if (!refCode) return baseLink;

    // Check if URL already has parameters
    const separator = baseLink.includes('?') ? '&' : '?';

    return `${baseLink}${separator}metadata_ref=${encodeURIComponent(refCode)}`;
    } catch (error) {
    console.error('Error adding referral to payment link:', error);
    return baseLink;
    }
    }
    46 changes: 46 additions & 0 deletions schema.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,46 @@
    import {
    timestamp,
    pgTable,
    text,
    numeric,
    jsonb,
    } from "drizzle-orm/pg-core";
    import { users } from "./user";

    // Schema to track referrals (who referred whom and commission amount)
    export const referrals = pgTable("referrals", {
    id: text("id")
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
    referrerId: text("referrerId")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
    referredId: text("referredId")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
    referredEmail: text("referredEmail").notNull(),
    purchaseAmount: numeric("purchaseAmount").notNull(),
    commissionAmount: numeric("commissionAmount").notNull(),
    planCodename: text("planCodename").notNull(),
    status: text("status").notNull().default("pending"), // pending, approved, rejected
    createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(),
    settledIn: text("settledIn").references(() => payouts.id), // Link to the payout this referral was settled in
    });

    // Schema to track payouts to referrers
    export const payouts = pgTable("payouts", {
    id: text("id")
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
    userId: text("userId")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
    amount: numeric("amount").notNull(),
    status: text("status").notNull().default("pending"), // pending, processing, completed, rejected
    paymentMethod: text("paymentMethod").notNull(), // paypal, bank_transfer, etc.
    paymentDetails: jsonb("paymentDetails"), // Store payment details like PayPal email, bank account info
    screenshot: text("screenshot"), // URL to proof of payment screenshot
    notes: text("notes"),
    createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(),
    completedAt: timestamp("completedAt", { mode: "date" }),
    });
    101 changes: 101 additions & 0 deletions webhook.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,101 @@
    // Referral code management
    try {
    const metadata = body.data.metadata || {};
    // Type assertion to fix TypeScript error
    const referralCode = metadata.ref as string | undefined;

    if (referralCode) {
    console.log(`Referral code found: ${referralCode}`);

    // Get the referrer user
    const referrer = await db
    .select()
    .from(users)
    .where(eq(users.referralCode, referralCode))
    .limit(1)
    .then((users) => users[0]);

    if (!referrer) {
    console.log(`Referrer not found for code: ${referralCode}`);
    } else if (referrer.id === user.id) {
    // Self-referral detected
    console.log(`Self-referral detected for user: ${user.email}`);

    await sendMail(
    user.email,
    "Self-referral detected",
    `
    <h1>Self-referral detected</h1>
    <p>We noticed that you used your own referral code when making a purchase. Self-referrals are not eligible for commission payouts.</p>
    <p>If you believe this is an error, please contact our support team.</p>
    `
    );
    } else {
    const currency = body.data.currency;
    const amountPaid = body.data.total_amount / 100; // Convert from cents to dollars
    const taxPaid = body.data.tax / 100; // Convert from cents to dollars
    let purchaseAmount = amountPaid - taxPaid; // Convert from cents to dollars
    if (currency !== "USD") {
    const response = await fetch(
    `https://api.currencyfreaks.com/v2.0/rates/latest?apikey=b656eb60121d44c5a21cf3fdb86b91f7&symbols=${currency}`
    );
    // {"date":"2025-04-08 00:00:00+00","base":"USD","rates":{"INR":"85.8825"}}
    const data = await response.json();
    const exchangeRate = data.rates[currency];
    purchaseAmount = purchaseAmount / exchangeRate; // Convert to USD
    console.log(`Purchase amount in USD: ${purchaseAmount}`);
    console.log(`Exchange rate: ${exchangeRate}`);
    console.log(`Currency: ${currency}`);
    }

    // Valid referral - calculate commission amount (30%)
    const commissionRate = 0.3; // 30% commission
    const commissionAmount = purchaseAmount * commissionRate;

    // Create a referral record
    await db.insert(referrals).values({
    referrerId: referrer.id,
    referredId: user.id,
    referredEmail: user.email,
    purchaseAmount: purchaseAmount.toString(),
    commissionAmount: commissionAmount.toString(),
    planCodename: planCodename,
    status: "pending", // Pending approval
    });

    // Send email to referrer
    await sendMail(
    referrer.email!,
    "You earned a commission!",
    `
    <h1>You earned a commission!</h1>
    <p>Great news! Someone just purchased Indie Kit Pro using your referral link.</p>
    <p><strong>Purchase details:</strong></p>
    <ul>
    <li>Product: ${planCodename}</li>
    <li>Purchase amount: $${purchaseAmount.toFixed(2)}</li>
    <li>Your commission (30%): $${commissionAmount.toFixed(2)}</li>
    </ul>
    <p>Your commission is now pending approval and will be processed in the next payout cycle.</p>
    <p><a href="https://indiekit.pro/app/my-referrals">View your referrals dashboard</a> to track this and other commissions.</p>
    <p>Thank you for being part of our affiliate program!</p>
    `
    );

    console.log(
    `Referral recorded successfully for referrer: ${
    referrer.email
    }, amount: $${commissionAmount.toFixed(2)}`
    );
    }
    }
    } catch (error) {
    console.error("Error processing referral:", error);
    }