Merge remote-tracking branch 'origin/dev_nikita' into dev_nikita

This commit is contained in:
firewookie
2026-03-10 15:29:14 +05:00
77 changed files with 13169 additions and 1380 deletions

View File

@@ -49,6 +49,20 @@ export const authApi = {
return response.data;
},
// Telegram OIDC authentication (popup flow with id_token)
loginTelegramOIDC: async (
idToken: string,
campaignSlug?: string | null,
referralCode?: string | null,
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/oidc', {
id_token: idToken,
campaign_slug: campaignSlug || undefined,
referral_code: referralCode || undefined,
});
return response.data;
},
// Email login
loginEmail: async (
email: string,
@@ -234,10 +248,11 @@ export const authApi = {
return response.data;
},
// Link Telegram account (Mini App initData or Login Widget data)
// Link Telegram account (Mini App initData, OIDC id_token, or Login Widget data)
linkTelegram: async (
data:
| { init_data: string }
| { id_token: string }
| {
id: number;
first_name: string;
@@ -280,6 +295,12 @@ export const authApi = {
return response.data;
},
// Auto-login from guest purchase success page
autoLogin: async (token: string): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/login/auto', { token });
return response.data;
},
// Account merge (no JWT required)
getMergePreview: async (mergeToken: string): Promise<MergePreviewResponse> => {
const response = await apiClient.get<MergePreviewResponse>(

View File

@@ -110,7 +110,15 @@ export const balanceApi = {
// Get specific pending payment details
getPendingPayment: async (method: string, paymentId: number): Promise<PendingPayment> => {
const response = await apiClient.get<PendingPayment>(
`/cabinet/balance/pending-payments/${method}/${paymentId}`,
`/cabinet/balance/pending-payments/${encodeURIComponent(method)}/${encodeURIComponent(paymentId)}`,
);
return response.data;
},
// Get latest pending payment by method (fallback when sessionStorage unavailable)
getLatestPayment: async (method: string): Promise<PendingPayment> => {
const response = await apiClient.get<PendingPayment>(
`/cabinet/balance/pending-payments/${encodeURIComponent(method)}/latest`,
);
return response.data;
},
@@ -118,7 +126,7 @@ export const balanceApi = {
// Manually check payment status
checkPaymentStatus: async (method: string, paymentId: number): Promise<ManualCheckResponse> => {
const response = await apiClient.post<ManualCheckResponse>(
`/cabinet/balance/pending-payments/${method}/${paymentId}/check`,
`/cabinet/balance/pending-payments/${encodeURIComponent(method)}/${encodeURIComponent(paymentId)}/check`,
);
return response.data;
},

View File

@@ -23,6 +23,20 @@ export interface EmailAuthEnabled {
enabled: boolean;
}
export interface GiftEnabled {
enabled: boolean;
}
export interface TelegramWidgetConfig {
bot_username: string;
size: 'large' | 'medium' | 'small';
radius: number;
userpic: boolean;
request_access: boolean;
oidc_enabled: boolean;
oidc_client_id: string;
}
export interface AnalyticsCounters {
yandex_metrika_id: string;
google_ads_id: string;
@@ -241,6 +255,24 @@ export const brandingApi = {
return response.data;
},
// Get gift enabled (public, no auth required)
getGiftEnabled: async (): Promise<GiftEnabled> => {
try {
const response = await apiClient.get<GiftEnabled>('/cabinet/branding/gift-enabled');
return response.data;
} catch {
return { enabled: false };
}
},
// Update gift enabled (admin only)
updateGiftEnabled: async (enabled: boolean): Promise<GiftEnabled> => {
const response = await apiClient.patch<GiftEnabled>('/cabinet/branding/gift-enabled', {
enabled,
});
return response.data;
},
// Get analytics counters (public, no auth required)
getAnalyticsCounters: async (): Promise<AnalyticsCounters> => {
try {
@@ -251,6 +283,26 @@ export const brandingApi = {
}
},
// Get Telegram widget config (public, no auth required)
getTelegramWidgetConfig: async (): Promise<TelegramWidgetConfig> => {
try {
const response = await apiClient.get<TelegramWidgetConfig>(
'/cabinet/branding/telegram-widget',
);
return response.data;
} catch {
return {
bot_username: import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '',
size: 'large',
radius: 8,
userpic: true,
request_access: true,
oidc_enabled: false,
oidc_client_id: '',
};
}
},
// Update analytics counters (admin only)
updateAnalyticsCounters: async (data: Partial<AnalyticsCounters>): Promise<AnalyticsCounters> => {
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data);

View File

@@ -77,6 +77,7 @@ const AUTH_ENDPOINTS = [
'/cabinet/auth/oauth/',
'/cabinet/auth/merge/',
'/cabinet/auth/account/link/server-complete',
'/cabinet/landing/',
];
function isAuthEndpoint(url: string | undefined): boolean {
@@ -91,18 +92,22 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
if (!isAuthEndpoint(config.url)) {
let token = tokenStorage.getAccessToken();
// Проверяем срок действия токена перед запросом
if (token && isTokenExpired(token)) {
// Используем централизованный менеджер для refresh
// Access token expired — try refresh
const newToken = await tokenRefreshManager.refreshAccessToken();
if (newToken) {
token = newToken;
} else {
// Refresh не удался - редирект на логин
tokenStorage.clearTokens();
safeRedirectToLogin();
return config;
}
} else if (!token && tokenStorage.getRefreshToken()) {
// No access token (e.g. tab reopen) but refresh token exists — restore session
const newToken = await tokenRefreshManager.refreshAccessToken();
if (newToken) {
token = newToken;
}
}
if (token && config.headers) {

157
src/api/gift.ts Normal file
View File

@@ -0,0 +1,157 @@
import apiClient from './client';
// Types
export interface GiftTariffPeriod {
days: number;
price_kopeks: number;
price_label: string;
original_price_kopeks: number | null;
discount_percent: number | null;
}
export interface GiftTariff {
id: number;
name: string;
description: string | null;
traffic_limit_gb: number;
device_limit: number;
periods: GiftTariffPeriod[];
}
export interface GiftPaymentMethodSubOption {
id: string;
name: string;
}
export interface GiftPaymentMethod {
method_id: string;
display_name: string;
description: string | null;
icon_url: string | null;
min_amount_kopeks: number | null;
max_amount_kopeks: number | null;
sub_options: GiftPaymentMethodSubOption[] | null;
}
export interface GiftConfig {
is_enabled: boolean;
tariffs: GiftTariff[];
payment_methods: GiftPaymentMethod[];
balance_kopeks: number;
currency_symbol: string;
}
export interface GiftPurchaseRequest {
tariff_id: number;
period_days: number;
recipient_type?: 'email' | 'telegram';
recipient_value?: string;
gift_message?: string;
payment_mode: 'balance' | 'gateway';
payment_method?: string;
}
export interface GiftPurchaseResponse {
status: 'ok' | 'created' | 'paid';
purchase_token: string;
payment_url: string | null;
warning: string | null;
}
export type GiftPurchaseStatusValue =
| 'pending'
| 'paid'
| 'delivered'
| 'pending_activation'
| 'failed'
| 'expired';
export interface GiftPurchaseStatus {
status: GiftPurchaseStatusValue;
is_gift: boolean;
is_code_only: boolean;
purchase_token: string | null;
recipient_contact_value: string | null;
gift_message: string | null;
tariff_name: string | null;
period_days: number | null;
warning: string | null;
}
export interface PendingGift {
token: string;
tariff_name: string | null;
period_days: number;
gift_message: string | null;
sender_display: string | null;
created_at: string | null;
}
export interface SentGift {
token: string;
tariff_name: string | null;
period_days: number;
device_limit: number;
status: string;
gift_recipient_value: string | null;
gift_message: string | null;
activated_by_username: string | null;
created_at: string | null;
}
export interface ReceivedGift {
token: string;
tariff_name: string | null;
period_days: number;
device_limit: number;
status: string;
sender_display: string | null;
gift_message: string | null;
created_at: string | null;
}
export interface ActivateGiftResponse {
status: string;
tariff_name: string | null;
period_days: number | null;
}
// API
export const giftApi = {
getConfig: async (): Promise<GiftConfig> => {
const { data } = await apiClient.get<GiftConfig>('/cabinet/gift/config');
return data;
},
createPurchase: async (request: GiftPurchaseRequest): Promise<GiftPurchaseResponse> => {
const { data } = await apiClient.post<GiftPurchaseResponse>('/cabinet/gift/purchase', request);
return data;
},
getPurchaseStatus: async (token: string): Promise<GiftPurchaseStatus> => {
const { data } = await apiClient.get<GiftPurchaseStatus>(`/cabinet/gift/purchase/${token}`);
return data;
},
getPendingGifts: async (): Promise<PendingGift[]> => {
const { data } = await apiClient.get<PendingGift[]>('/cabinet/gift/pending');
return data;
},
getSentGifts: async (): Promise<SentGift[]> => {
const { data } = await apiClient.get<SentGift[]>('/cabinet/gift/sent');
return data;
},
getReceivedGifts: async (): Promise<ReceivedGift[]> => {
const { data } = await apiClient.get<ReceivedGift[]>('/cabinet/gift/received');
return data;
},
activateGiftCode: async (code: string): Promise<ActivateGiftResponse> => {
const { data } = await apiClient.post<ActivateGiftResponse>('/cabinet/gift/activate', { code });
return data;
},
};

404
src/api/landings.ts Normal file
View File

@@ -0,0 +1,404 @@
import apiClient from './client';
import type { AnimationConfig } from '@/components/ui/backgrounds/types';
// ============================================================
// Public types
// ============================================================
export interface LandingFeature {
icon: string;
title: string;
description: string;
}
export interface LandingTariffPeriod {
days: number;
label: string;
price_kopeks: number;
price_label: string;
original_price_kopeks: number | null;
original_price_label: string | null;
discount_percent: number | null;
}
export interface LandingTariff {
id: number;
name: string;
description: string | null;
traffic_limit_gb: number;
device_limit: number;
tier_level: number;
periods: LandingTariffPeriod[];
}
export interface LandingPaymentMethodSubOption {
id: string;
name: string;
}
/** Payment method as returned by the public landing config API */
export interface LandingPaymentMethod {
method_id: string;
display_name: string;
description: string | null;
icon_url: string | null;
sort_order: number;
min_amount_kopeks: number | null;
max_amount_kopeks: number | null;
currency: string | null;
sub_options: LandingPaymentMethodSubOption[] | null;
}
/** Payment method as stored/returned by the admin landing API (sub_options is a dict) */
export interface AdminLandingPaymentMethod {
method_id: string;
display_name: string;
description: string | null;
icon_url: string | null;
sort_order: number;
min_amount_kopeks: number | null;
max_amount_kopeks: number | null;
currency: string | null;
return_url: string | null;
sub_options: Record<string, boolean> | null;
}
/** Editable fields on a payment method in the landing editor */
export type EditableMethodField =
| 'display_name'
| 'description'
| 'icon_url'
| 'min_amount_kopeks'
| 'max_amount_kopeks'
| 'currency'
| 'return_url';
export interface LandingDiscountInfo {
percent: number;
ends_at: string; // ISO datetime
badge_text: string | null;
}
export interface LandingConfig {
slug: string;
title: string;
subtitle: string | null;
features: LandingFeature[];
footer_text: string | null;
tariffs: LandingTariff[];
payment_methods: LandingPaymentMethod[];
gift_enabled: boolean;
custom_css: string | null;
meta_title: string | null;
meta_description: string | null;
discount: LandingDiscountInfo | null;
background_config: AnimationConfig | null;
}
export interface PurchaseRequest {
tariff_id: number;
period_days: number;
contact_type: 'email' | 'telegram';
contact_value: string;
payment_method: string;
is_gift: boolean;
gift_recipient_type?: 'email' | 'telegram';
gift_recipient_value?: string;
gift_message?: string;
}
export interface PurchaseResponse {
purchase_token: string;
payment_url: string;
}
export interface PurchaseStatus {
status: 'pending' | 'paid' | 'delivered' | 'pending_activation' | 'failed' | 'expired';
subscription_url: string | null;
subscription_crypto_link: string | null;
is_gift: boolean;
contact_value: string | null;
recipient_contact_value: string | null;
period_days: number | null;
tariff_name: string | null;
gift_message: string | null;
contact_type: 'email' | 'telegram' | null;
cabinet_email: string | null;
cabinet_password: string | null;
auto_login_token: string | null;
recipient_in_bot: boolean | null;
bot_link: string | null;
}
// ============================================================
// Locale helpers
// ============================================================
/** Locale dict for multi-language text fields (admin API) */
export type LocaleDict = Record<string, string>;
/** Supported locales for the admin editor */
export const SUPPORTED_LOCALES = ['ru', 'en', 'zh', 'fa'] as const;
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
export const LOCALE_META: Record<SupportedLocale, { flag: string; name: string; rtl: boolean }> = {
ru: { flag: '\u{1F1F7}\u{1F1FA}', name: 'RU', rtl: false },
en: { flag: '\u{1F1EC}\u{1F1E7}', name: 'EN', rtl: false },
zh: { flag: '\u{1F1E8}\u{1F1F3}', name: 'ZH', rtl: false },
fa: { flag: '\u{1F1EE}\u{1F1F7}', name: 'FA', rtl: true },
};
// ============================================================
// Admin types
// ============================================================
/** Admin feature type with localized title/description */
export interface AdminLandingFeature {
icon: string;
title: LocaleDict;
description: LocaleDict;
}
export interface LandingListItem {
id: number;
slug: string;
title: LocaleDict;
is_active: boolean;
display_order: number;
gift_enabled: boolean;
tariff_count: number;
method_count: number;
purchase_stats: {
total: number;
pending: number;
paid: number;
delivered: number;
pending_activation: number;
failed: number;
expired: number;
};
created_at: string | null;
updated_at: string | null;
has_active_discount: boolean;
}
export interface LandingDetail {
id: number;
slug: string;
title: LocaleDict;
subtitle: LocaleDict | null;
is_active: boolean;
features: AdminLandingFeature[];
footer_text: LocaleDict | null;
allowed_tariff_ids: number[];
allowed_periods: Record<string, number[]>;
payment_methods: AdminLandingPaymentMethod[];
gift_enabled: boolean;
custom_css: string | null;
meta_title: LocaleDict | null;
meta_description: LocaleDict | null;
display_order: number;
created_at: string | null;
updated_at: string | null;
discount_percent: number | null;
discount_overrides: Record<string, number> | null;
discount_starts_at: string | null;
discount_ends_at: string | null;
discount_badge_text: LocaleDict | null;
background_config: AnimationConfig | null;
}
export interface LandingCreateRequest {
slug: string;
title: LocaleDict;
subtitle?: LocaleDict;
is_active?: boolean;
features?: AdminLandingFeature[];
footer_text?: LocaleDict;
allowed_tariff_ids?: number[];
allowed_periods?: Record<string, number[]>;
payment_methods?: AdminLandingPaymentMethod[];
gift_enabled?: boolean;
custom_css?: string;
meta_title?: LocaleDict;
meta_description?: LocaleDict;
discount_percent?: number | null;
discount_overrides?: Record<string, number> | null;
discount_starts_at?: string | null;
discount_ends_at?: string | null;
discount_badge_text?: LocaleDict | null;
background_config?: AnimationConfig | null;
}
export type LandingUpdateRequest = Partial<LandingCreateRequest>;
/**
* Normalize a value that might be a plain string (old API) or a LocaleDict.
* If it's a string, wraps it as `{ ru: value }`.
* If null/undefined, returns the fallback.
*/
/** Extract best display string from a LocaleDict: ru → en → first available → '' */
export function resolveLocaleDisplay(dict: LocaleDict | string | null | undefined): string {
if (!dict) return '';
if (typeof dict === 'string') return dict;
return dict.ru || dict.en || Object.values(dict).find((v) => v?.trim()) || '';
}
export function toLocaleDict(
value: string | LocaleDict | null | undefined,
fallback: LocaleDict = {},
): LocaleDict {
if (value === null || value === undefined) return fallback;
if (typeof value === 'string') return value ? { ru: value } : fallback;
return value;
}
// ============================================================
// Public API
// ============================================================
export const landingApi = {
getConfig: async (slug: string, lang?: string): Promise<LandingConfig> => {
const params = lang ? `?lang=${lang}` : '';
const response = await apiClient.get(`/cabinet/landing/${slug}${params}`);
return response.data;
},
createPurchase: async (slug: string, data: PurchaseRequest): Promise<PurchaseResponse> => {
const response = await apiClient.post(`/cabinet/landing/${slug}/purchase`, data);
return response.data;
},
getPurchaseStatus: async (token: string): Promise<PurchaseStatus> => {
const response = await apiClient.get(`/cabinet/landing/purchase/${token}`);
return response.data;
},
activatePurchase: async (token: string): Promise<PurchaseStatus> => {
const response = await apiClient.post(`/cabinet/landing/activate/${token}`);
return response.data;
},
};
// ============================================================
// Admin stats types
// ============================================================
export interface LandingDailyStat {
date: string;
purchases: number;
revenue_kopeks: number;
gifts: number;
}
export interface LandingTariffStat {
tariff_id: number | null;
tariff_name: string;
purchases: number;
revenue_kopeks: number;
}
export interface LandingStatsResponse {
total_purchases: number;
total_revenue_kopeks: number;
total_gifts: number;
total_regular: number;
avg_purchase_kopeks: number;
total_created: number;
total_successful: number;
conversion_rate: number;
daily_stats: LandingDailyStat[];
tariff_stats: LandingTariffStat[];
}
// ============================================================
// Admin purchase list types
// ============================================================
export type PurchaseItemStatus =
| 'pending'
| 'paid'
| 'delivered'
| 'pending_activation'
| 'failed'
| 'expired';
export interface LandingPurchaseItem {
id: number;
token: string;
contact_type: 'email' | 'telegram';
contact_value: string;
is_gift: boolean;
gift_recipient_type: 'email' | 'telegram' | null;
gift_recipient_value: string | null;
tariff_name: string;
period_days: number;
amount_kopeks: number;
currency: string;
payment_method: string;
status: PurchaseItemStatus;
created_at: string;
paid_at: string | null;
}
export interface LandingPurchaseListResponse {
items: LandingPurchaseItem[];
total: number;
}
// ============================================================
// Admin API
// ============================================================
export const adminLandingsApi = {
list: async (): Promise<LandingListItem[]> => {
const response = await apiClient.get('/cabinet/admin/landings');
return response.data;
},
get: async (id: number): Promise<LandingDetail> => {
const response = await apiClient.get(`/cabinet/admin/landings/${id}`);
return response.data;
},
create: async (data: LandingCreateRequest): Promise<LandingDetail> => {
const response = await apiClient.post('/cabinet/admin/landings', data);
return response.data;
},
update: async (id: number, data: LandingUpdateRequest): Promise<LandingDetail> => {
const response = await apiClient.put(`/cabinet/admin/landings/${id}`, data);
return response.data;
},
delete: async (id: number): Promise<{ success: boolean }> => {
const response = await apiClient.delete(`/cabinet/admin/landings/${id}`);
return response.data;
},
toggle: async (id: number): Promise<LandingDetail> => {
const response = await apiClient.post(`/cabinet/admin/landings/${id}/toggle`);
return response.data;
},
reorder: async (landingIds: number[]): Promise<void> => {
await apiClient.put('/cabinet/admin/landings/order', { landing_ids: landingIds });
},
getStats: async (id: number): Promise<LandingStatsResponse> => {
const response = await apiClient.get(`/cabinet/admin/landings/${id}/stats`);
return response.data;
},
getPurchases: async (
id: number,
offset: number,
limit: number,
status?: PurchaseItemStatus,
): Promise<LandingPurchaseListResponse> => {
const params: Record<string, string | number> = { offset, limit };
if (status) params.status = status;
const response = await apiClient.get(`/cabinet/admin/landings/${id}/purchases`, { params });
return response.data;
},
};

92
src/api/menuLayout.ts Normal file
View File

@@ -0,0 +1,92 @@
import apiClient from './client';
export type OpenIn = 'external' | 'webapp';
export interface MenuButtonConfig {
id: string;
type: 'builtin' | 'custom';
style: 'primary' | 'success' | 'danger' | 'default';
icon_custom_emoji_id: string;
enabled: boolean;
labels: Record<string, string>;
url: string | null;
open_in: OpenIn;
}
export interface MenuRowConfig {
id: string;
max_per_row: number;
buttons: MenuButtonConfig[];
}
export interface MenuConfig {
rows: MenuRowConfig[];
}
export const BOT_LOCALES = ['ru', 'en', 'ua', 'zh', 'fa'] as const;
export type BotLocale = (typeof BOT_LOCALES)[number];
export const BUILTIN_SECTIONS = [
'home',
'subscription',
'balance',
'referral',
'support',
'info',
'admin',
'language',
] as const;
export type BuiltinSection = (typeof BUILTIN_SECTIONS)[number];
export const STYLE_OPTIONS = [
{ value: 'default' as const, colorClass: 'bg-dark-500' },
{ value: 'primary' as const, colorClass: 'bg-blue-500' },
{ value: 'success' as const, colorClass: 'bg-success-500' },
{ value: 'danger' as const, colorClass: 'bg-red-500' },
];
const DEFAULT_CONFIG: MenuConfig = { rows: [] };
const DEFAULT_BUTTON: Omit<MenuButtonConfig, 'id' | 'type'> = {
style: 'primary',
icon_custom_emoji_id: '',
enabled: true,
labels: {},
url: null,
open_in: 'external',
};
function normalizeConfig(data: MenuConfig): MenuConfig {
if (!data || typeof data !== 'object') {
return DEFAULT_CONFIG;
}
return {
rows: (data.rows || []).map((row) => ({
id: row.id,
max_per_row: row.max_per_row ?? 2,
buttons: (row.buttons || []).map((btn) => ({
...DEFAULT_BUTTON,
...btn,
labels: { ...(btn.labels || {}) },
})),
})),
};
}
export const menuLayoutApi = {
getConfig: async (): Promise<MenuConfig> => {
const response = await apiClient.get<MenuConfig>('/cabinet/admin/menu-layout');
return normalizeConfig(response.data);
},
updateConfig: async (config: MenuConfig): Promise<MenuConfig> => {
const response = await apiClient.put<MenuConfig>('/cabinet/admin/menu-layout', config);
return normalizeConfig(response.data);
},
resetConfig: async (): Promise<MenuConfig> => {
const response = await apiClient.post<MenuConfig>('/cabinet/admin/menu-layout/reset');
return normalizeConfig(response.data);
},
};

View File

@@ -49,6 +49,16 @@ export interface UserRoleAssignment {
user_telegram_id: number | null;
}
export interface AdminWithRoles {
user_id: number;
telegram_id: number | null;
username: string | null;
first_name: string | null;
last_name: string | null;
email: string | null;
role_names: string[];
}
export interface AssignRolePayload {
user_id: number;
role_id: number;
@@ -167,6 +177,13 @@ export const rbacApi = {
return response.data;
},
// --- RBAC Users ---
getRbacUsers: async (): Promise<AdminWithRoles[]> => {
const response = await apiClient.get<AdminWithRoles[]>(`${BASE}/users`);
return response.data;
},
// --- Role Users ---
getRoleUsers: async (roleId: number): Promise<UserRoleAssignment[]> => {

View File

@@ -85,6 +85,8 @@ export interface TariffDetail {
daily_price_kopeks: number;
// Режим сброса трафика
traffic_reset_mode: string | null; // 'DAY', 'WEEK', 'MONTH', 'NO_RESET', null = глобальная настройка
// Внешний сквад RemnaWave
external_squad_uuid: string | null;
created_at: string;
updated_at: string | null;
}
@@ -121,6 +123,14 @@ export interface TariffCreateRequest {
daily_price_kopeks?: number;
// Режим сброса трафика
traffic_reset_mode?: string | null;
// Внешний сквад RemnaWave
external_squad_uuid?: string | null;
}
export interface ExternalSquadInfo {
uuid: string;
name: string;
members_count: number;
}
export interface TariffUpdateRequest {
@@ -156,6 +166,8 @@ export interface TariffUpdateRequest {
daily_price_kopeks?: number;
// Режим сброса трафика
traffic_reset_mode?: string | null;
// Внешний сквад RemnaWave
external_squad_uuid?: string | null;
}
export interface TariffToggleResponse {
@@ -249,4 +261,10 @@ export const tariffsApi = {
const response = await apiClient.get('/cabinet/admin/payment-methods/promo-groups');
return response.data;
},
// Get available external squads from RemnaWave
getAvailableExternalSquads: async (): Promise<ExternalSquadInfo[]> => {
const response = await apiClient.get('/cabinet/admin/tariffs/available-external-squads');
return response.data;
},
};