improvement for the backend api

This commit is contained in:
ivannoskov
2025-11-19 12:56:04 +03:00
parent b0a9934220
commit d4672ea32b
40 changed files with 2383 additions and 3313 deletions

View File

@@ -1,4 +1,4 @@
import { TargetChannel, TargetChannelDetail } from "@/lib/types/api";
import { TargetChannel } from "@/lib/types/api";
// ============================================================================
// Mock Target Channels Data
@@ -6,74 +6,24 @@ import { TargetChannel, TargetChannelDetail } from "@/lib/types/api";
export const mockTargetChannels: TargetChannel[] = [
{
id: "tc1",
id: "550e8400-e29b-41d4-a716-446655440010",
telegram_id: -1001234567890,
title: "Мой Главный Канал",
username: "my_main_channel",
description: "Основной канал для продвижения продукта",
is_active: true,
created_at: "2024-01-10T10:00:00.000Z",
updated_at: "2024-01-10T10:00:00.000Z",
total_purchases: 15,
total_subscriptions: 1250,
avg_cpf: 45.5,
},
{
id: "tc2",
id: "550e8400-e29b-41d4-a716-446655440011",
telegram_id: -1009876543210,
title: "Тестовый Канал",
username: "test_channel_promo",
description: "Канал для тестирования рекламных креативов",
is_active: true,
created_at: "2024-02-15T12:00:00.000Z",
updated_at: "2024-02-15T12:00:00.000Z",
total_purchases: 8,
total_subscriptions: 450,
avg_cpf: 52.3,
},
{
id: "tc3",
id: "550e8400-e29b-41d4-a716-446655440012",
telegram_id: -1005555555555,
title: "Новости и Обновления",
username: null,
description: "Приватный канал для новостей",
is_active: false,
created_at: "2024-03-01T08:00:00.000Z",
updated_at: "2024-03-20T14:30:00.000Z",
total_purchases: 3,
total_subscriptions: 120,
avg_cpf: 67.8,
},
];
export const getMockTargetChannelDetail = (
id: string
): TargetChannelDetail | null => {
const channel = mockTargetChannels.find((c) => c.id === id);
if (!channel) return null;
return {
...channel,
recent_purchases: [], // Will be populated from purchases mock
stats_by_period: [
{
period: "day",
subscriptions: 25,
cost: 1200,
cpf: 48.0,
},
{
period: "week",
subscriptions: 180,
cost: 8500,
cpf: 47.2,
},
{
period: "month",
subscriptions: 750,
cost: 35000,
cpf: 46.7,
},
],
};
};

View File

@@ -1,4 +1,4 @@
import { Creative, CreativeDetail } from "@/lib/types/api";
import { Creative } from "@/lib/types/api";
import { mockTargetChannels } from "./channels";
// ============================================================================
@@ -11,61 +11,39 @@ export const mockCreatives: Creative[] = [
name: 'Креатив "Присоединяйся"',
text: "🔥 Присоединяйся к нашему сообществу!\n\nУзнавай первым о новых возможностях и фишках.\n\n👉 {link}",
target_channel_id: "tc1",
is_archived: false,
target_channel_title: mockTargetChannels[0].title,
created_at: "2024-01-15T10:00:00.000Z",
updated_at: "2024-01-15T10:00:00.000Z",
target_channel: mockTargetChannels[0],
total_purchases: 10,
total_subscriptions: 850,
avg_cpf: 42.3,
status: "active",
placements_count: 10,
},
{
id: "cr2",
name: 'Креатив "Эксклюзив"',
text: "⭐ Эксклюзивный контент только для подписчиков!\n\nНе упусти возможность быть в курсе всех трендов.\n\nПереходи: {link}",
target_channel_id: "tc1",
is_archived: false,
target_channel_title: mockTargetChannels[0].title,
created_at: "2024-01-20T12:00:00.000Z",
updated_at: "2024-01-20T12:00:00.000Z",
target_channel: mockTargetChannels[0],
total_purchases: 5,
total_subscriptions: 400,
avg_cpf: 48.5,
status: "active",
placements_count: 5,
},
{
id: "cr3",
name: 'Креатив "Обучение"',
text: "📚 Хочешь научиться зарабатывать больше?\n\nПодписывайся на канал с проверенными методиками!\n\n{link}",
target_channel_id: "tc2",
is_archived: false,
target_channel_title: mockTargetChannels[1].title,
created_at: "2024-02-05T09:00:00.000Z",
updated_at: "2024-02-05T09:00:00.000Z",
target_channel: mockTargetChannels[1],
total_purchases: 8,
total_subscriptions: 450,
avg_cpf: 52.3,
status: "active",
placements_count: 8,
},
{
id: "cr4",
name: "Старый креатив (архив)",
text: "Устаревший текст с предложением. {link}",
target_channel_id: "tc1",
is_archived: true,
target_channel_title: mockTargetChannels[0].title,
created_at: "2023-12-01T10:00:00.000Z",
updated_at: "2024-01-10T15:00:00.000Z",
target_channel: mockTargetChannels[0],
total_purchases: 2,
total_subscriptions: 80,
avg_cpf: 75.0,
status: "archived",
placements_count: 2,
},
];
export const getMockCreativeDetail = (id: string): CreativeDetail | null => {
const creative = mockCreatives.find((c) => c.id === id);
if (!creative) return null;
return {
...creative,
purchases: [], // Will be populated from purchases mock
};
};

