Menu
Back to Blog
2 min read
SaaS

Affiliate & Referral Systems

Affiliate und Referral Programme implementieren. Tracking Links, Provisionsberechnung und Auszahlungen mit Node.js und Stripe automatisieren.

Affiliate MarketingReferral ProgramCommission TrackingPartner ProgramReferral LinksPayout System
Affiliate & Referral Systems

Affiliate & Referral Systems

Meta-Description: Affiliate und Referral Programme implementieren. Tracking Links, Provisionsberechnung und Auszahlungen mit Node.js und Stripe automatisieren.

Keywords: Affiliate Marketing, Referral Program, Commission Tracking, Partner Program, Referral Links, Payout System, Revenue Share


Einführung

Affiliate und Referral Systeme treiben Wachstum durch Empfehlungen. Von Tracking-Links über Commission-Berechnung bis automatisierte Payouts – ein gut implementiertes System skaliert Kundenakquise kosteneffizient. Dieser Guide zeigt Production-ready Implementierungen.


System Architecture

┌─────────────────────────────────────────────────────────────┐
│          AFFILIATE & REFERRAL SYSTEM                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────┐         ┌─────────────────┐           │
│  │   AFFILIATE     │         │    REFERRAL     │           │
│  │   (B2B/Partner) │         │   (Customer)    │           │
│  ├─────────────────┤         ├─────────────────┤           │
│  │ • Influencers   │         │ • Invite friends│           │
│  │ • Bloggers      │         │ • Share links   │           │
│  │ • Review sites  │         │ • Earn rewards  │           │
│  │ • Partners      │         │ • Both benefit  │           │
│  └────────┬────────┘         └────────┬────────┘           │
│           │                           │                     │
│           └───────────┬───────────────┘                     │
│                       ▼                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              TRACKING SYSTEM                         │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │                                                     │   │
│  │  Unique Link ──► Click Track ──► Cookie/Store     │   │
│  │       │              │               │             │   │
│  │  ?ref=ABC123    IP, Device,     30-90 day         │   │
│  │                 Timestamp       attribution        │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                       │                                     │
│                       ▼                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │            CONVERSION & COMMISSION                   │   │
│  │                                                     │   │
│  │  Purchase ──► Attribution ──► Commission ──► Payout│   │
│  │     │             │              │            │     │   │
│  │  Order ID    Match Cookie    Calculate     Monthly │   │
│  │  completed   to Affiliate    20-50%        Stripe  │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Commission Models:                                        │
│  ├── One-time: Single payment per conversion              │
│  ├── Recurring: % of subscription for lifetime            │
│  ├── Tiered: Higher rates at volume thresholds            │
│  └── Hybrid: One-time + recurring combined                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Database Schema

