Files
app_umbrix/PRICING-LOCATION-ANALYSIS.md

1223 lines
45 KiB
Markdown
Raw Normal View History

# 🎯 Анализ: Система тарифов с выбором локаций + скидки за период
## 📊 Текущее состояние
### Что есть сейчас:
1.**Тарифы** определены в `/app/plans/page.tsx`:
- Trial (7 дней бесплатно)
- Start (100₽/мес, 1 локация)
- Plus (299₽/мес, 3 локации)
- Max (350₽/мес, 15+ локаций)
2.**SetupWizard** (`/components/SetupWizard.tsx`):
- Есть UI для выбора локаций
- Но используется ПОСЛЕ покупки (в wizard'е настройки)
- НЕ влияет на создание пользователя
3.**API создания пользователя** (`/app/api/create-user/route.ts`):
- Создает пользователя с фиксированными inbounds
- НЕТ привязки к нодам/локациям
- Используется hardcoded список inbounds:
```typescript
inbounds: {
vless: ['VLESS TCP', 'VLESS Reality'],
vmess: ['VMess WS'],
trojan: ['Trojan TCP']
}
```
4.**Что НЕ работает**:
- Выбор локаций НЕ сохраняется в Marzban
- Нет связи между тарифом и доступными нодами
- Нет скидок за длительные периоды (3 мес, 6 мес, год)
---
## 🔍 Анализ Marzban API
### Доступные endpoints (из https://panel.umbrix.net/docs):
#### 1. **Управление нодами** (`/api/nodes`):
```bash
GET /api/nodes # Список всех нод с их параметрами
GET /api/node/{node_id} # Детали конкретной ноды
POST /api/node # Создать ноду
PUT /api/node/{node_id} # Изменить ноду
DELETE /api/node/{node_id} # Удалить ноду
GET /api/nodes/usage # Статистика по нодам
```
**Структура ноды** (`NodeResponse`):
```json
{
"id": 1,
"name": "🇺🇸 USA Node",
"address": "194.113.210.187",
"port": 62050,
"api_port": 62051,
"status": "connected",
"message": null,
"xray_version": "1.8.21",
"usage": {
"uplink": 123456789,
"downlink": 987654321
}
}
```
#### 2. **Inbounds** (`/api/inbounds`):
```bash
GET /api/inbounds # Список всех inbounds (протоколов)
```
**Структура inbound**:
```json
{
"tag": "VLESS TCP",
"protocol": "vless",
"network": "tcp",
"tls": "reality",
"port": 443
}
```
#### 3. **Создание пользователя** (`POST /api/user`):
```json
{
"username": "test_user",
"status": "active",
"expire": 1735689600, // Unix timestamp
"data_limit": 107374182400, // Bytes
"data_limit_reset_strategy": "no_reset",
"proxies": {
"vmess": {"id": "uuid-here"},
"vless": {},
"trojan": {}
},
"inbounds": {
"vless": ["VLESS TCP", "VLESS Reality"],
"vmess": ["VMess WS"],
"trojan": ["Trojan TCP"]
},
"note": "Тариф Plus, TG: 12345"
}
```
---
## 🎨 Новая воронка с выбором локаций
### Идеальный User Flow:
```
1. Пользователь открывает /plans
2. Выбирает тариф (Start/Plus/Max)
3. Видит варианты периода оплаты:
┌─────────────────────────────────────┐
│ ⭕ 1 месяц - 299₽ (без скидки) │
│ ⭕ 3 месяца - 807₽ (-10% = 897₽) │
│ ⭕ 6 месяцев - 1523₽ (-15% = 1794₽)│
│ ⭕ 1 год - 2870₽ (-20% = 3588₽)│
└─────────────────────────────────────┘
4. Выбирает локации (в зависимости от тарифа):
┌─────────────────────────────────────┐
│ Start: 1 локация из списка │
│ Plus: 3 локации из списка │
│ Max: Все локации │
└─────────────────────────────────────┘
Список локаций (из Marzban API):
☐ 🇳🇱 Нидерланды (ping: 15ms)
☐ 🇩🇪 Германия (ping: 20ms)
☐ 🇺🇸 США (ping: 120ms)
☐ 🇸🇬 Сингапур (ping: 180ms)
☑ 🇯🇵 Япония (ping: 160ms)
☐ 🇬🇧 Великобритания (ping: 35ms)
5. Нажимает "Оплатить" → Payment gateway
6. После оплаты → API создает пользователя с выбранными локациями
```
---
## 🚀 Техническая реализация
### **Этап 1: Получить список нод из Marzban**
Создать новый API endpoint `/api/nodes`:
```typescript
// app/api/nodes/route.ts
import { NextResponse } from 'next/server';
const MARZBAN_API = process.env.MARZBAN_PANEL_URL;
const ADMIN_USERNAME = process.env.MARZBAN_ADMIN_USERNAME;
const ADMIN_PASSWORD = process.env.MARZBAN_ADMIN_PASSWORD;
export async function GET() {
try {
// 1. Получить токен админа
const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=password&username=${ADMIN_USERNAME}&password=${ADMIN_PASSWORD}`,
});
const { access_token } = await tokenResponse.json();
// 2. Получить список нод
const nodesResponse = await fetch(`${MARZBAN_API}/api/nodes`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const nodes = await nodesResponse.json();
// 3. Форматировать для фронтенда
const locations = nodes
.filter(node => node.status === 'connected')
.map(node => ({
id: node.id,
name: node.name, // "🇺🇸 USA Node"
address: node.address,
ping: calculatePing(node.address), // TODO: реальный ping
country: extractCountry(node.name), // "US"
}));
return NextResponse.json({ success: true, locations });
} catch (error) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}
function extractCountry(nodeName: string): string {
// Извлечь код страны из emoji флага
const match = nodeName.match(/🇦-🇿{2}/);
return match ? getFlagCode(match[0]) : 'Unknown';
}
function calculatePing(address: string): string {
// TODO: Реальный ping check (или хардкод по регионам)
const mockPings = {
'194.113.210.187': '120ms', // USA
'193.168.175.128': '15ms', // NL
// ...
};
return mockPings[address] || '50ms';
}
```
---
### **Этап 2: Страница выбора тарифа с периодом**
Обновить `/app/plans/page.tsx`:
```tsx
'use client';
import { useState, useEffect } from 'react';
import { Check, ChevronRight } from 'lucide-react';
type Period = '1month' | '3months' | '6months' | '1year';
type PlanType = 'start' | 'plus' | 'max';
interface Location {
id: number;
name: string;
country: string;
ping: string;
}
export default function Plans() {
const [selectedPlan, setSelectedPlan] = useState<PlanType | null>(null);
const [selectedPeriod, setSelectedPeriod] = useState<Period>('1month');
const [selectedLocations, setSelectedLocations] = useState<number[]>([]);
const [availableLocations, setAvailableLocations] = useState<Location[]>([]);
// Загрузить локации из API
useEffect(() => {
async function fetchLocations() {
const response = await fetch('/api/nodes');
const data = await response.json();
if (data.success) {
setAvailableLocations(data.locations);
}
}
fetchLocations();
}, []);
// Тарифы с ценами за разные периоды
const plans = {
start: {
name: 'Start',
locations: 1,
prices: {
'1month': 100,
'3months': 270, // -10%
'6months': 510, // -15%
'1year': 960, // -20%
}
},
plus: {
name: 'Plus',
locations: 3,
prices: {
'1month': 299,
'3months': 807, // -10%
'6months': 1523, // -15%
'1year': 2870, // -20%
}
},
max: {
name: 'Max',
locations: 999, // Все
prices: {
'1month': 350,
'3months': 945, // -10%
'6months': 1785, // -15%
'1year': 3360, // -20%
}
}
};
const handlePlanSelect = (plan: PlanType) => {
setSelectedPlan(plan);
};
const handlePeriodSelect = (period: Period) => {
setSelectedPeriod(period);
};
const handleLocationToggle = (locationId: number) => {
if (!selectedPlan) return;
const maxLocations = plans[selectedPlan].locations;
if (selectedLocations.includes(locationId)) {
setSelectedLocations(selectedLocations.filter(id => id !== locationId));
} else if (maxLocations === 999 || selectedLocations.length < maxLocations) {
setSelectedLocations([...selectedLocations, locationId]);
}
};
const handlePurchase = async () => {
if (!selectedPlan || selectedLocations.length === 0) {
alert('Выберите тариф и хотя бы одну локацию');
return;
}
// Отправить на payment gateway
const response = await fetch('/api/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan: selectedPlan,
period: selectedPeriod,
locations: selectedLocations,
telegramId: getTelegramId(),
}),
});
const data = await response.json();
if (data.success) {
window.location.href = data.paymentUrl;
}
};
const getDiscountPercent = (period: Period): number => {
const discounts = {
'1month': 0,
'3months': 10,
'6months': 15,
'1year': 20,
};
return discounts[period];
};
const calculateSavings = (plan: PlanType, period: Period): number => {
const basePrices = plans[plan].prices;
const monthlyPrice = basePrices['1month'];
const months = { '1month': 1, '3months': 3, '6months': 6, '1year': 12 }[period];
const fullPrice = monthlyPrice * months;
const discountedPrice = basePrices[period];
return fullPrice - discountedPrice;
};
return (
<div className="min-h-screen bg-slate-900 text-white p-4">
<h1 className="text-2xl font-bold mb-6">Выбор тарифа</h1>
{/* Шаг 1: Выбор тарифа */}
{!selectedPlan && (
<div className="space-y-4">
{Object.entries(plans).map(([key, plan]) => (
<button
key={key}
onClick={() => handlePlanSelect(key as PlanType)}
className="w-full p-4 bg-slate-800 rounded-lg text-left hover:bg-slate-700"
>
<h3 className="text-xl font-bold">{plan.name}</h3>
<p className="text-slate-400">
{plan.locations === 999 ? 'Все локации' : `${plan.locations} локаций`}
</p>
<p className="text-blue-400 mt-2">От {plan.prices['1month']}₽/мес</p>
</button>
))}
</div>
)}
{/* Шаг 2: Выбор периода */}
{selectedPlan && !selectedLocations.length && (
<div className="space-y-4">
<button onClick={() => setSelectedPlan(null)} className="text-blue-400">← Назад</button>
<h2 className="text-xl font-bold mb-4">Выберите период</h2>
{Object.entries(plans[selectedPlan].prices).map(([period, price]) => {
const discount = getDiscountPercent(period as Period);
const savings = calculateSavings(selectedPlan, period as Period);
return (
<button
key={period}
onClick={() => {
handlePeriodSelect(period as Period);
// Переходим к выбору локаций
}}
className="w-full p-4 bg-slate-800 rounded-lg text-left hover:bg-slate-700"
>
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-bold">
{period === '1month' && '1 месяц'}
{period === '3months' && '3 месяца'}
{period === '6months' && '6 месяцев'}
{period === '1year' && '1 год'}
</h3>
{discount > 0 && (
<p className="text-green-400 text-sm">
💰 Экономия {savings}₽ (-{discount}%)
</p>
)}
</div>
<div className="text-right">
<p className="text-2xl font-bold">{price}₽</p>
{discount > 0 && (
<p className="text-slate-400 line-through text-sm">
{plans[selectedPlan].prices['1month'] *
({ '3months': 3, '6months': 6, '1year': 12 }[period] || 1)}₽
</p>
)}
</div>
</div>
</button>
);
})}
</div>
)}
{/* Шаг 3: Выбор локаций */}
{selectedPlan && selectedPeriod && (
<div className="space-y-4">
<button onClick={() => setSelectedPeriod('1month')} className="text-blue-400">← Назад</button>
<h2 className="text-xl font-bold mb-2">Выберите локации</h2>
<p className="text-slate-400 mb-4">
{plans[selectedPlan].locations === 999
? 'Доступны все локации'
: `Выберите до ${plans[selectedPlan].locations} локаций`}
</p>
<div className="space-y-2">
{availableLocations.map(location => (
<button
key={location.id}
onClick={() => handleLocationToggle(location.id)}
disabled={
!selectedLocations.includes(location.id) &&
plans[selectedPlan].locations !== 999 &&
selectedLocations.length >= plans[selectedPlan].locations
}
className={`w-full p-4 rounded-lg border transition-colors ${
selectedLocations.includes(location.id)
? 'bg-blue-600 border-blue-400'
: 'bg-slate-800 border-slate-700 hover:border-slate-600'
}`}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
{selectedLocations.includes(location.id) && (
<Check className="w-5 h-5 text-white" />
)}
<span>{location.name}</span>
</div>
<span className="text-slate-400 text-sm">{location.ping}</span>
</div>
</button>
))}
</div>
{selectedLocations.length > 0 && (
<button
onClick={handlePurchase}
className="w-full py-4 bg-blue-600 hover:bg-blue-700 rounded-lg font-bold mt-6"
>
Перейти к оплате {plans[selectedPlan].prices[selectedPeriod]}₽
<ChevronRight className="inline ml-2" />
</button>
)}
</div>
)}
</div>
);
}
function getTelegramId() {
return (window as any).Telegram?.WebApp?.initDataUnsafe?.user?.id || null;
}
```
---
### **Этап 3: Обновить API создания пользователя**
Модифицировать `/app/api/create-user/route.ts`:
```typescript
export async function POST(request: NextRequest) {
const {
planType,
period, // NEW: '1month' | '3months' | '6months' | '1year'
locationIds, // NEW: [1, 3, 5] - выбранные ID нод
telegramId,
telegramUsername,
firstName,
lastName
} = await request.json();
// 1. Получить токен админа (same as before)
// 2. Определить параметры тарифа с учетом периода
const planConfig = getPlanConfig(planType, period);
// 3. Получить список нод и их inbounds
const nodesResponse = await fetch(`${MARZBAN_API}/api/nodes`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const allNodes = await nodesResponse.json();
// 4. Фильтровать только выбранные ноды
const selectedNodes = allNodes.filter(node => locationIds.includes(node.id));
// 5. Получить inbounds выбранных нод
const inboundsResponse = await fetch(`${MARZBAN_API}/api/inbounds`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const allInbounds = await inboundsResponse.json();
// 6. Создать mapping inbounds для выбранных нод
const userInbounds = {
vless: [],
vmess: [],
trojan: []
};
selectedNodes.forEach(node => {
// Добавить inbounds этой ноды в конфиг пользователя
allInbounds
.filter(inbound => inbound.node_id === node.id)
.forEach(inbound => {
if (inbound.protocol === 'vless') {
userInbounds.vless.push(inbound.tag);
} else if (inbound.protocol === 'vmess') {
userInbounds.vmess.push(inbound.tag);
} else if (inbound.protocol === 'trojan') {
userInbounds.trojan.push(inbound.tag);
}
});
});
// 7. Создать пользователя с кастомными inbounds
const createUserResponse = await fetch(`${MARZBAN_API}/api/user`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
status: 'active',
expire: planConfig.expireTimestamp,
data_limit: planConfig.dataLimitBytes,
data_limit_reset_strategy: 'no_reset',
proxies: {
vmess: { id: randomUUID() },
vless: {},
trojan: {}
},
inbounds: userInbounds, // <-- ГЛАВНОЕ ИЗМЕНЕНИЕ!
note: `Тариф: ${planConfig.name}, Период: ${period}, Локации: ${selectedNodes.map(n => n.name).join(', ')}, TG: ${telegramId}`,
}),
});
// Rest of the code...
}
function getPlanConfig(planType: PlanType, period: Period) {
const basePlans = {
trial: { name: 'Trial', dataLimitGB: 10, durationDays: 7 },
start: { name: 'Start', dataLimitGB: 50, basePriceRub: 100 },
plus: { name: 'Plus', dataLimitGB: 299, basePriceRub: 299 },
max: { name: 'Max', dataLimitGB: 999999, basePriceRub: 350 },
};
const periodMultipliers = {
'1month': { months: 1, discount: 0 },
'3months': { months: 3, discount: 0.10 },
'6months': { months: 6, discount: 0.15 },
'1year': { months: 12, discount: 0.20 },
};
const plan = basePlans[planType];
const periodConfig = periodMultipliers[period];
const durationDays = periodConfig.months * 30;
const fullPrice = plan.basePriceRub * periodConfig.months;
const discountedPrice = Math.round(fullPrice * (1 - periodConfig.discount));
return {
name: plan.name,
dataLimitBytes: plan.dataLimitGB * 1024 * 1024 * 1024,
expireTimestamp: Math.floor(Date.now() / 1000) + (durationDays * 24 * 60 * 60),
priceRub: discountedPrice,
periodDays: durationDays,
};
}
```
---
### **Этап 4: Payment gateway integration**
Создать `/app/api/create-payment/route.ts`:
```typescript
// Интеграция с платежным шлюзом (Stripe / YooKassa / Coinbase)
export async function POST(request: NextRequest) {
const { plan, period, locations, telegramId } = await request.json();
const planConfig = getPlanConfig(plan, period);
// 1. Создать заказ в БД (pending payment)
const orderId = createOrder({
telegramId,
plan,
period,
locations,
amount: planConfig.priceRub,
status: 'pending',
});
// 2. Создать платежную ссылку
const paymentUrl = await createStripeCheckout({
amount: planConfig.priceRub,
currency: 'RUB',
metadata: { orderId, telegramId },
successUrl: `https://app.umbrix.net/payment-success?order=${orderId}`,
cancelUrl: `https://app.umbrix.net/plans`,
});
return NextResponse.json({ success: true, paymentUrl });
}
```
---
### **Этап 5: Webhook для обработки оплаты**
Создать `/app/api/payment-webhook/route.ts`:
```typescript
// Webhook от платежного провайдера
export async function POST(request: NextRequest) {
const payload = await request.json();
// 1. Verify signature (Stripe/YooKassa)
// 2. Проверить статус оплаты
if (payload.status === 'succeeded') {
const { orderId } = payload.metadata;
// 3. Получить данные заказа из БД
const order = getOrder(orderId);
// 4. Создать пользователя в Marzban
await fetch('/api/create-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
planType: order.plan,
period: order.period,
locationIds: order.locations,
telegramId: order.telegramId,
}),
});
// 5. Обновить статус заказа
updateOrder(orderId, { status: 'completed' });
// 6. Отправить уведомление в Telegram
sendTelegramMessage(order.telegramId, '✅ Оплата прошла! Ваш VPN активирован.');
}
return NextResponse.json({ success: true });
}
```
---
## 🤖 Обновление n8n бота
### Добавить новые шаги в workflow:
```json
{
"nodes": [
{
"name": "Show Plans Menu",
"type": "n8n-nodes-base.telegram",
"parameters": {
"chatId": "={{ $json.userId }}",
"text": "💎 Выберите тариф:",
"replyMarkup": {
"inline_keyboard": [
[{"text": "🌍 Start (100₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=start"}}],
[{"text": "🌎 Plus (299₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=plus"}}],
[{"text": "🌏 Max (350₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=max"}}]
]
}
}
}
]
}
```
**ВАЖНО**: Все тарифы теперь открывают Mini App с pre-selected планом → выбор периода → выбор локаций → оплата.
---
## 📋 Чеклист внедрения
### Неделя 1: Backend API
- [ ] Создать `/api/nodes` - список доступных локаций из Marzban
- [ ] Обновить `/api/create-user` - добавить параметры `period` и `locationIds`
- [ ] Добавить функцию `getPlanConfig(planType, period)` с расчетом скидок
- [ ] Реализовать mapping нод → inbounds для создания пользователя
### Неделя 2: Frontend UI
- [ ] Переработать `/app/plans/page.tsx` - мультишаговый выбор:
- Шаг 1: Тариф (Start/Plus/Max)
- Шаг 2: Период (1м/3м/6м/1год) с показом скидок
- Шаг 3: Локации (чекбоксы с ограничением по тарифу)
- [ ] Добавить компонент `PeriodSelector` с расчетом экономии
- [ ] Добавить компонент `LocationSelector` с ping и флагами стран
- [ ] Стилизация под Telegram Design System
### Неделя 3: Payment Integration
- [ ] Выбрать платежный шлюз (YooKassa для RU, Stripe для международных)
- [ ] Создать `/api/create-payment` - генерация платежных ссылок
- [ ] Создать `/api/payment-webhook` - обработка успешных оплат
- [ ] Создать БД таблицу `orders` для хранения pending платежей
- [ ] Добавить страницу `/payment-success` - редирект после оплаты
### Неделя 4: n8n Bot Update
- [ ] Добавить inline кнопки с pre-selected тарифами
- [ ] Обновить WebApp URLs с query параметрами `?plan=start`
- [ ] Добавить уведомления о успешной оплате
- [ ] Добавить команду `/renew` для продления подписки
### Неделя 5: Testing & QA
- [ ] E2E тесты полной воронки (выбор → оплата → создание пользователя)
- [ ] Тестирование на разных тарифах и периодах
- [ ] Проверка корректности создания inbounds для выбранных нод
- [ ] Load testing (100 одновременных покупок)
- [ ] Security audit payment webhook
---
## 💡 Кардинальные предложения
### **❌ Предложение 1: Динамическое ценообразование - ОТКЛОНЕНО**
~~Вместо фиксированных цен - учитывать нагрузку на ноды~~
**Причина отклонения**: Усложняет ценообразование для пользователей, все локации должны быть одинаковы по цене.
### **Предложение 2: Гибкие пакеты трафика**
Разделить тарифы на "Устройства" и "Трафик":
```
Base Plan (1 устройство):
├── 50 ГБ - 100₽/мес
├── 200 ГБ - 250₽/мес
└── ∞ ГБ - 350₽/мес
+100₽ за каждое доп. устройство
+50₽ за каждую доп. локацию
```
### **⚠️ Предложение 3: Семейные планы - ТРЕБУЕТ АНАЛИЗА**
```
Family Plan (5 устройств, безлимит):
- 499₽/мес для семьи
- Каждый член семьи выбирает свои локации
- Общий аккаунт с sub-users в Marzban
```
**Проблема**: Marzban не имеет встроенного механизма ограничения количества устройств на уровне API.
**Варианты решения**:
#### Вариант 1: Мониторинг `online_at` (рекомендуемый)
```typescript
// GET /api/user/{username} возвращает:
{
"online_at": "2026-02-08T10:30:00Z", // Последняя активность
"sub_last_user_agent": "v2rayNG/1.8.5"
}
// Логика:
// - online_at обновляется при каждом подключении
// - Если разница между текущим временем и online_at < 5 минут - устройство активно
// - Подсчитываем активные сессии через периодический poll
```
**Ограничения**:
-НЕТ прямого поля `active_connections` или `device_count`
-НЕТ поля `ip_limit` в UserCreate
-ЕСТЬ `online_at` - последняя активность
-ЕСТЬ `sub_last_user_agent` - информация об устройстве
**Реализация** (см. детали ниже в разделе "Семейные планы: Детальный анализ")
#### Вариант 2: Ограничение на уровне Xray (требует кастомных патчей)
- Модифицировать `xray_config.json` с ограничением по IP
- Требует fork Marzban с кастомной логикой
- **НЕ рекомендуется** для MVP
#### Вариант 3: IP-based limiting (приблизительный)
- Собирать статистику подключений через `/api/user/{username}/usage`
- Отслеживать уникальные IP адреса
- Ограничение: один пользователь может иметь несколько устройств с одним IP (домашняя сеть)
**Решение**: Отложить семейные планы на V2.0 до реализации мониторинга устройств.
### **✅ Предложение 4: Gamification - УЖЕ РЕАЛИЗОВАНО**
**Текущая система рефералов**:
```typescript
// /api/referral/track - отслеживание приглашений
// /api/referral/stats - статистика пользователя
Бонусы:
- +7 дней за каждого друга
- +30 дней milestone bonus за каждые 5 друзей
Формула:
bonusDays = (referralCount * 7) + (Math.floor(referralCount / 5) * 30)
Примеры:
1 друг → +7 дней
5 друзей → +35 дней (+30 milestone)
10 друзей → +70 дней (+60 milestone) = 130 дней
```
**Что можно улучшить**:
- Добавить визуальные badges (Bronze/Silver/Gold)
- Добавить leaderboard на странице `/referral`
- Уведомления в Telegram при достижении milestone
---
## 🔥 Критические улучшения воронки
### **1. A/B Testing периодов оплаты**
```typescript
// Показывать разным пользователям разные default периоды
const defaultPeriod = userId % 2 === 0 ? '3months' : '6months';
// Трекинг конверсии:
// - Какой период чаще выбирают?
// - Какая скидка конвертирует лучше?
```
### **2. Urgency триггеры**
```
⏰ Специальное предложение!
🔥 -30% на годовую подписку (осталось 2 часа)
💰 При оплате сегодня - бонус +7 дней
```
### **3. Social proof**
```
✅ Уже 1,234 пользователей выбрали тариф Plus
⭐ 4.9/5 - средняя оценка
🌍 Топ-3 локации: 🇳🇱 🇩🇪 🇺🇸
```
### **4. Упрощенная воронка для новичков**
```
Вместо 3 шагов (тариф → период → локации):
"Быстрый старт" кнопка:
→ Plus (3 месяца) + Авто-выбор 3 лучших локаций по ping
→ Сразу на оплату (экономия 807₽ вместо 897₽)
```
---
## 🎯 Priority Roadmap
### **MVP (2 недели)**:
1. ✅ API `/api/nodes` - список локаций
2. ✅ Обновить `/app/plans/page.tsx` - мультишаговый выбор
3. ✅ Обновить `/api/create-user` - кастомные inbounds
4. ⚠️ YooKassa integration (payment gateway)
### **V1.1 (4 недели)**:
5. Скидки за период (3м/6м/1год)
6. Показ экономии при выборе периода
7. n8n bot updates
### **V1.2 (6 недель)**:
8. ~~Динамическое ценообразование~~ ❌ ОТКЛОНЕНО
9. A/B testing периодов
10. Analytics dashboard (конверсия по воронке)
### **V2.0 (8 недель)**:
11. **Device Monitor Service** - мониторинг устройств через Xray логи
12. Семейные планы (после реализации п.11)
13. Gamification улучшения (badges, leaderboard)
14. Автоматическое продление подписки
---
## ❓ Вопросы для обсуждения
1. **Какой payment gateway использовать?**
- YooKassa (для РФ) ✅
- Stripe (международный) ✅
- Crypto (Coinbase Commerce) ?
2. **Какие скидки оптимальны?**
- 3 мес: -10% ✅
- 6 мес: -15% ✅
- 1 год: -20% ✅
3. **~~Нужна ли локация-специфичная цена?~~** ❌ ОТКЛОНЕНО
- Все локации одинаковы ✅
4. **Trial период - как ограничить abuse?**
- 1 trial на Telegram ID ✅
- Требовать номер телефона? ⚠️
- Требовать payment method (0₽ charge)? ⚠️
5. **Семейные планы - реализовывать в MVP?**
- ❌ Отложить на V2.0
- ⚠️ Требует Device Monitor Service (40 часов разработки)
- ✅ Для MVP - добавить в ToS ограничение без технического enforcement
---
## 📞 Следующие шаги
1. ✅ Проанализировать текущее состояние
2. ✅ Изучить Marzban API
3. ✅ Спроектировать новую воронку
4. ✅ Проанализировать ограничения устройств
5. ✅ Изучить реферальную систему
6.**Согласовать со stakeholder'ами**
7.**Начать разработку MVP**
**Готово к обсуждению!** 🚀
---
## 🔬 ДОПОЛНЕНИЕ: Семейные планы - Детальный анализ ограничения устройств
### Проблема:
Marzban **НЕ имеет встроенного механизма ограничения количества одновременных подключений** (devices/connections).
### Анализ Marzban API:
#### ❌ Что НЕ доступно в API:
```json
// POST /api/user - UserCreate schema НЕ имеет:
{
"ip_limit": 5, // ❌ Нет такого поля
"device_limit": 5, // ❌ Нет такого поля
"max_connections": 5, // ❌ Нет такого поля
"active_sessions": [] // ❌ Нет такого поля
}
```
#### ✅ Что ДОСТУПНО в API:
```json
// GET /api/user/{username} - UserResponse:
{
"username": "test_user",
"status": "active",
"online_at": "2026-02-08T10:30:00Z", // ✅ Последняя активность
"sub_last_user_agent": "v2rayNG/1.8.5", // ✅ User-Agent устройства
"used_traffic": 123456789,
"data_limit": 107374182400
}
// GET /api/user/{username}/usage - UsageResponse:
{
"username": "test_user",
"usages": [
{
"date": "2026-02-08",
"upload": 12345,
"download": 67890,
"total": 80235
}
]
}
```
### Варианты решения:
---
### **Вариант 1: Server-side мониторинг через `online_at` (Рекомендуемый для MVP)**
**Как работает**:
1. Периодически (каждые 30 секунд) опрашиваем `GET /api/user/{username}`
2. Проверяем `online_at` - если разница < 2 минуты → устройство активно
3. Если активных устройств > лимита → временно блокируем (`status: 'disabled'`)
**Реализация**:
```typescript
// app/api/family-plans/monitor-devices/route.ts
import { NextRequest, NextResponse } from 'next/server';
const MARZBAN_API = process.env.MARZBAN_PANEL_URL;
const ADMIN_USERNAME = process.env.MARZBAN_ADMIN_USERNAME;
const ADMIN_PASSWORD = process.env.MARZBAN_ADMIN_PASSWORD;
interface FamilyPlanUser {
username: string;
deviceLimit: number;
lastCheck: Date;
}
// В production - хранить в Redis или БД
const familyPlanUsers: Map<string, FamilyPlanUser> = new Map();
// Cron job - запускать каждые 30 секунд
export async function POST(request: NextRequest) {
try {
// 1. Получить токен админа
const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=password&username=${ADMIN_USERNAME}&password=${ADMIN_PASSWORD}`,
});
const { access_token } = await tokenResponse.json();
// 2. Проверить каждого пользователя Family Plan
for (const [username, config] of familyPlanUsers.entries()) {
const userResponse = await fetch(`${MARZBAN_API}/api/user/${username}`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const user = await userResponse.json();
// 3. Проверить активность
const onlineAt = new Date(user.online_at);
const now = new Date();
const minutesSinceActive = (now.getTime() - onlineAt.getTime()) / 1000 / 60;
// Если пользователь активен (подключался менее 2 минут назад)
if (minutesSinceActive < 2) {
// ПРОБЛЕМА: Мы НЕ МОЖЕМ узнать сколько устройств подключено СЕЙЧАС!
// online_at показывает только ПОСЛЕДНЮЮ активность
// Вариант решения: собирать статистику подключений
// Но это требует доступа к логам Xray на уровне нод
}
}
return NextResponse.json({ success: true, message: 'Device monitoring completed' });
} catch (error) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}
```
**Проблема**: `online_at` показывает только **последнюю** активность, а НЕ **количество активных сессий**.
---
### **Вариант 2: IP-based мониторинг (Приблизительный)**
**Идея**: Отслеживать уникальные IP адреса через usage API.
**Проблема**:
- Несколько устройств могут быть за одним IP (домашний роутер)
- Один телефон может менять IP (мобильный интернет)
- **НЕ точный** метод
---
### **Вариант 3: Кастомизация Xray config (Профессиональный)**
**Идея**: Модифицировать `xray_config.json` для ограничения по IP.
```json
{
"inbounds": [
{
"tag": "VLESS TCP",
"protocol": "vless",
"settings": {
"clients": [
{
"id": "uuid-here",
"email": "test_user@marzban",
"level": 0,
"maxIps": 5 // ← Кастомное поле (требует патч Xray)
}
]
}
}
]
}
```
**Проблема**:
- Требует fork Xray с кастомной логикой
- Нужно пропатчить Marzban для поддержки `maxIps`
- **Сложно** для MVP
---
### **Вариант 4: Использовать sub-users (Встроенная функция Marzban)**
**Идея**: Создать **родительский аккаунт** + **4 дочерних аккаунта**.
```typescript
// Семейный план:
// 1. Создать главного пользователя: family_john_doe
// 2. Создать 4 sub-users:
// - family_john_doe_device_1
// - family_john_doe_device_2
// - family_john_doe_device_3
// - family_john_doe_device_4
// - family_john_doe_device_5
// Каждый sub-user = отдельное устройство с собственной подпиской
```
**Преимущества**:
- ✅ Работает "из коробки" в Marzban
- ✅ Каждое устройство = отдельная подписка
- ✅ Легко контролировать (5 username'ов)
**Недостатки**:
- ❌ Пользователю нужно выдавать 5 разных subscription URLs
- ❌ Неудобно для конечного пользователя
-Не настоящий "Family Plan" (просто 5 отдельных аккаунтов)
---
### **Рекомендация для V1.0**:
#### MVP подход:
1. **Отказаться от ограничения устройств на техническом уровне**
2. **Полагаться на честность пользователей**
3. **Добавить в ToS**: "Семейный план предназначен для использования в одной семье (до 5 устройств)"
4. **Мониторить аномальный трафик**: если один аккаунт генерирует 1TB/день - вручную проверить
#### V2.0 подход (после MVP):
1. Реализовать **Server-side webhook** для мониторинга Xray логов
2. Парсить логи подключений в реальном времени:
```bash
# Xray лог показывает:
[Info] accepted tcp:xxx.xxx.xxx.xxx:12345 [email:test_user@marzban]
```
3. Собирать статистику активных IP за последние 5 минут
4. Если > deviceLimit → отправить уведомление или временно заблокировать
---
### **Архитектура для V2.0**:
```
┌─────────────────────────────────────────┐
│ Marzban Panel (API) │
│ - Создание пользователей │
│ - Управление подписками │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ Xray Core (на каждой ноде) │
│ - Обработка трафика │
│ - Генерация логов подключений │
└────────────┬────────────────────────────┘
│ (Webhooks или log streaming)
┌─────────────────────────────────────────┐
│ Device Monitor Service (NEW!) │
│ - Парсинг логов Xray │
│ - Подсчет активных IP per user │
│ - Проверка лимитов устройств │
│ - Автоблокировка при превышении │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ PostgreSQL / Redis │
│ - Хранение активных сессий │
│ - Хранение семейных планов │
└─────────────────────────────────────────┘
```
**Стоимость разработки**: ~40 часов (Device Monitor Service)
---
### **Итоговое решение**:
| Вариант | Точность | Сложность | Время реализации | Рекомендация |
|---------|----------|-----------|------------------|--------------|
| 1. `online_at` мониторинг | ❌ Низкая | Средняя | 8 часов | ⚠️ Не точно |
| 2. IP-based | ❌ Низкая | Низкая | 4 часа | ⚠️ Не рекомендуется |
| 3. Xray патчи | ✅ Высокая | Очень высокая | 80+ часов | ❌ Слишком сложно |
| 4. Sub-users | ✅ Высокая | Средняя | 12 часов | ⚠️ Неудобно для пользователей |
| 5. **Без ограничений + ToS** | N/A | Очень низкая | 0 часов | ✅ **Для MVP** |
| 6. Log-based monitor (V2.0) | ✅ Высокая | Высокая | 40 часов | ✅ **Для V2.0** |
**Вывод**: Для MVP - **НЕ реализовывать техническое ограничение устройств**. Добавить в ToS и мониторить аномальный трафик вручную.
Для V2.0 - разработать Device Monitor Service с парсингом Xray логов.
**Готово к обсуждению!** 🚀