View File

@@ -1,4 +1,4 @@
import { ExternalChannel, ExternalChannelDetail } from "@/lib/types/api";
import { ExternalChannel } from "@/lib/types/api";
// ============================================================================
// Mock External Channels Data
@@ -6,90 +6,43 @@ import { ExternalChannel, ExternalChannelDetail } from "@/lib/types/api";
export const mockExternalChannels: ExternalChannel[] = [
{
id: "ec1",
id: "550e8400-e29b-41d4-a716-446655440020",
telegram_id: -1001111111111,
title: "Канал о Маркетинге",
username: "marketing_pro",
link: "https://t.me/marketing_pro",
subscribers_count: 50000,
description: "Лучшие практики маркетинга",
created_at: "2024-01-05T10:00:00.000Z",
updated_at: "2024-01-05T10:00:00.000Z",
target_channels: ["tc1", "tc2"],
total_purchases: 5,
avg_cpf: 42.5,
avg_cpm: 850.0,
},
{
id: "ec2",
id: "550e8400-e29b-41d4-a716-446655440021",
telegram_id: -1002222222222,
title: "Бизнес и Стартапы",
username: "business_startups",
link: "https://t.me/business_startups",
subscribers_count: 120000,
description: "Новости мира бизнеса",
created_at: "2024-01-08T11:00:00.000Z",
updated_at: "2024-01-08T11:00:00.000Z",
target_channels: ["tc1"],
total_purchases: 8,
avg_cpf: 55.8,
avg_cpm: 1200.0,
},
{
id: "ec3",
id: "550e8400-e29b-41d4-a716-446655440022",
telegram_id: -1003333333333,
title: "IT и Технологии",
username: "it_tech_news",
link: "https://t.me/it_tech_news",
subscribers_count: 85000,
description: "Все о новых технологиях",
created_at: "2024-01-12T09:00:00.000Z",
updated_at: "2024-01-12T09:00:00.000Z",
target_channels: ["tc1", "tc2"],
total_purchases: 3,
avg_cpf: 38.2,
avg_cpm: 750.0,
},
{
id: "ec4",
telegram_id: null,
id: "550e8400-e29b-41d4-a716-446655440023",
telegram_id: -1004444444444,
title: "Крипто Инвестиции",
username: "crypto_invest",
link: "https://t.me/crypto_invest",
subscribers_count: 200000,
description: null,
created_at: "2024-02-01T10:00:00.000Z",
updated_at: "2024-02-01T10:00:00.000Z",
target_channels: ["tc2"],
total_purchases: 2,
avg_cpf: 68.5,
avg_cpm: 1500.0,
},
{
id: "ec5",
id: "550e8400-e29b-41d4-a716-446655440024",
telegram_id: -1005555555555,
title: "Продажи и Переговоры",
username: "sales_expert",
link: "https://t.me/sales_expert",
subscribers_count: 35000,
description: "Экспертиза в продажах",
created_at: "2024-02-10T14:00:00.000Z",
updated_at: "2024-02-10T14:00:00.000Z",
target_channels: ["tc1"],
total_purchases: 4,
avg_cpf: 45.0,
avg_cpm: 900.0,
},
];
export const getMockExternalChannelDetail = (
id: string
): ExternalChannelDetail | null => {
const channel = mockExternalChannels.find((c) => c.id === id);
if (!channel) return null;
return {
...channel,
purchases: [], // Will be populated from purchases mock
};
};

View File