// prisma/schema.prisma
model Affiliate {
  id            String   @id @default(cuid())
  userId        String   @unique
  user          User     @relation(fields: [userId], references: [id])

  // Affiliate identity
  code          String   @unique // ref=ABC123
  slug          String?  @unique // custom vanity URL

  // Commission settings
  commissionType CommissionType @default(PERCENTAGE)
  commissionRate Decimal  @default(20) // 20%
  recurringRate  Decimal? // For subscription commissions

  // Tier (affects commission rates)
  tier          AffiliateTier @default(STANDARD)

  // Payout info
  payoutMethod  PayoutMethod @default(STRIPE)
  stripeAccountId String?
  paypalEmail   String?
  minimumPayout Int      @default(5000) // $50.00 minimum

  // Status
  status        AffiliateStatus @default(PENDING)
  approvedAt    DateTime?
  suspendedAt   DateTime?

  // Tracking
  clicks        Click[]
  conversions   Conversion[]
  payouts       Payout[]

  // Stats (denormalized for performance)
  totalClicks   Int      @default(0)
  totalConversions Int   @default(0)
  totalEarnings Int      @default(0) // cents
  pendingBalance Int     @default(0) // cents

  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

model Click {
  id          String   @id @default(cuid())
  affiliateId String
  affiliate   Affiliate @relation(fields: [affiliateId], references: [id])

  // Tracking data
  ipAddress   String?
  userAgent   String?
  referer     String?
  landingPage String

  // UTM parameters
  utmSource   String?
  utmMedium   String?
  utmCampaign String?

  // Visitor identification
  visitorId   String   // Fingerprint or cookie ID
  sessionId   String?

  // Conversion tracking
  converted   Boolean  @default(false)
  conversionId String?

  createdAt   DateTime @default(now())

  @@index([affiliateId, createdAt])
  @@index([visitorId])
}

model Conversion {
  id            String   @id @default(cuid())
  affiliateId   String
  affiliate     Affiliate @relation(fields: [affiliateId], references: [id])

  // Order info
  orderId       String   @unique
  customerId    String
  orderAmount   Int      // cents
  currency      String   @default("EUR")

  // Commission
  commissionAmount Int   // cents
  commissionRate Decimal
  commissionType CommissionType

  // Status
  status        ConversionStatus @default(PENDING)
  approvedAt    DateTime?
  rejectedAt    DateTime?
  rejectionReason String?

  // Payout tracking
  payoutId      String?
  payout        Payout?  @relation(fields: [payoutId], references: [id])
  paidAt        DateTime?

  // Attribution
  clickId       String?
  attributionWindow Int  // days

  // For recurring commissions
  isRecurring   Boolean  @default(false)
  parentConversionId String?
  recurringMonth Int?

  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  @@index([affiliateId, status])
  @@index([affiliateId, createdAt])
}

model Payout {
  id            String   @id @default(cuid())
  affiliateId   String
  affiliate     Affiliate @relation(fields: [affiliateId], references: [id])

  amount        Int      // cents
  currency      String   @default("EUR")
  fee           Int      @default(0) // Processing fee

  method        PayoutMethod
  stripeTransferId String?
  paypalPayoutId String?

  status        PayoutStatus @default(PENDING)
  processedAt   DateTime?
  failedAt      DateTime?
  failureReason String?

  conversions   Conversion[]

  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

// Referral-specific (customer referrals)
model Referral {
  id            String   @id @default(cuid())
  referrerId    String   // User who referred
  referrer      User     @relation("ReferralsMade", fields: [referrerId], references: [id])

  referredId    String?  // User who signed up
  referred      User?    @relation("ReferralsReceived", fields: [referredId], references: [id])

  code          String   @unique
  email         String?  // Invited email (before signup)

  // Rewards
  referrerReward Int?    // cents
  referredReward Int?    // cents
  rewardType    RewardType @default(CREDIT)

  // Status
  status        ReferralStatus @default(PENDING)
  signedUpAt    DateTime?
  qualifiedAt   DateTime? // Met qualification criteria
  rewardedAt    DateTime?

  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

enum CommissionType {
  PERCENTAGE
  FIXED
  TIERED
}

enum AffiliateTier {
  STANDARD    // 20%
  SILVER      // 25%
  GOLD        // 30%
  PLATINUM    // 35%
}

enum AffiliateStatus {
  PENDING
  ACTIVE
  SUSPENDED
  TERMINATED
}

enum PayoutMethod {
  STRIPE
  PAYPAL
  BANK_TRANSFER
}

enum ConversionStatus {
  PENDING     // Waiting for approval period
  APPROVED    // Commission confirmed
  REJECTED    // Refund, fraud, etc.
  PAID        // Included in payout
}

enum PayoutStatus {
  PENDING
  PROCESSING
  COMPLETED
  FAILED
}

enum RewardType {
  CREDIT      // Account credit
  CASH        // Cash payout
  DISCOUNT    // Discount code
}

enum ReferralStatus {
  PENDING     // Invited, not signed up
  SIGNED_UP   // Created account
  QUALIFIED   // Met criteria (purchase, etc.)
  REWARDED    // Rewards distributed
  EXPIRED     // Never signed up
}

Affiliate Link Generation

// lib/affiliate/links.ts
import { db } from '@/lib/db';
import crypto from 'crypto';

export async function generateAffiliateCode(): Promise<string> {
  const code = crypto.randomBytes(4).toString('hex').toUpperCase();

  // Ensure uniqueness
  const existing = await db.affiliate.findUnique({
    where: { code }
  });

  if (existing) {
    return generateAffiliateCode(); // Retry
  }

  return code;
}

export function buildAffiliateLink(
  baseUrl: string,
  affiliateCode: string,
  campaign?: string
): string {
  const url = new URL(baseUrl);
  url.searchParams.set('ref', affiliateCode);

  if (campaign) {
    url.searchParams.set('utm_campaign', campaign);
    url.searchParams.set('utm_source', 'affiliate');
    url.searchParams.set('utm_medium', 'referral');
  }

  return url.toString();
}

// Generate vanity URL
export async function setAffiliateSlug(
  affiliateId: string,
  slug: string
): Promise<boolean> {
  // Validate slug format
  if (!/^[a-z0-9-]{3,30}$/.test(slug)) {
    throw new Error('Invalid slug format');
  }

  // Check reserved words
  const reserved = ['admin', 'api', 'app', 'www', 'help', 'support'];
  if (reserved.includes(slug)) {
    throw new Error('Slug is reserved');
  }

  try {
    await db.affiliate.update({
      where: { id: affiliateId },
      data: { slug }
    });
    return true;
  } catch {
    throw new Error('Slug already taken');
  }
}

// Link resolver
export async function resolveAffiliateLink(
  refCode: string
): Promise<Affiliate | null> {
  // Try code first
  let affiliate = await db.affiliate.findUnique({
    where: { code: refCode, status: 'ACTIVE' }
  });

  // Try slug
  if (!affiliate) {
    affiliate = await db.affiliate.findUnique({
      where: { slug: refCode, status: 'ACTIVE' }
    });
  }

  return affiliate;
}

Click Tracking

// lib/affiliate/tracking.ts
import { db } from '@/lib/db';
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
import crypto from 'crypto';

const COOKIE_NAME = 'aff_ref';
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days

interface TrackClickParams {
  affiliateId: string;
  request: NextRequest;
  landingPage: string;
}

export async function trackClick(params: TrackClickParams): Promise<Click> {
  const { affiliateId, request, landingPage } = params;

  const visitorId = getVisitorId(request);
  const searchParams = new URL(request.url).searchParams;

  const click = await db.click.create({
    data: {
      affiliateId,
      ipAddress: request.headers.get('x-forwarded-for'),
      userAgent: request.headers.get('user-agent'),
      referer: request.headers.get('referer'),
      landingPage,
      utmSource: searchParams.get('utm_source'),
      utmMedium: searchParams.get('utm_medium'),
      utmCampaign: searchParams.get('utm_campaign'),
      visitorId,
      sessionId: crypto.randomUUID()
    }
  });

  // Update affiliate stats
  await db.affiliate.update({
    where: { id: affiliateId },
    data: { totalClicks: { increment: 1 } }
  });

  return click;
}

function getVisitorId(request: NextRequest): string {
  // Try to get existing visitor ID from cookie
  const existingId = request.cookies.get('visitor_id')?.value;
  if (existingId) return existingId;

  // Generate fingerprint from request headers
  const components = [
    request.headers.get('user-agent'),
    request.headers.get('accept-language'),
    request.headers.get('accept-encoding')
  ].filter(Boolean).join('|');

  return crypto.createHash('sha256').update(components).digest('hex').slice(0, 16);
}

// Set affiliate cookie
export function setAffiliateCookie(affiliateCode: string): void {
  const cookieStore = cookies();

  cookieStore.set(COOKIE_NAME, affiliateCode, {
    maxAge: COOKIE_MAX_AGE,
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/'
  });
}

// Get affiliate from cookie
export function getAffiliateFromCookie(): string | null {
  const cookieStore = cookies();
  return cookieStore.get(COOKIE_NAME)?.value || null;
}

// Attribution window check
export async function findAttributableClick(
  visitorId: string,
  affiliateId: string,
  windowDays: number = 30
): Promise<Click | null> {
  const windowStart = new Date();
  windowStart.setDate(windowStart.getDate() - windowDays);

  return db.click.findFirst({
    where: {
      affiliateId,
      visitorId,
      createdAt: { gte: windowStart },
      converted: false
    },
    orderBy: { createdAt: 'desc' }
  });
}

Tracking Middleware

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const refCode = request.nextUrl.searchParams.get('ref');

  if (refCode) {
    // Validate affiliate exists and is active
    const isValid = await validateAffiliateCode(refCode);

    if (isValid) {
      // Set cookie
      response.cookies.set('aff_ref', refCode, {
        maxAge: 30 * 24 * 60 * 60, // 30 days
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'lax'
      });

      // Track click asynchronously
      trackClickAsync(refCode, request);

      // Remove ref from URL for cleaner UX
      const cleanUrl = new URL(request.url);
      cleanUrl.searchParams.delete('ref');

      if (cleanUrl.toString() !== request.url) {
        return NextResponse.redirect(cleanUrl);
      }
    }
  }

  return response;
}

async function validateAffiliateCode(code: string): Promise<boolean> {
  // Fast validation via edge-compatible check
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_APP_URL}/api/affiliate/validate?code=${code}`
  );
  return response.ok;
}

function trackClickAsync(refCode: string, request: NextRequest): void {
  // Fire and forget
  fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/affiliate/track`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      refCode,
      landingPage: request.nextUrl.pathname,
      userAgent: request.headers.get('user-agent'),
      referer: request.headers.get('referer'),
      ip: request.headers.get('x-forwarded-for')
    })
  }).catch(console.error);
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

