Next.js ile Kampanya & Kupon Sistemi

Bu makalemde Next.js App Router mimarisini kullanarak, gerçek hayatta e-ticaret ve SaaS projelerindeki ihtiyaçları kapsayan kampanya ve kupon sistemini tasarlamaya çalışacağım.

Bu makalenin içeriği; veri modeli, iş kuralları, API katmanı, güvenlik, performans ve frontend entegrasyonunu kapsamaktadır.

1. Kampanya & Kupon Mantığını Ayırmak

Çoğu projede yapılan temel hata, kampanya ile kuponu aynı olaymış gibi ele almaktır.

Kampanya: Otomatik uygulanır. (%20 indirim, sepette 3 ürün olunca geçerli gibi)
Kupon: Kullanıcı aksiyonu gerektirir. (kod girişi gibi)

Bu 2 durumu da ayırmak;

  • İş kurallarını sadeleştirir,
  • Performans kazandırır,
  • Yönetimi kolaylaştırır.

2. Temel Veri Modeli

Modelleme olarak Prisma ORM üzerinden kurgulayacağım, fakat diğer ORM paketleri ile de uyarlayabilirsiniz.

Kampanya Modeli


    model Campaign {
        id            String @id @default(uuid())
        title         String
        description   String?
        discountType  DiscountType
        discountValue Float
        minCartTotal  Float?
        startDate     DateTime
        endDate       DateTime
        isActive      Boolean @default(true)
        createdAt     DateTime @default(now())
    }

Kupon Modeli


    model Coupon {
        id            String @id @default(uuid())
        code          String @unique
        discountType  DiscountType
        discountValue Float
        usageLimit    Int?
        usedCount     Int @default(0)
        expiresAt     DateTime?
        isActive      Boolean @default(true)
        createdAt     DateTime @default(now())
    }

Ortam Enum


    enum DiscountType {
        PERCENT
        FIXED
    }

3. İndirim Hesaplama Stratejisi

İndirim hesaplama işlemi asla frontend kısmında olmamalıdır. Bu durum hem güvenlik hem de tutarlılık açısından kritiktir.

Hesaplama Fonksiyonu


    export function calculateDiscount(
        total: number,
        type: 'PERCENT' | 'FIXED',
        value: number
        ) {
        if (type === 'PERCENT') {
            return total - total * (value / 100)
        }

        return Math.max(total - value, 0)
    }

Bu fonksiyon hem kampanya hem de kupon için yeniden kullanılabilir.

4. Aktif Kampanya Fonksiyonu

Kampanya Servisi


    export async function getActiveCampaign(total: number) {
        return prisma.campaign.findFirst({
            where: {
                isActive: true,
                startDate: { lte: new Date() },
                endDate: { gte: new Date() },
                OR: [
                    { minCartTotal: null },
                    { minCartTotal: { lte: total } }
                ]
            },
            orderBy: { discountValue: 'desc' }
        })
    }

5. Kupon Doğrulama API'si (App Router)

API Route


    // app/api/coupon/validate/route.ts
    import { NextResponse } from 'next/server'

    export async function POST(req: Request) {
        const { code, total } = await req.json()

        const coupon = await prisma.coupon.findUnique({
            where: { code }
        })

        if (!coupon || !coupon.isActive) {
            return NextResponse.json({ error: 'Geçersiz kupon' }, { status: 400 })
        }

        if (coupon.expiresAt && coupon.expiresAt < new Date()) {
            return NextResponse.json({ error: 'Kupon süresi dolmuş' }, { status: 400 })
        }

        if (coupon.usageLimit && coupon.usedCount >= coupon.usageLimit) {
            return NextResponse.json({ error: 'Kupon kullanım limiti doldu' }, { status: 400 })
        }

        const discountedTotal = calculateDiscount(
            total,
            coupon.discountType,
            coupon.discountValue
        )

        return NextResponse.json({ discountedTotal })
    }

6. Güvenli Kupon Kullanımı

Kupon kullanım sayısı ödeme başarıyla tamamlandıktan sonra arttırılmalıdır.


    await prisma.coupon.update({
        where: { id: couponId },
        data: {
            usedCount: { increment: 1 }
        }
    })

Bu işlem genellikle payment webhook içinde yapılır.

7. Frontend Entegrasyonu (Client Component)


    'use client'

    import { useState } from 'react'

    export default function CouponInput({ total }: { total: number }) {
        const [code, setCode] = useState('')
        const [price, setPrice] = useState(total)

        const applyCoupon = async () => {
            const res = await fetch('/api/coupon/validate', {
                method: 'POST',
                body: JSON.stringify({ code, total })
            })

            const data = await res.json()

            if (res.ok) {
                setPrice(data.discountedTotal)
            }
        }

        return (
            <div>
                <input value={code} onChange={e => setCode(e.target.value)} />
                <button onClick={applyCoupon}>Uygula</button>
                <p>Toplam: {price} ₺</p>
            </div>
        )
    }

8. Kampanya & Kupon Çakışmasını Önleme

  • Kampanya varsa kuponu devre dışı bırak,
  • Sadece en yüksek indirimi uygula.

Sonuç

Bu makalemde güvenli, ölçeklenebilir ve gerçek projelere uygun bir kampanya & kupon sistemi tasarlamaya çalıştım. Bu yapı; e-ticaret, SaaS ve üyelik tabanlı sistemlerde kullanılabilir ve ya genişletilebilir.


19 Ara 2025
Yorum