@@ -0,0 +1,213 @@
import { Placement, Subscription, ViewsSnapshot } from "@/lib/types/api";
import { mockTargetChannels } from "./channels";
import { mockExternalChannels } from "./external-channels";
import { mockCreatives } from "./creatives";
// ============================================================================
// Mock Placements Data
// ============================================================================
export const mockPlacements: Placement[] = [
{
id: "p1",
target_channel_id: "tc1",
target_channel_title: mockTargetChannels[0].title,
external_channel_id: "ec1",
external_channel_title: mockExternalChannels[0].title,
creative_id: "cr1",
creative_name: mockCreatives[0].name,
placement_date: "2024-03-15T10:00:00.000Z",
cost: 5000,
comment: "Тестовая закупка на утро",
ad_post_url: "https://t.me/marketing_pro/1234",
invite_link_type: "approval",
invite_link: "https://t.me/+AbCdEfGhIjKlMnOp",
status: "active",
subscriptions_count: 125,
views_count: 12500,
views_availability: "available",
last_views_fetch_at: "2024-03-16T10:00:00.000Z",
created_at: "2024-03-14T15:00:00.000Z",
},
{
id: "p2",
target_channel_id: "tc1",
target_channel_title: mockTargetChannels[0].title,
external_channel_id: "ec2",
external_channel_title: mockExternalChannels[1].title,
creative_id: "cr2",
creative_name: mockCreatives[1].name,
placement_date: "2024-03-18T14:00:00.000Z",
cost: 12000,
comment: "Большой канал, ожидаем хороших результатов",
ad_post_url: "https://t.me/business_startups/5678",
invite_link_type: "approval",
invite_link: "https://t.me/+QrStUvWxYzAbCdEf",
status: "active",
subscriptions_count: 210,
views_count: 28000,
views_availability: "available",
last_views_fetch_at: "2024-03-19T10:00:00.000Z",
created_at: "2024-03-17T10:00:00.000Z",
},
{
id: "p3",
target_channel_id: "tc2",
target_channel_title: mockTargetChannels[1].title,
external_channel_id: "ec3",
external_channel_title: mockExternalChannels[2].title,
creative_id: "cr3",
creative_name: mockCreatives[2].name,
placement_date: "2024-03-20T16:00:00.000Z",
cost: 3500,
comment: null,
ad_post_url: "https://t.me/it_tech_news/9101",
invite_link_type: "public",
invite_link: "https://t.me/+GhIjKlMnOpQrStUv",
status: "active",
subscriptions_count: 75,
views_count: 8500,
views_availability: "available",
last_views_fetch_at: "2024-03-21T10:00:00.000Z",
created_at: "2024-03-19T12:00:00.000Z",
},
{
id: "p4",
target_channel_id: "tc1",
target_channel_title: mockTargetChannels[0].title,
external_channel_id: "ec1",
external_channel_title: mockExternalChannels[0].title,
creative_id: "cr1",
creative_name: mockCreatives[0].name,
placement_date: "2024-03-22T08:00:00.000Z",
cost: 4500,
comment: "Повтор в этом же канале",
ad_post_url: "https://t.me/marketing_pro/1290",
invite_link_type: "approval",
invite_link: "https://t.me/+XyZaBcDeFgHiJkLm",
status: "active",
subscriptions_count: 95,
views_count: null,
views_availability: "unavailable",
last_views_fetch_at: "2024-03-23T09:00:00.000Z",
created_at: "2024-03-21T14:00:00.000Z",
},
{
id: "p5",
target_channel_id: "tc1",
target_channel_title: mockTargetChannels[0].title,
external_channel_id: "ec2",
external_channel_title: mockExternalChannels[1].title,
creative_id: "cr1",
creative_name: mockCreatives[0].name,
placement_date: "2024-02-10T10:00:00.000Z",
cost: 10000,
comment: "Старая закупка (архив)",
ad_post_url: "https://t.me/business_startups/4567",
invite_link_type: "public",
invite_link: "https://t.me/+OldLinkArch12345",
status: "archived",
subscriptions_count: 150,
views_count: 20000,
views_availability: "available",
last_views_fetch_at: "2024-02-12T10:00:00.000Z",
created_at: "2024-02-09T10:00:00.000Z",
},
];
// Keep old name for backward compatibility
export const mockPurchases = mockPlacements;
// ============================================================================
// Mock Subscriptions
// ============================================================================
export const mockSubscriptions: Subscription[] = [
{
id: "s1",
purchase_id: "p1",
telegram_user_id: 123456789,
user_first_name: "Иван",
user_last_name: "Иванов",
user_username: "ivan_ivanov",
subscribed_at: "2024-03-15T11:35:00.000Z",
is_active: true,
event_type: "join_request",
},
{
id: "s2",
purchase_id: "p1",
telegram_user_id: 987654321,
user_first_name: "Мария",
user_last_name: null,
user_username: "maria123",
subscribed_at: "2024-03-15T12:05:00.000Z",
is_active: true,
event_type: "join_request",
},
{
id: "s3",
purchase_id: "p1",
telegram_user_id: 456789123,
user_first_name: "Алексей",
user_last_name: "Петров",
user_username: null,
subscribed_at: "2024-03-15T13:20:00.000Z",
is_active: false,
event_type: "join_request",
},
{
id: "s4",
purchase_id: "p2",
telegram_user_id: 789123456,
user_first_name: "Елена",
user_last_name: "Сидорова",
user_username: "elena_s",
subscribed_at: "2024-03-18T14:30:00.000Z",
is_active: true,
event_type: "chat_member",
},
];
// ============================================================================
// Mock Views History
// ============================================================================
export const mockViewsHistory: ViewsSnapshot[] = [
{
id: "v1",
purchase_id: "p1",
views: 10000,
fetched_at: "2024-03-15T12:00:00.000Z",
},
{
id: "v2",
purchase_id: "p1",
views: 11500,
fetched_at: "2024-03-15T18:00:00.000Z",
},
{
id: "v3",
purchase_id: "p1",
views: 12500,
fetched_at: "2024-03-16T10:00:00.000Z",
},
{
id: "v4",
purchase_id: "p2",
views: 25000,
fetched_at: "2024-03-18T15:00:00.000Z",
},
{
id: "v5",
purchase_id: "p2",
views: 27000,
fetched_at: "2024-03-18T20:00:00.000Z",
},
{
id: "v6",
purchase_id: "p2",
views: 28000,
fetched_at: "2024-03-19T10:00:00.000Z",
},
];

