Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
# Marketplace Platform Development
|
||||
|
||||
**Meta-Description:** Multi-Vendor Marketplace bauen. Seller Management, Product Listings, Escrow Payments und Commission Handling.
|
||||
|
||||
**Keywords:** Marketplace, Multi-Vendor, E-Commerce, Seller Management, Commission, Escrow, Platform Business
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
**Marketplace Platforms** verbinden Käufer und Verkäufer. Von **Seller Onboarding** bis **Commission Splitting** – die technischen Herausforderungen sind komplex. Dieser Guide zeigt die Architektur und Implementation eines Multi-Vendor Marketplaces.
|
||||
|
||||
---
|
||||
|
||||
## Marketplace Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MARKETPLACE PLATFORM ARCHITECTURE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Stakeholders: │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Platform (You) │ │
|
||||
│ │ ├── Commission on sales │ │
|
||||
│ │ ├── Subscription fees │ │
|
||||
│ │ └── Value-added services │ │
|
||||
│ │ │ │
|
||||
│ │ Sellers │ │
|
||||
│ │ ├── List products │ │
|
||||
│ │ ├── Receive payouts │ │
|
||||
│ │ └── Manage inventory │ │
|
||||
│ │ │ │
|
||||
│ │ Buyers │ │
|
||||
│ │ ├── Browse & purchase │ │
|
||||
│ │ ├── Reviews & ratings │ │
|
||||
│ │ └── Dispute resolution │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Payment Flow: │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Buyer pays $100 │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ Platform receives $100 (Stripe Connect) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ Platform takes 15% commission ($15) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ Seller receives $85 (after payout delay) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Core Features: │
|
||||
│ ├── Seller Dashboard │
|
||||
│ ├── Product Catalog │
|
||||
│ ├── Shopping Cart │
|
||||
│ ├── Checkout (Split Payments) │
|
||||
│ ├── Order Management │
|
||||
│ ├── Reviews & Ratings │
|
||||
│ ├── Dispute Resolution │
|
||||
│ └── Analytics & Reporting │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
```typescript
|
||||
// prisma/schema.prisma
|
||||
model Seller {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
// Business Info
|
||||
businessName String
|
||||
businessType String // individual, company
|
||||
taxId String?
|
||||
description String?
|
||||
logo String?
|
||||
|
||||
// Stripe Connect
|
||||
stripeAccountId String? @unique
|
||||
stripeOnboarded Boolean @default(false)
|
||||
payoutEnabled Boolean @default(false)
|
||||
|
||||
// Settings
|
||||
commissionRate Float @default(0.15) // 15%
|
||||
payoutSchedule String @default("daily") // daily, weekly, monthly
|
||||
|
||||
// Status
|
||||
status SellerStatus @default(PENDING)
|
||||
verifiedAt DateTime?
|
||||
|
||||
// Relations
|
||||
products Product[]
|
||||
orders OrderItem[]
|
||||
payouts Payout[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum SellerStatus {
|
||||
PENDING
|
||||
ACTIVE
|
||||
SUSPENDED
|
||||
BANNED
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(cuid())
|
||||
sellerId String
|
||||
seller Seller @relation(fields: [sellerId], references: [id])
|
||||
|
||||
// Product Info
|
||||
name String
|
||||
slug String
|
||||
description String
|
||||
price Int // in cents
|
||||
compareAtPrice Int?
|
||||
currency String @default("EUR")
|
||||
|
||||
// Inventory
|
||||
sku String?
|
||||
inventory Int @default(0)
|
||||
trackInventory Boolean @default(true)
|
||||
|
||||
// Categorization
|
||||
categoryId String?
|
||||
category Category? @relation(fields: [categoryId], references: [id])
|
||||
tags String[]
|
||||
|
||||
// Media
|
||||
images ProductImage[]
|
||||
|
||||
// Status
|
||||
status ProductStatus @default(DRAFT)
|
||||
publishedAt DateTime?
|
||||
|
||||
// Stats
|
||||
viewCount Int @default(0)
|
||||
salesCount Int @default(0)
|
||||
rating Float?
|
||||
reviewCount Int @default(0)
|
||||
|
||||
// Relations
|
||||
variants ProductVariant[]
|
||||
orderItems OrderItem[]
|
||||
reviews Review[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([sellerId, slug])
|
||||
@@index([categoryId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model Order {
|
||||
id String @id @default(cuid())
|
||||
orderNumber String @unique
|
||||
|
||||
// Buyer
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
// Totals
|
||||
subtotal Int // Sum of items
|
||||
platformFee Int // Platform commission
|
||||
shippingTotal Int
|
||||
taxTotal Int
|
||||
total Int
|
||||
|
||||
// Payment
|
||||
stripePaymentId String?
|
||||
paymentStatus PaymentStatus @default(PENDING)
|
||||
paidAt DateTime?
|
||||
|
||||
// Shipping
|
||||
shippingAddress Json
|
||||
shippingMethod String?
|
||||
|
||||
// Status
|
||||
status OrderStatus @default(PENDING)
|
||||
|
||||
// Items (can be from multiple sellers)
|
||||
items OrderItem[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model OrderItem {
|
||||
id String @id @default(cuid())
|
||||
orderId String
|
||||
order Order @relation(fields: [orderId], references: [id])
|
||||
|
||||
// Product
|
||||
productId String
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
variantId String?
|
||||
|
||||
// Seller
|
||||
sellerId String
|
||||
seller Seller @relation(fields: [sellerId], references: [id])
|
||||
|
||||
// Pricing
|
||||
price Int
|
||||
quantity Int
|
||||
subtotal Int // price * quantity
|
||||
commission Int // Platform commission
|
||||
sellerPayout Int // subtotal - commission
|
||||
|
||||
// Status (per-seller fulfillment)
|
||||
status OrderItemStatus @default(PENDING)
|
||||
shippedAt DateTime?
|
||||
trackingNumber String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Payout {
|
||||
id String @id @default(cuid())
|
||||
sellerId String
|
||||
seller Seller @relation(fields: [sellerId], references: [id])
|
||||
|
||||
amount Int
|
||||
currency String @default("EUR")
|
||||
status PayoutStatus @default(PENDING)
|
||||
|
||||
stripePayoutId String?
|
||||
paidAt DateTime?
|
||||
failedAt DateTime?
|
||||
failureReason String?
|
||||
|
||||
// Payout period
|
||||
periodStart DateTime
|
||||
periodEnd DateTime
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Seller Onboarding
|
||||
|
||||
```typescript
|
||||
// lib/marketplace/seller-onboarding.ts
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function createSellerAccount(
|
||||
userId: string,
|
||||
data: {
|
||||
businessName: string;
|
||||
businessType: 'individual' | 'company';
|
||||
country: string;
|
||||
email: string;
|
||||
}
|
||||
): Promise<string> {
|
||||
// Create Stripe Connect account
|
||||
const account = await stripe.accounts.create({
|
||||
type: 'express',
|
||||
country: data.country,
|
||||
email: data.email,
|
||||
business_type: data.businessType,
|
||||
business_profile: {
|
||||
name: data.businessName
|
||||
},
|
||||
capabilities: {
|
||||
card_payments: { requested: true },
|
||||
transfers: { requested: true }
|
||||
}
|
||||
});
|
||||
|
||||
// Create seller record
|
||||
await db.seller.create({
|
||||
data: {
|
||||
userId,
|
||||
businessName: data.businessName,
|
||||
businessType: data.businessType,
|
||||
stripeAccountId: account.id,
|
||||
status: 'PENDING'
|
||||
}
|
||||
});
|
||||
|
||||
// Generate onboarding link
|
||||
const accountLink = await stripe.accountLinks.create({
|
||||
account: account.id,
|
||||
refresh_url: `${process.env.NEXT_PUBLIC_URL}/seller/onboarding/refresh`,
|
||||
return_url: `${process.env.NEXT_PUBLIC_URL}/seller/onboarding/complete`,
|
||||
type: 'account_onboarding'
|
||||
});
|
||||
|
||||
return accountLink.url;
|
||||
}
|
||||
|
||||
export async function completeSellerOnboarding(
|
||||
stripeAccountId: string
|
||||
): Promise<void> {
|
||||
// Check account status
|
||||
const account = await stripe.accounts.retrieve(stripeAccountId);
|
||||
|
||||
if (account.charges_enabled && account.payouts_enabled) {
|
||||
await db.seller.update({
|
||||
where: { stripeAccountId },
|
||||
data: {
|
||||
stripeOnboarded: true,
|
||||
payoutEnabled: true,
|
||||
status: 'ACTIVE',
|
||||
verifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook handler for account updates
|
||||
export async function handleAccountUpdated(
|
||||
account: Stripe.Account
|
||||
): Promise<void> {
|
||||
await db.seller.update({
|
||||
where: { stripeAccountId: account.id },
|
||||
data: {
|
||||
payoutEnabled: account.payouts_enabled || false,
|
||||
stripeOnboarded: account.details_submitted || false
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Split Payments
|
||||
|
||||
```typescript
|
||||
// lib/marketplace/checkout.ts
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
interface CartItem {
|
||||
productId: string;
|
||||
quantity: number;
|
||||
variantId?: string;
|
||||
}
|
||||
|
||||
export async function createMarketplaceCheckout(
|
||||
userId: string,
|
||||
items: CartItem[],
|
||||
shippingAddress: any
|
||||
): Promise<string> {
|
||||
// Group items by seller
|
||||
const itemsBySeller = await groupItemsBySeller(items);
|
||||
|
||||
// Calculate totals
|
||||
const { subtotal, platformFee, lineItems, transfers } =
|
||||
await calculateOrderTotals(itemsBySeller);
|
||||
|
||||
// Create order
|
||||
const order = await db.order.create({
|
||||
data: {
|
||||
orderNumber: `ORD-${nanoid(10).toUpperCase()}`,
|
||||
userId,
|
||||
subtotal,
|
||||
platformFee,
|
||||
shippingTotal: 0, // Calculate based on sellers
|
||||
taxTotal: 0,
|
||||
total: subtotal,
|
||||
shippingAddress,
|
||||
status: 'PENDING',
|
||||
items: {
|
||||
create: lineItems.map(item => ({
|
||||
productId: item.productId,
|
||||
sellerId: item.sellerId,
|
||||
variantId: item.variantId,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
subtotal: item.subtotal,
|
||||
commission: item.commission,
|
||||
sellerPayout: item.sellerPayout,
|
||||
status: 'PENDING'
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create Stripe Checkout with transfers
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: 'payment',
|
||||
customer_email: (await db.user.findUnique({ where: { id: userId } }))?.email,
|
||||
line_items: lineItems.map(item => ({
|
||||
price_data: {
|
||||
currency: 'eur',
|
||||
unit_amount: item.price,
|
||||
product_data: {
|
||||
name: item.productName,
|
||||
images: item.images
|
||||
}
|
||||
},
|
||||
quantity: item.quantity
|
||||
})),
|
||||
payment_intent_data: {
|
||||
// Split payment to sellers
|
||||
transfer_group: order.id,
|
||||
metadata: {
|
||||
orderId: order.id
|
||||
}
|
||||
},
|
||||
success_url: `${process.env.NEXT_PUBLIC_URL}/orders/${order.id}/success`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`,
|
||||
metadata: {
|
||||
orderId: order.id
|
||||
}
|
||||
});
|
||||
|
||||
return session.url!;
|
||||
}
|
||||
|
||||
async function calculateOrderTotals(itemsBySeller: Map<string, CartItem[]>) {
|
||||
const lineItems: any[] = [];
|
||||
let subtotal = 0;
|
||||
let platformFee = 0;
|
||||
const transfers: any[] = [];
|
||||
|
||||
for (const [sellerId, items] of itemsBySeller) {
|
||||
const seller = await db.seller.findUnique({
|
||||
where: { id: sellerId }
|
||||
});
|
||||
|
||||
for (const item of items) {
|
||||
const product = await db.product.findUnique({
|
||||
where: { id: item.productId }
|
||||
});
|
||||
|
||||
if (!product) continue;
|
||||
|
||||
const itemSubtotal = product.price * item.quantity;
|
||||
const commission = Math.round(itemSubtotal * (seller?.commissionRate || 0.15));
|
||||
const sellerPayout = itemSubtotal - commission;
|
||||
|
||||
subtotal += itemSubtotal;
|
||||
platformFee += commission;
|
||||
|
||||
lineItems.push({
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
sellerId,
|
||||
variantId: item.variantId,
|
||||
price: product.price,
|
||||
quantity: item.quantity,
|
||||
subtotal: itemSubtotal,
|
||||
commission,
|
||||
sellerPayout,
|
||||
images: [] // Add product images
|
||||
});
|
||||
|
||||
// Prepare transfer for seller
|
||||
transfers.push({
|
||||
sellerId,
|
||||
stripeAccountId: seller?.stripeAccountId,
|
||||
amount: sellerPayout
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { subtotal, platformFee, lineItems, transfers };
|
||||
}
|
||||
|
||||
// After successful payment, create transfers
|
||||
export async function createSellerTransfers(orderId: string): Promise<void> {
|
||||
const order = await db.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
items: {
|
||||
include: { seller: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!order) return;
|
||||
|
||||
// Group payouts by seller
|
||||
const payoutsBySeller = new Map<string, number>();
|
||||
|
||||
for (const item of order.items) {
|
||||
const current = payoutsBySeller.get(item.sellerId) || 0;
|
||||
payoutsBySeller.set(item.sellerId, current + item.sellerPayout);
|
||||
}
|
||||
|
||||
// Create transfers
|
||||
for (const [sellerId, amount] of payoutsBySeller) {
|
||||
const seller = order.items.find(i => i.sellerId === sellerId)?.seller;
|
||||
|
||||
if (!seller?.stripeAccountId) continue;
|
||||
|
||||
await stripe.transfers.create({
|
||||
amount,
|
||||
currency: 'eur',
|
||||
destination: seller.stripeAccountId,
|
||||
transfer_group: orderId,
|
||||
metadata: {
|
||||
orderId,
|
||||
sellerId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update order status
|
||||
await db.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
paymentStatus: 'PAID',
|
||||
paidAt: new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Seller Dashboard
|
||||
|
||||
```tsx
|
||||
// app/seller/dashboard/page.tsx
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export default async function SellerDashboard() {
|
||||
const session = await getServerSession();
|
||||
const seller = await db.seller.findUnique({
|
||||
where: { userId: session!.user.id }
|
||||
});
|
||||
|
||||
const [stats, recentOrders, topProducts] = await Promise.all([
|
||||
getSellerStats(seller!.id),
|
||||
getRecentOrders(seller!.id),
|
||||
getTopProducts(seller!.id)
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">Seller Dashboard</h1>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
title="Revenue (30d)"
|
||||
value={`€${(stats.revenue / 100).toFixed(2)}`}
|
||||
change={stats.revenueChange}
|
||||
/>
|
||||
<StatCard
|
||||
title="Orders"
|
||||
value={stats.orderCount}
|
||||
change={stats.orderChange}
|
||||
/>
|
||||
<StatCard
|
||||
title="Products"
|
||||
value={stats.productCount}
|
||||
/>
|
||||
<StatCard
|
||||
title="Pending Payout"
|
||||
value={`€${(stats.pendingPayout / 100).toFixed(2)}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Recent Orders */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Recent Orders</h2>
|
||||
<OrderList orders={recentOrders} />
|
||||
</div>
|
||||
|
||||
{/* Top Products */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Top Products</h2>
|
||||
<ProductList products={topProducts} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function getSellerStats(sellerId: string) {
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const [revenue, orders, products, pendingPayout] = await Promise.all([
|
||||
db.orderItem.aggregate({
|
||||
where: {
|
||||
sellerId,
|
||||
order: { paidAt: { gte: thirtyDaysAgo } }
|
||||
},
|
||||
_sum: { sellerPayout: true }
|
||||
}),
|
||||
db.orderItem.count({
|
||||
where: {
|
||||
sellerId,
|
||||
createdAt: { gte: thirtyDaysAgo }
|
||||
}
|
||||
}),
|
||||
db.product.count({ where: { sellerId } }),
|
||||
db.orderItem.aggregate({
|
||||
where: {
|
||||
sellerId,
|
||||
order: { paymentStatus: 'PAID' },
|
||||
// Not yet paid out
|
||||
},
|
||||
_sum: { sellerPayout: true }
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
revenue: revenue._sum.sellerPayout || 0,
|
||||
revenueChange: 0, // Calculate vs previous period
|
||||
orderCount: orders,
|
||||
orderChange: 0,
|
||||
productCount: products,
|
||||
pendingPayout: pendingPayout._sum.sellerPayout || 0
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform Economics
|
||||
|
||||
| Revenue Stream | Example |
|
||||
|---------------|---------|
|
||||
| **Commission** | 10-20% per sale |
|
||||
| **Listing Fee** | $0.20 per listing |
|
||||
| **Subscription** | $29/mo for premium |
|
||||
| **Promoted Listings** | $0.10 per click |
|
||||
| **Transaction Fee** | 2.9% + $0.30 |
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
Marketplace Platform Development erfordert:
|
||||
|
||||
1. **Multi-Party Payments**: Stripe Connect für Split Payments
|
||||
2. **Seller Management**: Onboarding und Verification
|
||||
3. **Trust & Safety**: Reviews, Disputes, Moderation
|
||||
4. **Scalability**: Handling vieler Verkäufer und Produkte
|
||||
|
||||
Marketplaces sind komplex, aber hochprofitabel.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "Marketplace platform architecture diagram, buyers sellers and platform"
|
||||
2. "Seller dashboard interface, revenue charts and order list"
|
||||
3. "Payment flow visualization, split payment to multiple sellers"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [Stripe Connect](https://stripe.com/docs/connect)
|
||||
- [Marketplace Business Models](https://a16z.com/marketplace-100/)
|
||||
- [Sharetribe Marketplace Guide](https://www.sharetribe.com/academy/)
|
||||
- [Building Marketplaces](https://www.nfx.com/post/marketplace-handbook)
|
||||
Reference in New Issue
Block a user