feat: promo codes UI + apply on /billing

AdminPromos.js: создание/список/toggle/удаление промокодов
  auto-generated code, type (credits/%), max_uses, expires, description
AdminPanel: раздел Промокоды между Тарифами и Пользователями
/billing page: кнопка '🎁 Есть промокод?' → форма ввода → apply-promo API
API routes: /api/admin/promos, /api/admin/promos/[id], /api/billing/apply-promo
This commit is contained in:
Ник (Claude)
2026-06-13 09:37:19 +03:00
parent e5f6662aed
commit b620927c25
6 changed files with 353 additions and 1 deletions
+26
View File
@@ -0,0 +1,26 @@
import { NextResponse } from 'next/server';
import { requireUser } from '@/lib/session';
const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
const ENGINE_SECRET = process.env.ENGINE_SECRET || '';
const h = (uid) => ({ 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(uid) });
export async function PATCH(req, { params }) {
const user = await requireUser();
if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const body = await req.json();
const res = await fetch(`${ENGINE_URL}/api/admin/promos/${params.id}`, {
method: 'PATCH', headers: { ...h(user.id), 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return NextResponse.json(await res.json());
}
export async function DELETE(req, { params }) {
const user = await requireUser();
if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const res = await fetch(`${ENGINE_URL}/api/admin/promos/${params.id}`, {
method: 'DELETE', headers: h(user.id),
});
return NextResponse.json(await res.json());
}
+25
View File
@@ -0,0 +1,25 @@
import { NextResponse } from 'next/server';
import { requireUser } from '@/lib/session';
const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
const ENGINE_SECRET = process.env.ENGINE_SECRET || '';
const h = (uid) => ({ 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(uid) });
export async function GET(req) {
const user = await requireUser();
if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const res = await fetch(`${ENGINE_URL}/api/admin/promos`, { headers: h(user.id) });
return NextResponse.json(await res.json());
}
export async function POST(req) {
const user = await requireUser();
if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const body = await req.json();
const res = await fetch(`${ENGINE_URL}/api/admin/promos`, {
method: 'POST',
headers: { ...h(user.id), 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return NextResponse.json(await res.json(), { status: res.status });
}
+17
View File
@@ -0,0 +1,17 @@
import { NextResponse } from 'next/server';
import { requireUser } from '@/lib/session';
const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
const ENGINE_SECRET = process.env.ENGINE_SECRET || '';
export async function POST(req) {
const user = await requireUser();
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = await req.json();
const res = await fetch(`${ENGINE_URL}/api/billing/apply-promo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(user.id) },
body: JSON.stringify(body),
});
return NextResponse.json(await res.json(), { status: res.status });
}