View File

@@ -1,264 +0,0 @@
import {
Purchase,
PurchaseDetail,
Subscription,
ViewsSnapshot,
} from "@/lib/types/api";
import { mockTargetChannels } from "./channels";
import { mockExternalChannels } from "./external-channels";
import { mockCreatives } from "./creatives";
// ============================================================================
// Mock Purchases Data
// ============================================================================
export const mockPurchases: Purchase[] = [
{
id: "p1",
target_channel_id: "tc1",
external_channel_id: "ec1",
creative_id: "cr1",
scheduled_date: "2024-03-15T10:00:00.000Z",
actual_date: "2024-03-15T11:30:00.000Z",
cost: 5000,
comment: "Тестовая закупка на утро",
post_link: "https://t.me/marketing_pro/1234",
invite_link: "https://t.me/+AbCdEfGhIjKlMnOp",
invite_link_type: "public",
is_archived: false,
created_at: "2024-03-14T15:00:00.000Z",
updated_at: "2024-03-15T11:30:00.000Z",
target_channel: mockTargetChannels[0],
external_channel: mockExternalChannels[0],
creative: mockCreatives[0],
subscriptions_count: 125,
views_count: 12500,
cpf: 40.0,
cpm: 400.0,
conversion_rate: 1.0,
},
{
id: "p2",
target_channel_id: "tc1",
external_channel_id: "ec2",
creative_id: "cr2",
scheduled_date: "2024-03-18T14:00:00.000Z",
actual_date: "2024-03-18T14:15:00.000Z",
cost: 12000,
comment: "Большой канал, ожидаем хороших результатов",
post_link: "https://t.me/business_startups/5678",
invite_link: "https://t.me/+QrStUvWxYzAbCdEf",
invite_link_type: "public",
is_archived: false,
created_at: "2024-03-17T10:00:00.000Z",
updated_at: "2024-03-18T14:15:00.000Z",
target_channel: mockTargetChannels[0],
external_channel: mockExternalChannels[1],
creative: mockCreatives[1],
subscriptions_count: 210,
views_count: 28000,
cpf: 57.1,
cpm: 428.6,
conversion_rate: 0.75,
},
{
id: "p3",
target_channel_id: "tc2",
external_channel_id: "ec3",
creative_id: "cr3",
scheduled_date: "2024-03-20T16:00:00.000Z",
actual_date: null,
cost: 3500,
comment: null,
post_link: "https://t.me/it_tech_news/9101",
invite_link: "https://t.me/+GhIjKlMnOpQrStUv",
invite_link_type: "private",
is_archived: false,
created_at: "2024-03-19T12:00:00.000Z",
updated_at: "2024-03-19T12:00:00.000Z",
target_channel: mockTargetChannels[1],
external_channel: mockExternalChannels[2],
creative: mockCreatives[2],
subscriptions_count: 85,
views_count: 8500,
cpf: 41.2,
cpm: 411.8,
conversion_rate: 1.0,
},
{
id: "p4",
target_channel_id: "tc1",
external_channel_id: "ec5",
creative_id: "cr1",
scheduled_date: "2024-03-22T12:00:00.000Z",
actual_date: null,
cost: 4500,
comment: "Повторная закупка в том же канале",
post_link: null,
invite_link: "https://t.me/+WxYzAbCdEfGhIjKl",
invite_link_type: "public",
is_archived: false,
created_at: "2024-03-21T09:00:00.000Z",
updated_at: "2024-03-21T09:00:00.000Z",
target_channel: mockTargetChannels[0],
external_channel: mockExternalChannels[4],
creative: mockCreatives[0],
subscriptions_count: 95,
views_count: null,
cpf: 47.4,
cpm: null,
conversion_rate: null,
},
{
id: "p5",
target_channel_id: "tc1",
external_channel_id: "ec1",
creative_id: "cr1",
scheduled_date: "2024-02-10T10:00:00.000Z",
actual_date: "2024-02-10T10:30:00.000Z",
cost: 5500,
comment: "Старая закупка для истории",
post_link: "https://t.me/marketing_pro/1100",
invite_link: "https://t.me/+OldInviteLink123",
invite_link_type: "public",
is_archived: true,
created_at: "2024-02-09T15:00:00.000Z",
updated_at: "2024-02-20T10:00:00.000Z",
target_channel: mockTargetChannels[0],
external_channel: mockExternalChannels[0],
creative: mockCreatives[0],
subscriptions_count: 140,
views_count: 15000,
cpf: 39.3,
cpm: 366.7,
conversion_rate: 0.93,
},
];
// Mock Subscriptions
export const mockSubscriptions: Subscription[] = [
{
id: "s1",
purchase_id: "p1",
telegram_user_id: 111111111,
user_first_name: "Иван",
user_last_name: "Петров",
user_username: "user1",
subscribed_at: "2024-03-15T11:35:00.000Z",
is_active: true,
event_type: "chat_member",
},
{
id: "s2",
purchase_id: "p1",
telegram_user_id: 222222222,
user_first_name: "Мария",
user_last_name: "Сидорова",
user_username: "user2",
subscribed_at: "2024-03-15T11:40:00.000Z",
is_active: true,
event_type: "chat_member",
},
{
id: "s3",
purchase_id: "p1",
telegram_user_id: 333333333,
user_first_name: "Алексей",
user_last_name: null,
user_username: null,
subscribed_at: "2024-03-15T12:00:00.000Z",
is_active: false,
event_type: "join_request",
},
{
id: "s4",
purchase_id: "p2",
telegram_user_id: 444444444,
user_first_name: "Елена",
user_last_name: "Кузнецова",
user_username: "user4",
subscribed_at: "2024-03-18T14:20:00.000Z",
is_active: true,
event_type: "chat_member",
},
{
id: "s5",
purchase_id: "p2",
telegram_user_id: 555555555,
user_first_name: "Дмитрий",
user_last_name: "Смирнов",
user_username: "user5",
subscribed_at: "2024-03-18T14:25:00.000Z",
is_active: true,
event_type: "chat_member",
},
];
// Mock Views Snapshots
export const mockViewsSnapshots: ViewsSnapshot[] = [
{
id: "v1",
purchase_id: "p1",
views: 5000,
fetched_at: "2024-03-15T12:00:00.000Z",
},
{
id: "v2",
purchase_id: "p1",
views: 8500,
fetched_at: "2024-03-15T18:00:00.000Z",
},
{
id: "v3",
purchase_id: "p1",
views: 10200,
fetched_at: "2024-03-16T09:00:00.000Z",
},
{
id: "v4",
purchase_id: "p1",
views: 11800,
fetched_at: "2024-03-16T18:00:00.000Z",
},
{
id: "v5",
purchase_id: "p1",
views: 12500,
fetched_at: "2024-03-17T12:00:00.000Z",
},
{
id: "v6",
purchase_id: "p2",
views: 15000,
fetched_at: "2024-03-18T15:00:00.000Z",
},
{
id: "v7",
purchase_id: "p2",
views: 22000,
fetched_at: "2024-03-18T20:00:00.000Z",
},
{
id: "v8",
purchase_id: "p2",
views: 26500,
fetched_at: "2024-03-19T09:00:00.000Z",
},
{
id: "v9",
purchase_id: "p2",
views: 28000,
fetched_at: "2024-03-19T18:00:00.000Z",
},
];
export const getMockPurchaseDetail = (id: string): PurchaseDetail | null => {
const purchase = mockPurchases.find((p) => p.id === id);
if (!purchase) return null;
return {
...purchase,
subscriptions: mockSubscriptions.filter((s) => s.purchase_id === id),
views_history: mockViewsSnapshots.filter((v) => v.purchase_id === id),
};
};