Commission Calculation

// lib/affiliate/commission.ts
import { db } from '@/lib/db';
import { Decimal } from '@prisma/client/runtime/library';

interface CommissionResult {
  amount: number;
  rate: Decimal;
  type: CommissionType;
}

// Tier-based commission rates
const TIER_RATES: Record<AffiliateTier, number> = {
  STANDARD: 20,
  SILVER: 25,
  GOLD: 30,
  PLATINUM: 35
};

export function calculateCommission(
  orderAmount: number,
  affiliate: Affiliate
): CommissionResult {
  let rate: Decimal;
  let amount: number;

  switch (affiliate.commissionType) {
    case 'PERCENTAGE':
      rate = affiliate.tier
        ? new Decimal(TIER_RATES[affiliate.tier])
        : affiliate.commissionRate;
      amount = Math.floor(orderAmount * rate.toNumber() / 100);
      break;

    case 'FIXED':
      rate = affiliate.commissionRate;
      amount = rate.toNumber() * 100; // Convert to cents
      break;

    case 'TIERED':
      const tierResult = calculateTieredCommission(orderAmount, affiliate);
      rate = tierResult.rate;
      amount = tierResult.amount;
      break;

    default:
      rate = new Decimal(20);
      amount = Math.floor(orderAmount * 0.2);
  }

  return {
    amount,
    rate,
    type: affiliate.commissionType
  };
}

