Files
Portfolio/blog-posts/99-affiliate-referral-systems.md
T
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

1256 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/)