Files
tgex-frontend/lib/mocks/handlers.ts

574 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// Mock API Handlers
// ============================================================================
import {
mockCurrentUser,
mockTokens,
mockTargetChannels,
mockExternalChannels,
mockCreatives,
mockPlacements,
mockSubscriptions,
mockViewsHistory,
} from "./index";
import type {
ListResponse,
SuccessResponse,
ApiError,
TargetChannel,
ExternalChannel,
Creative,
Placement,
PlacementCreateRequest,
PlacementUpdateRequest,
CreativeCreateRequest,
ExternalChannelCreateRequest,
ViewsFetchResponse,
ViewsHistoryResponse,
AuthCompleteRequest,
AuthCompleteResponse,
AuthRefreshRequest,
AuthRefreshResponse,
AuthInitResponse,
ExternalChannelUpdateRequest,
CreativeUpdateRequest,
ExternalChannelsListResponse,
} from "@/lib/types/api";
// Simulate network delay
const delay = (ms: number = 300) =>
new Promise((resolve) => setTimeout(resolve, ms));
// Helper to generate mock error
const mockError = (code: string, message: string): ApiError => ({
error: { code, message },
});
// Helper to generate IDs
let idCounter = 1000;
const generateId = () => `mock_${idCounter++}`;
// ============================================================================
// Mock API Handlers
// ============================================================================
export const mockApiHandlers = {
// --------------------------------------------------------------------------
// Auth
// --------------------------------------------------------------------------
async authInit(): Promise<AuthInitResponse> {
await delay();
return {
bot_username: "tgex_bot",
start_param: "login",
};
},
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,
};
},
async authRefresh(data: AuthRefreshRequest): Promise<AuthRefreshResponse> {
await delay();
if (!data.refresh_token) {
throw mockError("VALIDATION_ERROR", "Refresh token is required");
}
return {
access_token: `${mockTokens.access_token}_refreshed`,
};
},
async authMe() {
await delay();
return mockCurrentUser;
},
// --------------------------------------------------------------------------
// Target Channels
// --------------------------------------------------------------------------
async getTargetChannels() {
await delay();
return {
target_channels: mockTargetChannels,
};
},
async deleteTargetChannel(id: string): Promise<SuccessResponse> {
await delay();
const channel = mockTargetChannels.find((c) => c.id === id);
if (!channel) {
throw mockError("NOT_FOUND", "Target channel not found");
}
return { success: true };
},
// --------------------------------------------------------------------------
// External Channels
// --------------------------------------------------------------------------
async getExternalChannels(
targetChannelId: string
): Promise<ExternalChannelsListResponse> {
await delay();
// Возвращаем все каналы для указанного target channel
// В реальном API это будет фильтрация по связям через промежуточную таблицу
return {
external_channels: mockExternalChannels,
};
},
async getExternalChannel(id: string): Promise<ExternalChannel> {
await delay();
const channel = mockExternalChannels.find((c) => c.id === id);
if (!channel) {
throw mockError("NOT_FOUND", "External channel not found");
}
return channel;
},
async createExternalChannel(
data: ExternalChannelCreateRequest
): Promise<ExternalChannel> {
await delay(500);
const newChannel: ExternalChannel = {
id: generateId(),
telegram_id: data.telegram_id,
title: data.title,
username: data.username || null,
subscribers_count: data.subscribers_count || null,
description: data.description || null,
};
mockExternalChannels.push(newChannel);
return newChannel;
},
async updateExternalChannel(
id: string,
data: ExternalChannelUpdateRequest
): Promise<ExternalChannel> {
await delay();
const channel = mockExternalChannels.find((c) => c.id === id);
if (!channel) {
throw mockError("NOT_FOUND", "External channel not found");
}
return {
...channel,
...data,
};
},
async deleteExternalChannel(id: string): Promise<SuccessResponse> {
await delay();
const index = mockExternalChannels.findIndex((c) => c.id === id);
if (index === -1) {
throw mockError("NOT_FOUND", "External channel not found");
}
mockExternalChannels.splice(index, 1);
return { success: true };
},
async updateExternalChannelLinks(
id: string,
data: {
add_target_channel_ids?: string[];
remove_target_channel_ids?: string[];
}
): Promise<ExternalChannel> {
await delay();
const channel = mockExternalChannels.find((c) => c.id === id);
if (!channel) {
throw mockError("NOT_FOUND", "External channel not found");
}
// В упрощенной версии просто возвращаем канал
return channel;
},
// --------------------------------------------------------------------------
// Creatives
// --------------------------------------------------------------------------
async getCreatives(params?: {
target_channel_id?: string;
include_archived?: boolean;
}) {
await delay();
let filtered = [...mockCreatives];
if (params?.target_channel_id) {
filtered = filtered.filter(
(c) => c.target_channel_id === params.target_channel_id
);
}
if (!params?.include_archived) {
filtered = filtered.filter((c) => c.status !== "archived");
}
return {
creatives: filtered,
};
},
async getCreative(id: string): Promise<Creative> {
await delay();
const creative = mockCreatives.find((c) => c.id === id);
if (!creative) {
throw mockError("NOT_FOUND", "Creative not found");
}
return creative;
},
async createCreative(data: CreativeCreateRequest): Promise<Creative> {
await delay(500);
const targetChannel = mockTargetChannels.find(
(c) => c.id === data.target_channel_id
);
if (!targetChannel) {
throw mockError("NOT_FOUND", "Target channel not found");
}
const newCreative: Creative = {
id: generateId(),
name: data.name,
text: data.text,
target_channel_id: data.target_channel_id,
target_channel_title: targetChannel.title,
created_at: new Date().toISOString(),
status: "active",
placements_count: 0,
};
mockCreatives.push(newCreative);
return newCreative;
},
async updateCreative(
id: string,
data: CreativeUpdateRequest
): Promise<Creative> {
await delay();
const creative = mockCreatives.find((c) => c.id === id);
if (!creative) {
throw mockError("NOT_FOUND", "Creative not found");
}
if (data.text && !data.text.includes("{link}")) {
throw mockError(
"VALIDATION_ERROR",
"Creative text must contain {link} placeholder"
);
}
return {
...creative,
...data,
};
},
async deleteCreative(id: string): Promise<SuccessResponse> {
await delay();
const creative = mockCreatives.find((c) => c.id === id);
if (!creative) {
throw mockError("NOT_FOUND", "Creative not found");
}
if (creative.placements_count > 0) {
throw mockError(
"CREATIVE_IN_USE",
"Cannot delete creative with active placements"
);
}
const index = mockCreatives.findIndex((c) => c.id === id);
mockCreatives.splice(index, 1);
return { success: true };
},
// --------------------------------------------------------------------------
// Placements (Purchases)
// --------------------------------------------------------------------------
async getPlacements(params?: {
target_channel_id?: string;
external_channel_id?: string;
creative_id?: string;
include_archived?: boolean;
}) {
await delay();
let filtered = [...mockPlacements];
if (params?.target_channel_id) {
filtered = filtered.filter(
(p) => p.target_channel_id === params.target_channel_id
);
}
if (params?.external_channel_id) {
filtered = filtered.filter(
(p) => p.external_channel_id === params.external_channel_id
);
}
if (params?.creative_id) {
filtered = filtered.filter((p) => p.creative_id === params.creative_id);
}
if (!params?.include_archived) {
filtered = filtered.filter((p) => p.status !== "archived");
}
return {
placements: filtered,
};
},
async getPlacement(id: string): Promise<Placement> {
await delay();
const placement = mockPlacements.find((p) => p.id === id);
if (!placement) {
throw mockError("NOT_FOUND", "Placement not found");
}
return placement;
},
async createPlacement(data: PlacementCreateRequest): Promise<Placement> {
await delay(800);
const targetChannel = mockTargetChannels.find(
(c) => c.id === data.target_channel_id
);
const externalChannel = mockExternalChannels.find(
(c) => c.id === data.external_channel_id
);
const creative = mockCreatives.find((c) => c.id === data.creative_id);
if (!targetChannel || !externalChannel || !creative) {
throw mockError("NOT_FOUND", "One of the required entities not found");
}
// Generate mock invite link
const inviteLink = `https://t.me/+${Math.random()
.toString(36)
.substring(2, 15)}`;
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,
creative_name: creative.name,
placement_date: data.placement_date,
cost: data.cost || null,
comment: data.comment || null,
ad_post_url: data.ad_post_url || null,
invite_link_type: data.invite_link_type || "public",
invite_link: inviteLink,
status: "active",
subscriptions_count: 0,
views_count: null,
views_availability: "unknown",
last_views_fetch_at: null,
created_at: new Date().toISOString(),
};
mockPlacements.push(newPlacement);
return newPlacement;
},
async updatePlacement(
id: string,
data: PlacementUpdateRequest
): Promise<Placement> {
await delay();
const placement = mockPlacements.find((p) => p.id === id);
if (!placement) {
throw mockError("NOT_FOUND", "Placement not found");
}
return {
...placement,
...data,
};
},
async deletePlacement(id: string): Promise<SuccessResponse> {
await delay();
const index = mockPlacements.findIndex((p) => p.id === id);
if (index === -1) {
throw mockError("NOT_FOUND", "Placement not found");
}
mockPlacements.splice(index, 1);
return { success: true };
},
async fetchPlacementViews(id: string): Promise<ViewsFetchResponse> {
await delay(1000);
const placement = mockPlacements.find((p) => p.id === id);
if (!placement) {
throw mockError("NOT_FOUND", "Placement not found");
}
// Mock: add some random views
const newViews =
(placement.views_count || 0) + Math.floor(Math.random() * 500);
return {
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(),
};
},
// --------------------------------------------------------------------------
// Analytics
// --------------------------------------------------------------------------
async getSpendingAnalytics(params?: {
target_channel_id?: string;
date_from?: string;
date_to?: string;
grouping?: "day" | "week" | "month" | "quarter" | "year";
}) {
await delay();
return {
total_cost: 45000,
total_subscriptions: 850,
total_views: 125000,
avg_cpf: 52.94,
avg_cpm: 360,
chart_data: [
{
period: "2024-03-01",
cost: 15000,
subscriptions: 280,
views: 42000,
cpf: 53.57,
cpm: 357.14,
},
{
period: "2024-03-08",
cost: 18000,
subscriptions: 340,
views: 51000,
cpf: 52.94,
cpm: 352.94,
},
{
period: "2024-03-15",
cost: 12000,
subscriptions: 230,
views: 32000,
cpf: 52.17,
cpm: 375,
},
],
};
},
async getPlacementsAnalytics(params?: { target_channel_id?: string }) {
await delay();
return {
placements: mockPlacements.map((p) => ({
id: p.id,
target_channel_id: p.target_channel_id,
target_channel_title: p.target_channel_title,
external_channel_id: p.external_channel_id,
external_channel_title: p.external_channel_title,
creative_id: p.creative_id,
creative_name: p.creative_name,
placement_date: p.placement_date,
cost: p.cost,
subscriptions_count: p.subscriptions_count,
views_count: p.views_count,
cpf: p.cost && p.subscriptions_count > 0 ? p.cost / p.subscriptions_count : null,
cpm: p.cost && p.views_count ? (p.cost / p.views_count) * 1000 : null,
})),
};
},
async getCreativesAnalytics(params?: { target_channel_id?: string }) {
await delay();
return {
creatives: mockCreatives.map((c) => ({
id: c.id,
name: c.name,
placements_count: c.placements_count,
total_cost: 15000,
total_subscriptions: 280,
total_views: 42000,
avg_cpf: 53.57,
avg_cpm: 357.14,
})),
};
},
async getExternalChannelsAnalytics(params?: { target_channel_id?: string }) {
await delay();
return {
external_channels: mockExternalChannels.map((ec) => ({
id: ec.id,
title: ec.title,
username: ec.username,
placements_count: 3,
total_cost: 12000,
total_subscriptions: 210,
total_views: 28000,
avg_cpf: 57.14,
avg_cpm: 428.57,
})),
};
},
};