function calculateTieredCommission(
  orderAmount: number,
  affiliate: Affiliate
): { amount: number; rate: Decimal } {
  // Get affiliate's total conversions this month
  const monthStart = new Date();
  monthStart.setDate(1);
  monthStart.setHours(0, 0, 0, 0);

  // Tiered rates based on monthly volume
  const tiers = [
    { threshold: 10000_00, rate: 20 }, // $10k+ = 20%
    { threshold: 5000_00, rate: 25 },  // $5k+ = 25%
    { threshold: 1000_00, rate: 30 },  // $1k+ = 30%
    { threshold: 0, rate: 35 }         // Base = 35%
  ];

  // For simplicity, use the affiliate's total earnings to determine tier
  const volumeTier = tiers.find(t => affiliate.totalEarnings >= t.threshold);
  const rate = volumeTier?.rate || 20;

  return {
    amount: Math.floor(orderAmount * rate / 100),
    rate: new Decimal(rate)
  };
}

// Calculate recurring commission for subscriptions
export function calculateRecurringCommission(
  subscriptionAmount: number,
  affiliate: Affiliate,
  month: number
): CommissionResult | null {
  // Most programs cap recurring at 12 months
  if (month > 12) return null;

  // Recurring rate might be lower than initial
  const recurringRate = affiliate.recurringRate || affiliate.commissionRate;
  const amount = Math.floor(subscriptionAmount * recurringRate.toNumber() / 100);

  return {
    amount,
    rate: recurringRate,
    type: 'PERCENTAGE'
  };
}

Conversion Recording

// lib/affiliate/conversions.ts
import { db } from '@/lib/db';
import { calculateCommission } from './commission';
import { getAffiliateFromCookie } from './tracking';

interface RecordConversionParams {
  orderId: string;
  customerId: string;
  orderAmount: number;
  currency?: string;
  isSubscription?: boolean;
}