View File

@@ -6,20 +6,14 @@ import { User } from "@/lib/types/api";
export const mockUsers: User[] = [
{
id: "1",
id: "550e8400-e29b-41d4-a716-446655440001",
telegram_id: 123456789,
username: "ivan_test",
first_name: "Иван",
last_name: "Иванов",
created_at: "2024-01-15T10:00:00.000Z",
},
{
id: "2",
id: "550e8400-e29b-41d4-a716-446655440002",
telegram_id: 987654321,
username: "maria_test",
first_name: "Мария",
last_name: "Петрова",
created_at: "2024-02-01T10:00:00.000Z",
},
];

View File

@@ -6,13 +6,11 @@ import {
mockCurrentUser,
mockTokens,
mockTargetChannels,
getMockTargetChannelDetail,
mockExternalChannels,
getMockExternalChannelDetail,
mockCreatives,
getMockCreativeDetail,
mockPurchases,
getMockPurchaseDetail,
mockPlacements,
mockSubscriptions,
mockViewsHistory,
mockAnalyticsOverview,
mockCostsReportByDay,
mockCostsReportByWeek,
@@ -28,22 +26,21 @@ import type {
TargetChannel,
ExternalChannel,
Creative,
Purchase,
PurchaseCreateRequest,
PurchaseCreateResponse,
Placement,
PlacementCreateRequest,
PlacementUpdateRequest,
CreativeCreateRequest,
ExternalChannelCreateRequest,
PurchaseRefreshViewsResponse,
ExternalChannelImportResponse,
ViewsFetchResponse,
ViewsHistoryResponse,
AuthCompleteRequest,
AuthCompleteResponse,
AuthRefreshRequest,
AuthRefreshResponse,
AuthInitResponse,
TargetChannelUpdateRequest,
ExternalChannelUpdateRequest,
CreativeUpdateRequest,
PurchaseUpdateRequest,
ExternalChannelsListResponse,
} from "@/lib/types/api";
// Simulate network delay
@@ -75,15 +72,21 @@ export const mockApiHandlers = {
};
},
async authComplete(data: AuthCompleteRequest): Promise<AuthCompleteResponse> {
async authComplete(
data: AuthCompleteRequest | { token: string | null }
): Promise<{ access_token: string }> {
await delay();
if (!data.token) {
throw mockError("VALIDATION_ERROR", "Token is required");
}
// Save mock user to localStorage for later retrieval
if (typeof window !== "undefined") {
localStorage.setItem("tgex_user", JSON.stringify(mockCurrentUser));
}
return {
access_token: mockTokens.access_token,
refresh_token: mockTokens.refresh_token,
user: mockCurrentUser,
};
},
@@ -105,44 +108,10 @@ export const mockApiHandlers = {
// --------------------------------------------------------------------------
// Target Channels
// --------------------------------------------------------------------------
async getTargetChannels(params?: {
is_active?: boolean;
}): Promise<ListResponse<TargetChannel>> {
async getTargetChannels() {
await delay();
let filtered = [...mockTargetChannels];
if (params?.is_active !== undefined) {
filtered = filtered.filter((c) => c.is_active === params.is_active);
}
return {
data: filtered,
total: filtered.length,
};
},
async getTargetChannel(id: string) {
await delay();
const detail = getMockTargetChannelDetail(id);
if (!detail) {
throw mockError("NOT_FOUND", "Target channel not found");
}
return detail;
},
async updateTargetChannel(
id: string,
data: TargetChannelUpdateRequest
): Promise<TargetChannel> {
await delay();
const channel = mockTargetChannels.find((c) => c.id === id);
if (!channel) {
throw mockError("NOT_FOUND", "Target channel not found");
}
return {
...channel,
is_active: data.is_active,
updated_at: new Date().toISOString(),
target_channels: mockTargetChannels,
};
},
@@ -158,41 +127,24 @@ export const mockApiHandlers = {
// --------------------------------------------------------------------------
// External Channels
// --------------------------------------------------------------------------
async getExternalChannels(params?: {
target_channel_id?: string;
search?: string;
}): Promise<ListResponse<ExternalChannel>> {
async getExternalChannels(
targetChannelId: string
): Promise<ExternalChannelsListResponse> {
await delay();
let filtered = [...mockExternalChannels];
if (params?.target_channel_id) {
filtered = filtered.filter((c) =>
c.target_channels.includes(params.target_channel_id!)
);
}
if (params?.search) {
const search = params.search.toLowerCase();
filtered = filtered.filter(
(c) =>
c.title.toLowerCase().includes(search) ||
c.username?.toLowerCase().includes(search)
);
}
// Возвращаем все каналы для указанного target channel
// В реальном API это будет фильтрация по связям через промежуточную таблицу
return {
data: filtered,
total: filtered.length,
external_channels: mockExternalChannels,
};
},
async getExternalChannel(id: string) {
async getExternalChannel(id: string): Promise<ExternalChannel> {
await delay();
const detail = getMockExternalChannelDetail(id);
if (!detail) {
const channel = mockExternalChannels.find((c) => c.id === id);
if (!channel) {
throw mockError("NOT_FOUND", "External channel not found");
}
return detail;
return channel;
},
async createExternalChannel(
@@ -201,18 +153,11 @@ export const mockApiHandlers = {
await delay(500);
const newChannel: ExternalChannel = {
id: generateId(),
telegram_id: null,
telegram_id: data.telegram_id,
title: data.title,
username: data.username || null,
link: data.link,
subscribers_count: data.subscribers_count || null,
description: data.description || null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
target_channels: data.target_channel_ids,
total_purchases: 0,
avg_cpf: null,
avg_cpm: null,
};
mockExternalChannels.push(newChannel);
return newChannel;
@@ -230,7 +175,6 @@ export const mockApiHandlers = {
return {
...channel,
...data,
updated_at: new Date().toISOString(),
};
},
@@ -244,42 +188,20 @@ export const mockApiHandlers = {
return { success: true };
},
async importExternalChannels(
file: File,
targetChannelId: string
): Promise<ExternalChannelImportResponse> {
await delay(1500); // Longer delay for file processing
// Mock: simulate importing 3-5 channels
const importCount = Math.floor(Math.random() * 3) + 3;
const imported: ExternalChannel[] = [];
for (let i = 0; i < importCount; i++) {
const newChannel: ExternalChannel = {
id: generateId(),
telegram_id: null,
title: `Импортированный канал ${i + 1}`,
username: `imported_channel_${i + 1}`,
link: `https://t.me/imported_channel_${i + 1}`,
subscribers_count: Math.floor(Math.random() * 100000) + 10000,
description: "Импортировано из Excel",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
target_channels: [targetChannelId],
total_purchases: 0,
avg_cpf: null,
avg_cpm: null,
};
imported.push(newChannel);
mockExternalChannels.push(newChannel);
async updateExternalChannelLinks(
id: string,
data: {
add_target_channel_ids?: string[];
remove_target_channel_ids?: string[];
}
return {
imported: importCount,
skipped: 1,
errors: ["Строка 15: некорректный формат ссылки"],
channels: imported,
};
): Promise<ExternalChannel> {
await delay();
const channel = mockExternalChannels.find((c) => c.id === id);
if (!channel) {
throw mockError("NOT_FOUND", "External channel not found");
}
// В упрощенной версии просто возвращаем канал
return channel;
},
// --------------------------------------------------------------------------
@@ -287,8 +209,8 @@ export const mockApiHandlers = {
// --------------------------------------------------------------------------
async getCreatives(params?: {
target_channel_id?: string;
is_archived?: boolean;
}): Promise<ListResponse<Creative>> {
include_archived?: boolean;
}) {
await delay();
let filtered = [...mockCreatives];
@@ -298,36 +220,27 @@ export const mockApiHandlers = {
);
}
if (params?.is_archived !== undefined) {
filtered = filtered.filter((c) => c.is_archived === params.is_archived);
if (!params?.include_archived) {
filtered = filtered.filter((c) => c.status !== "archived");
}
return {
data: filtered,
total: filtered.length,
creatives: filtered,
};
},
async getCreative(id: string) {
async getCreative(id: string): Promise<Creative> {
await delay();
const detail = getMockCreativeDetail(id);
if (!detail) {
const creative = mockCreatives.find((c) => c.id === id);
if (!creative) {
throw mockError("NOT_FOUND", "Creative not found");
}
return detail;
return creative;
},
async createCreative(data: CreativeCreateRequest): Promise<Creative> {
await delay(500);
// Validate {link} placeholder
if (!data.text.includes("{link}")) {
throw mockError(
"VALIDATION_ERROR",
"Creative text must contain {link} placeholder"
);
}
const targetChannel = mockTargetChannels.find(
(c) => c.id === data.target_channel_id
);
@@ -340,13 +253,10 @@ export const mockApiHandlers = {
name: data.name,
text: data.text,
target_channel_id: data.target_channel_id,
is_archived: false,
target_channel_title: targetChannel.title,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
target_channel: targetChannel,
total_purchases: 0,
total_subscriptions: 0,
avg_cpf: null,
status: "active",
placements_count: 0,
};
mockCreatives.push(newCreative);
return newCreative;
@@ -372,7 +282,6 @@ export const mockApiHandlers = {
return {
...creative,
...data,
updated_at: new Date().toISOString(),
};
},
@@ -383,10 +292,10 @@ export const mockApiHandlers = {
throw mockError("NOT_FOUND", "Creative not found");
}
if (creative.total_purchases > 0) {
if (creative.placements_count > 0) {
throw mockError(
"CREATIVE_IN_USE",
"Cannot delete creative with active purchases"
"Cannot delete creative with active placements"
);
}
@@ -396,20 +305,16 @@ export const mockApiHandlers = {
},
// --------------------------------------------------------------------------
// Purchases
// Placements (Purchases)
// --------------------------------------------------------------------------
async getPurchases(params?: {
async getPlacements(params?: {
target_channel_id?: string;
external_channel_id?: string;
creative_id?: string;
is_archived?: boolean;
date_from?: string;
date_to?: string;
sort?: string;
order?: "asc" | "desc";
}): Promise<ListResponse<Purchase>> {
include_archived?: boolean;
}) {
await delay();
let filtered = [...mockPurchases];
let filtered = [...mockPlacements];
if (params?.target_channel_id) {
filtered = filtered.filter(
@@ -427,62 +332,25 @@ export const mockApiHandlers = {
filtered = filtered.filter((p) => p.creative_id === params.creative_id);
}
if (params?.is_archived !== undefined) {
filtered = filtered.filter((p) => p.is_archived === params.is_archived);
}
// Sorting
if (params?.sort) {
filtered.sort((a, b) => {
let aVal: any;
let bVal: any;
switch (params.sort) {
case "date":
aVal = a.actual_date || a.scheduled_date || a.created_at;
bVal = b.actual_date || b.scheduled_date || b.created_at;
break;
case "cost":
aVal = a.cost || 0;
bVal = b.cost || 0;
break;
case "cpf":
aVal = a.cpf || 0;
bVal = b.cpf || 0;
break;
case "subscriptions":
aVal = a.subscriptions_count;
bVal = b.subscriptions_count;
break;
default:
return 0;
}
if (params.order === "desc") {
return bVal > aVal ? 1 : -1;
}
return aVal > bVal ? 1 : -1;
});
if (!params?.include_archived) {
filtered = filtered.filter((p) => p.status !== "archived");
}
return {
data: filtered,
total: filtered.length,
placements: filtered,
};
},
async getPurchase(id: string) {
async getPlacement(id: string): Promise<Placement> {
await delay();
const detail = getMockPurchaseDetail(id);
if (!detail) {
throw mockError("NOT_FOUND", "Purchase not found");
const placement = mockPlacements.find((p) => p.id === id);
if (!placement) {
throw mockError("NOT_FOUND", "Placement not found");
}
return detail;
return placement;
},
async createPurchase(
data: PurchaseCreateRequest
): Promise<PurchaseCreateResponse> {
async createPlacement(data: PlacementCreateRequest): Promise<Placement> {
await delay(800);
const targetChannel = mockTargetChannels.find(
@@ -502,85 +370,111 @@ export const mockApiHandlers = {
.toString(36)
.substring(2, 15)}`;
const newPurchase: Purchase = {
const newPlacement: Placement = {
id: generateId(),
target_channel_id: data.target_channel_id,
target_channel_title: targetChannel.title,
external_channel_id: data.external_channel_id,
external_channel_title: externalChannel.title,
creative_id: data.creative_id,
scheduled_date: data.scheduled_date || null,
actual_date: data.actual_date || null,
creative_name: creative.name,
placement_date: data.placement_date,
cost: data.cost || null,
comment: data.comment || null,
post_link: data.post_link || null,
ad_post_url: data.ad_post_url || null,
invite_link_type: data.invite_link_type || "public",
invite_link: inviteLink,
invite_link_type: data.invite_link_type,
is_archived: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
target_channel: targetChannel,
external_channel: externalChannel,
creative: creative,
status: "active",
subscriptions_count: 0,
views_count: null,
cpf: null,
cpm: null,
conversion_rate: null,
views_availability: "unknown",
last_views_fetch_at: null,
created_at: new Date().toISOString(),
};
mockPurchases.push(newPurchase);
// Format message with invite link
const formattedMessage = creative.text.replace("{link}", inviteLink);
return {
...newPurchase,
formatted_message: formattedMessage,
};
mockPlacements.push(newPlacement);
return newPlacement;
},
async updatePurchase(
async updatePlacement(
id: string,
data: PurchaseUpdateRequest
): Promise<Purchase> {
data: PlacementUpdateRequest
): Promise<Placement> {
await delay();
const purchase = mockPurchases.find((p) => p.id === id);
if (!purchase) {
throw mockError("NOT_FOUND", "Purchase not found");
const placement = mockPlacements.find((p) => p.id === id);
if (!placement) {
throw mockError("NOT_FOUND", "Placement not found");
}
return {
...purchase,
...placement,
...data,
updated_at: new Date().toISOString(),
};
},
async deletePurchase(id: string): Promise<SuccessResponse> {
async deletePlacement(id: string): Promise<SuccessResponse> {
await delay();
const index = mockPurchases.findIndex((p) => p.id === id);
const index = mockPlacements.findIndex((p) => p.id === id);
if (index === -1) {
throw mockError("NOT_FOUND", "Purchase not found");
throw mockError("NOT_FOUND", "Placement not found");
}
mockPurchases.splice(index, 1);
mockPlacements.splice(index, 1);
return { success: true };
},
async refreshPurchaseViews(
id: string
): Promise<PurchaseRefreshViewsResponse> {
async fetchPlacementViews(id: string): Promise<ViewsFetchResponse> {
await delay(1000);
const purchase = mockPurchases.find((p) => p.id === id);
if (!purchase) {
throw mockError("NOT_FOUND", "Purchase not found");
const placement = mockPlacements.find((p) => p.id === id);
if (!placement) {
throw mockError("NOT_FOUND", "Placement not found");
}
// Mock: add some random views
const newViews =
(purchase.views_count || 0) + Math.floor(Math.random() * 500);
(placement.views_count || 0) + Math.floor(Math.random() * 500);
return {
views: newViews,
placement_id: id,
views_count: newViews,
views_availability: "available",
fetched_at: new Date().toISOString(),
error_message: null,
};
},
async getPlacementViewsHistory(
id: string,
params?: { from_date?: string; to_date?: string }
): Promise<ViewsHistoryResponse> {
await delay();
const placement = mockPlacements.find((p) => p.id === id);
if (!placement) {
throw mockError("NOT_FOUND", "Placement not found");
}
const filteredHistory = mockViewsHistory.filter(
(v) => v.purchase_id === id
);
return {
histories: filteredHistory,
};
},
async setPlacementViewsManually(
id: string,
views_count: number
): Promise<Placement> {
await delay();
const placement = mockPlacements.find((p) => p.id === id);
if (!placement) {
throw mockError("NOT_FOUND", "Placement not found");
}
return {
...placement,
views_count,
views_availability: "manual",
last_views_fetch_at: new Date().toISOString(),
};
},

View File

@@ -6,5 +6,5 @@ export * from "./data/users";
export * from "./data/channels";
export * from "./data/external-channels";
export * from "./data/creatives";
export * from "./data/purchases";
export * from "./data/placements";
export * from "./data/analytics";