# 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

| Aspect | Recommendation |
|--------|----------------|
| **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

- [Stripe Connect for Marketplaces](https://stripe.com/docs/connect)
- [First Promoter](https://firstpromoter.com/)
- [Rewardful API](https://www.rewardful.com/)
- [Affiliate Marketing Best Practices](https://www.refersion.com/blog/affiliate-marketing-best-practices/)