export async function recordConversion(
  params: RecordConversionParams
): Promise<Conversion | null> {
  const { orderId, customerId, orderAmount, currency = 'EUR', isSubscription } = params;

  // Check for existing conversion (prevent duplicates)
  const existing = await db.conversion.findUnique({
    where: { orderId }
  });

  if (existing) return existing;

  // Get affiliate from cookie or customer's first touch
  const affiliateCode = getAffiliateFromCookie();
  if (!affiliateCode) return null;

  const affiliate = await db.affiliate.findUnique({
    where: { code: affiliateCode, status: 'ACTIVE' }
  });

  if (!affiliate) return null;

  // Calculate commission
  const commission = calculateCommission(orderAmount, affiliate);

  // Find attributable click
  const click = await db.click.findFirst({
    where: {
      affiliateId: affiliate.id,
      converted: false,
      createdAt: {
        gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 days
      }
    },
    orderBy: { createdAt: 'desc' }
  });

  // Create conversion
  const conversion = await db.$transaction(async (tx) => {
    // Create conversion record
    const conv = await tx.conversion.create({
      data: {
        affiliateId: affiliate.id,
        orderId,
        customerId,
        orderAmount,
        currency,
        commissionAmount: commission.amount,
        commissionRate: commission.rate,
        commissionType: commission.type,
        clickId: click?.id,
        attributionWindow: 30,
        isRecurring: isSubscription
      }
    });

    // Mark click as converted
    if (click) {
      await tx.click.update({
        where: { id: click.id },
        data: { converted: true, conversionId: conv.id }
      });
    }

    // Update affiliate stats
    await tx.affiliate.update({
      where: { id: affiliate.id },
      data: {
        totalConversions: { increment: 1 },
        pendingBalance: { increment: commission.amount }
      }
    });

    return conv;
  });

  return conversion;
}

// Auto-approve conversions after hold period
export async function approveConversions(): Promise<number> {
  const holdPeriod = 14; // days
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - holdPeriod);

  const pendingConversions = await db.conversion.findMany({
    where: {
      status: 'PENDING',
      createdAt: { lte: cutoffDate }
    }
  });

  let approved = 0;

  for (const conversion of pendingConversions) {
    // Check if order was refunded
    const wasRefunded = await checkOrderRefunded(conversion.orderId);

    if (wasRefunded) {
      await db.conversion.update({
        where: { id: conversion.id },
        data: {
          status: 'REJECTED',
          rejectedAt: new Date(),
          rejectionReason: 'Order refunded'
        }
      });

      // Reduce pending balance
      await db.affiliate.update({
        where: { id: conversion.affiliateId },
        data: { pendingBalance: { decrement: conversion.commissionAmount } }
      });
    } else {
      await db.conversion.update({
        where: { id: conversion.id },
        data: {
          status: 'APPROVED',
          approvedAt: new Date()
        }
      });

      // Move from pending to earned
      await db.affiliate.update({
        where: { id: conversion.affiliateId },
        data: {
          pendingBalance: { decrement: conversion.commissionAmount },
          totalEarnings: { increment: conversion.commissionAmount }
        }
      });

      approved++;
    }
  }

  return approved;
}

async function checkOrderRefunded(orderId: string): Promise<boolean> {
  // Check with payment provider
  const order = await db.order.findUnique({
    where: { id: orderId }
  });
  return order?.status === 'REFUNDED';
}

Payout System

// lib/affiliate/payouts.ts
import { db } from '@/lib/db';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

interface ProcessPayoutsResult {
  processed: number;
  failed: number;
  totalAmount: number;
}

export async function processPayouts(): Promise<ProcessPayoutsResult> {
  const result = { processed: 0, failed: 0, totalAmount: 0 };

  // Find affiliates with approved conversions ready for payout
  const affiliatesWithBalance = await db.affiliate.findMany({
    where: {
      status: 'ACTIVE',
      totalEarnings: { gte: db.affiliate.fields.minimumPayout }
    },
    include: {
      conversions: {
        where: {
          status: 'APPROVED',
          payoutId: null
        }
      }
    }
  });

  for (const affiliate of affiliatesWithBalance) {
    if (affiliate.conversions.length === 0) continue;

    const payoutAmount = affiliate.conversions.reduce(
      (sum, c) => sum + c.commissionAmount,
      0
    );

    if (payoutAmount < affiliate.minimumPayout) continue;

    try {
      const payout = await createPayout(affiliate, payoutAmount);

      // Link conversions to payout
      await db.conversion.updateMany({
        where: {
          id: { in: affiliate.conversions.map(c => c.id) }
        },
        data: {
          payoutId: payout.id,
          status: 'PAID',
          paidAt: new Date()
        }
      });

      // Reset affiliate balance
      await db.affiliate.update({
        where: { id: affiliate.id },
        data: { totalEarnings: { decrement: payoutAmount } }
      });

      result.processed++;
      result.totalAmount += payoutAmount;

    } catch (error) {
      console.error(`Payout failed for ${affiliate.id}:`, error);
      result.failed++;
    }
  }

  return result;
}

