531 lines
14 KiB
TypeScript
531 lines
14 KiB
TypeScript
// ============================================================================
|
||
// Mock API Handlers
|
||
// ============================================================================
|
||
|
||
import {
|
||
mockCurrentUser,
|
||
mockTokens,
|
||
mockTargetChannels,
|
||
mockExternalChannels,
|
||
mockCreatives,
|
||
mockPlacements,
|
||
mockSubscriptions,
|
||
mockViewsHistory,
|
||
mockAnalyticsOverview,
|
||
mockCostsReportByDay,
|
||
mockCostsReportByWeek,
|
||
mockCostsReportByMonth,
|
||
mockAnalyticsChannelsData,
|
||
mockAnalyticsCreativesData,
|
||
} 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 getAnalyticsOverview(params?: {
|
||
target_channel_id?: string;
|
||
date_from?: string;
|
||
date_to?: string;
|
||
}) {
|
||
await delay();
|
||
return mockAnalyticsOverview;
|
||
},
|
||
|
||
async getAnalyticsCosts(params?: {
|
||
period?: "day" | "week" | "month";
|
||
target_channel_id?: string;
|
||
}) {
|
||
await delay();
|
||
const period = params?.period || "week";
|
||
|
||
if (period === "day") {
|
||
return mockCostsReportByDay;
|
||
} else if (period === "month") {
|
||
return mockCostsReportByMonth;
|
||
}
|
||
return mockCostsReportByWeek;
|
||
},
|
||
|
||
async getAnalyticsChannels(params?: {
|
||
target_channel_id?: string;
|
||
date_from?: string;
|
||
date_to?: string;
|
||
type?: "external" | "target";
|
||
}) {
|
||
await delay();
|
||
return {
|
||
data: mockAnalyticsChannelsData,
|
||
};
|
||
},
|
||
|
||
async getAnalyticsCreatives(params?: {
|
||
target_channel_id?: string;
|
||
date_from?: string;
|
||
date_to?: string;
|
||
}) {
|
||
await delay();
|
||
return {
|
||
data: mockAnalyticsCreativesData,
|
||
};
|
||
},
|
||
};
|