feat: all project setup

This commit is contained in:
ivannoskov
2025-11-10 12:07:24 +03:00
parent fbfd7719bb
commit b0a9934220
99 changed files with 19597 additions and 0 deletions

636
lib/mocks/handlers.ts Normal file
View File

@@ -0,0 +1,636 @@
// ============================================================================
// 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,
};
},
};