async function createPayout(
  affiliate: Affiliate,
  amount: number
): Promise<Payout> {
  // Create payout record
  const payout = await db.payout.create({
    data: {
      affiliateId: affiliate.id,
      amount,
      currency: 'EUR',
      method: affiliate.payoutMethod,
      status: 'PROCESSING'
    }
  });

  try {
    switch (affiliate.payoutMethod) {
      case 'STRIPE':
        await processStripePayout(affiliate, payout);
        break;
      case 'PAYPAL':
        await processPayPalPayout(affiliate, payout);
        break;
      default:
        throw new Error('Unsupported payout method');
    }

    await db.payout.update({
      where: { id: payout.id },
      data: {
        status: 'COMPLETED',
        processedAt: new Date()
      }
    });

  } catch (error) {
    await db.payout.update({
      where: { id: payout.id },
      data: {
        status: 'FAILED',
        failedAt: new Date(),
        failureReason: error instanceof Error ? error.message : 'Unknown error'
      }
    });
    throw error;
  }

  return payout;
}

async function processStripePayout(
  affiliate: Affiliate,
  payout: Payout
): Promise<void> {
  if (!affiliate.stripeAccountId) {
    throw new Error('No Stripe account connected');
  }

  const transfer = await stripe.transfers.create({
    amount: payout.amount,
    currency: payout.currency.toLowerCase(),
    destination: affiliate.stripeAccountId,
    metadata: {
      payoutId: payout.id,
      affiliateId: affiliate.id
    }
  });

  await db.payout.update({
    where: { id: payout.id },
    data: { stripeTransferId: transfer.id }
  });
}

async function processPayPalPayout(
  affiliate: Affiliate,
  payout: Payout
): Promise<void> {
  if (!affiliate.paypalEmail) {
    throw new Error('No PayPal email configured');
  }

  // PayPal Payouts API integration
  const paypalPayout = await createPayPalPayout({
    email: affiliate.paypalEmail,
    amount: (payout.amount / 100).toFixed(2),
    currency: payout.currency,
    note: `Affiliate commission payout #${payout.id}`
  });

  await db.payout.update({
    where: { id: payout.id },
    data: { paypalPayoutId: paypalPayout.batch_id }
  });
}

Customer Referral System

// lib/referral/referrals.ts
import { db } from '@/lib/db';
import crypto from 'crypto';

const REFERRAL_REWARDS = {
  referrer: 1000, // $10 credit
  referred: 500   // $5 credit
};

export async function createReferralCode(userId: string): Promise<string> {
  const existing = await db.referral.findFirst({
    where: { referrerId: userId }
  });

  if (existing) return existing.code;

  const code = crypto.randomBytes(4).toString('hex').toUpperCase();

  await db.referral.create({
    data: {
      referrerId: userId,
      code,
      status: 'PENDING'
    }
  });

  return code;
}

export async function processReferralSignup(
  newUserId: string,
  referralCode: string
): Promise<boolean> {
  const referral = await db.referral.findUnique({
    where: { code: referralCode }
  });

  if (!referral || referral.status !== 'PENDING') {
    return false;
  }

  // Prevent self-referral
  if (referral.referrerId === newUserId) {
    return false;
  }

  await db.referral.update({
    where: { id: referral.id },
    data: {
      referredId: newUserId,
      status: 'SIGNED_UP',
      signedUpAt: new Date()
    }
  });

  return true;
}

