637 lines
17 KiB
TypeScript
637 lines
17 KiB
TypeScript
// ============================================================================
|
|
// Mock API Handlers
|
|
// ============================================================================
|
|
|
|
import {
|
|
mockCurrentUser,
|
|
mockTokens,
|
|
mockTargetChannels,
|
|
getMockTargetChannelDetail,
|
|
mockExternalChannels,
|
|
getMockExternalChannelDetail,
|
|
mockCreatives,
|
|
getMockCreativeDetail,
|
|
mockPurchases,
|
|
getMockPurchaseDetail,
|
|
mockAnalyticsOverview,
|
|
mockCostsReportByDay,
|
|
mockCostsReportByWeek,
|
|
mockCostsReportByMonth,
|
|
mockAnalyticsChannelsData,
|
|
mockAnalyticsCreativesData,
|
|
} from "./index";
|
|
|
|
import type {
|
|
ListResponse,
|
|
SuccessResponse,
|
|
ApiError,
|
|
TargetChannel,
|
|
ExternalChannel,
|
|
Creative,
|
|
Purchase,
|
|
PurchaseCreateRequest,
|
|
PurchaseCreateResponse,
|
|
CreativeCreateRequest,
|
|
ExternalChannelCreateRequest,
|
|
PurchaseRefreshViewsResponse,
|
|
ExternalChannelImportResponse,
|
|
AuthCompleteRequest,
|
|
AuthCompleteResponse,
|
|
AuthRefreshRequest,
|
|
AuthRefreshResponse,
|
|
AuthInitResponse,
|
|
TargetChannelUpdateRequest,
|
|
ExternalChannelUpdateRequest,
|
|
CreativeUpdateRequest,
|
|
PurchaseUpdateRequest,
|
|
} 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): Promise<AuthCompleteResponse> {
|
|
await delay();
|
|
if (!data.token) {
|
|
throw mockError("VALIDATION_ERROR", "Token is required");
|
|
}
|
|
return {
|
|
access_token: mockTokens.access_token,
|
|
refresh_token: mockTokens.refresh_token,
|
|
user: mockCurrentUser,
|
|
};
|
|
},
|
|
|
|
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(params?: {
|
|
is_active?: boolean;
|
|
}): Promise<ListResponse<TargetChannel>> {
|
|
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(),
|
|
};
|
|
},
|
|
|
|
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(params?: {
|
|
target_channel_id?: string;
|
|
search?: string;
|
|
}): Promise<ListResponse<ExternalChannel>> {
|
|
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)
|
|
);
|
|
}
|
|
|
|
return {
|
|
data: filtered,
|
|
total: filtered.length,
|
|
};
|
|
},
|
|
|
|
async getExternalChannel(id: string) {
|
|
await delay();
|
|
const detail = getMockExternalChannelDetail(id);
|
|
if (!detail) {
|
|
throw mockError("NOT_FOUND", "External channel not found");
|
|
}
|
|
return detail;
|
|
},
|
|
|
|
async createExternalChannel(
|
|
data: ExternalChannelCreateRequest
|
|
): Promise<ExternalChannel> {
|
|
await delay(500);
|
|
const newChannel: ExternalChannel = {
|
|
id: generateId(),
|
|
telegram_id: null,
|
|
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;
|
|
},
|
|
|
|
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,
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
},
|
|
|
|
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 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);
|
|
}
|
|
|
|
return {
|
|
imported: importCount,
|
|
skipped: 1,
|
|
errors: ["Строка 15: некорректный формат ссылки"],
|
|
channels: imported,
|
|
};
|
|
},
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Creatives
|
|
// --------------------------------------------------------------------------
|
|
async getCreatives(params?: {
|
|
target_channel_id?: string;
|
|
is_archived?: boolean;
|
|
}): Promise<ListResponse<Creative>> {
|
|
await delay();
|
|
let filtered = [...mockCreatives];
|
|
|
|
if (params?.target_channel_id) {
|
|
filtered = filtered.filter(
|
|
(c) => c.target_channel_id === params.target_channel_id
|
|
);
|
|
}
|
|
|
|
if (params?.is_archived !== undefined) {
|
|
filtered = filtered.filter((c) => c.is_archived === params.is_archived);
|
|
}
|
|
|
|
return {
|
|
data: filtered,
|
|
total: filtered.length,
|
|
};
|
|
},
|
|
|
|
async getCreative(id: string) {
|
|
await delay();
|
|
const detail = getMockCreativeDetail(id);
|
|
if (!detail) {
|
|
throw mockError("NOT_FOUND", "Creative not found");
|
|
}
|
|
return detail;
|
|
},
|
|
|
|
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
|
|
);
|
|
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,
|
|
is_archived: false,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
target_channel: targetChannel,
|
|
total_purchases: 0,
|
|
total_subscriptions: 0,
|
|
avg_cpf: null,
|
|
};
|
|
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,
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
},
|
|
|
|
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.total_purchases > 0) {
|
|
throw mockError(
|
|
"CREATIVE_IN_USE",
|
|
"Cannot delete creative with active purchases"
|
|
);
|
|
}
|
|
|
|
const index = mockCreatives.findIndex((c) => c.id === id);
|
|
mockCreatives.splice(index, 1);
|
|
return { success: true };
|
|
},
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Purchases
|
|
// --------------------------------------------------------------------------
|
|
async getPurchases(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>> {
|
|
await delay();
|
|
let filtered = [...mockPurchases];
|
|
|
|
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?.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;
|
|
});
|
|
}
|
|
|
|
return {
|
|
data: filtered,
|
|
total: filtered.length,
|
|
};
|
|
},
|
|
|
|
async getPurchase(id: string) {
|
|
await delay();
|
|
const detail = getMockPurchaseDetail(id);
|
|
if (!detail) {
|
|
throw mockError("NOT_FOUND", "Purchase not found");
|
|
}
|
|
return detail;
|
|
},
|
|
|
|
async createPurchase(
|
|
data: PurchaseCreateRequest
|
|
): Promise<PurchaseCreateResponse> {
|
|
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 newPurchase: Purchase = {
|
|
id: generateId(),
|
|
target_channel_id: data.target_channel_id,
|
|
external_channel_id: data.external_channel_id,
|
|
creative_id: data.creative_id,
|
|
scheduled_date: data.scheduled_date || null,
|
|
actual_date: data.actual_date || null,
|
|
cost: data.cost || null,
|
|
comment: data.comment || null,
|
|
post_link: data.post_link || null,
|
|
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,
|
|
subscriptions_count: 0,
|
|
views_count: null,
|
|
cpf: null,
|
|
cpm: null,
|
|
conversion_rate: null,
|
|
};
|
|
|
|
mockPurchases.push(newPurchase);
|
|
|
|
// Format message with invite link
|
|
const formattedMessage = creative.text.replace("{link}", inviteLink);
|
|
|
|
return {
|
|
...newPurchase,
|
|
formatted_message: formattedMessage,
|
|
};
|
|
},
|
|
|
|
async updatePurchase(
|
|
id: string,
|
|
data: PurchaseUpdateRequest
|
|
): Promise<Purchase> {
|
|
await delay();
|
|
const purchase = mockPurchases.find((p) => p.id === id);
|
|
if (!purchase) {
|
|
throw mockError("NOT_FOUND", "Purchase not found");
|
|
}
|
|
|
|
return {
|
|
...purchase,
|
|
...data,
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
},
|
|
|
|
async deletePurchase(id: string): Promise<SuccessResponse> {
|
|
await delay();
|
|
const index = mockPurchases.findIndex((p) => p.id === id);
|
|
if (index === -1) {
|
|
throw mockError("NOT_FOUND", "Purchase not found");
|
|
}
|
|
mockPurchases.splice(index, 1);
|
|
return { success: true };
|
|
},
|
|
|
|
async refreshPurchaseViews(
|
|
id: string
|
|
): Promise<PurchaseRefreshViewsResponse> {
|
|
await delay(1000);
|
|
const purchase = mockPurchases.find((p) => p.id === id);
|
|
if (!purchase) {
|
|
throw mockError("NOT_FOUND", "Purchase not found");
|
|
}
|
|
|
|
// Mock: add some random views
|
|
const newViews =
|
|
(purchase.views_count || 0) + Math.floor(Math.random() * 500);
|
|
|
|
return {
|
|
views: newViews,
|
|
fetched_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,
|
|
};
|
|
},
|
|
};
|