export async function qualifyReferral(
  referredUserId: string
): Promise<void> {
  const referral = await db.referral.findFirst({
    where: {
      referredId: referredUserId,
      status: 'SIGNED_UP'
    }
  });

  if (!referral) return;

  await db.$transaction([
    // Update referral status
    db.referral.update({
      where: { id: referral.id },
      data: {
        status: 'QUALIFIED',
        qualifiedAt: new Date(),
        referrerReward: REFERRAL_REWARDS.referrer,
        referredReward: REFERRAL_REWARDS.referred
      }
    }),

    // Credit referrer
    db.user.update({
      where: { id: referral.referrerId },
      data: { credit: { increment: REFERRAL_REWARDS.referrer } }
    }),

    // Credit referred user
    db.user.update({
      where: { id: referredUserId },
      data: { credit: { increment: REFERRAL_REWARDS.referred } }
    })
  ]);

  // Send notifications
  await notifyReferralReward(referral.referrerId, REFERRAL_REWARDS.referrer);
  await notifyReferralReward(referredUserId, REFERRAL_REWARDS.referred);
}

Affiliate Dashboard API

// app/api/affiliate/dashboard/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';

export async function GET(request: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const affiliate = await db.affiliate.findUnique({
    where: { userId: session.user.id }
  });

  if (!affiliate) {
    return NextResponse.json({ error: 'Not an affiliate' }, { status: 404 });
  }

  // Get date range
  const { searchParams } = new URL(request.url);
  const range = searchParams.get('range') || '30d';
  const startDate = getStartDate(range);

  // Fetch stats
  const [clicks, conversions, recentConversions] = await Promise.all([
    db.click.count({
      where: {
        affiliateId: affiliate.id,
        createdAt: { gte: startDate }
      }
    }),

    db.conversion.aggregate({
      where: {
        affiliateId: affiliate.id,
        createdAt: { gte: startDate }
      },
      _count: true,
      _sum: { commissionAmount: true, orderAmount: true }
    }),

    db.conversion.findMany({
      where: { affiliateId: affiliate.id },
      orderBy: { createdAt: 'desc' },
      take: 10,
      select: {
        id: true,
        orderAmount: true,
        commissionAmount: true,
        status: true,
        createdAt: true
      }
    })
  ]);

  // Calculate conversion rate
  const conversionRate = clicks > 0
    ? ((conversions._count || 0) / clicks * 100).toFixed(2)
    : '0.00';

  return NextResponse.json({
    affiliate: {
      code: affiliate.code,
      tier: affiliate.tier,
      commissionRate: affiliate.commissionRate,
      status: affiliate.status
    },
    stats: {
      clicks,
      conversions: conversions._count || 0,
      conversionRate: `${conversionRate}%`,
      revenue: conversions._sum.orderAmount || 0,
      earnings: conversions._sum.commissionAmount || 0,
      pendingBalance: affiliate.pendingBalance,
      totalEarnings: affiliate.totalEarnings
    },
    recentConversions,
    links: {
      main: `${process.env.NEXT_PUBLIC_APP_URL}?ref=${affiliate.code}`,
      pricing: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?ref=${affiliate.code}`
    }
  });
}

function getStartDate(range: string): Date {
  const now = new Date();
  switch (range) {
    case '7d': return new Date(now.setDate(now.getDate() - 7));
    case '30d': return new Date(now.setDate(now.getDate() - 30));
    case '90d': return new Date(now.setDate(now.getDate() - 90));
    case 'ytd': return new Date(now.getFullYear(), 0, 1);
    default: return new Date(now.setDate(now.getDate() - 30));
  }
}

Best Practices

AspectRecommendation
**Attribution**30-90 day cookie window
**Hold Period**14-30 days before approval
**Commission**20-50% for digital products
**Minimum Payout**$50-100 threshold
**Fraud Prevention**IP tracking, refund checks
**Communication**Real-time dashboard, email alerts

Fazit

Affiliate & Referral Systems erfordern:

  1. Tracking: Cookies, Attribution-Windows
  2. Commission: Faire, klare Berechnung
  3. Payouts: Automatisiert, zuverlässig
  4. Dashboard: Transparenz für Partner

Gute Partner-Programme wachsen organisch durch zufriedene Affiliates.


Bildprompts

  1. "Affiliate dashboard interface, earnings chart and conversion metrics"
  2. "Referral flow diagram, invite friend reward visualization"
  3. "Commission payout system, money transfer illustration"

Quellen