497 lines
14 KiB
TypeScript
497 lines
14 KiB
TypeScript
import { apiClient, ensureCsrfCookie } from './client';
|
|
|
|
/**
|
|
* API-вызовы для админских endpoint'ов SaaS (см. ImpersonationController).
|
|
*
|
|
* На MVP вызываются без auth:saas-admin middleware (см. routes/web.php).
|
|
* Production: middleware('auth:saas-admin') + cookie session — apiClient уже
|
|
* настроен на withCredentials.
|
|
*/
|
|
|
|
export interface ImpersonationInitPayload {
|
|
tenant_id: number;
|
|
requested_by: number; // на MVP параметром; на prod — request()->user()->id
|
|
reason: string; // ≥30 chars (валидируется на backend)
|
|
}
|
|
|
|
export interface ImpersonationInitResponse {
|
|
token_id: number;
|
|
expires_at: string; // ISO8601
|
|
sent_to_email: string;
|
|
/** dev-only: исчезнет после интеграции MailService на prod */
|
|
_dev_plain_code?: string;
|
|
}
|
|
|
|
export async function impersonationInit(payload: ImpersonationInitPayload): Promise<ImpersonationInitResponse> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.post<ImpersonationInitResponse>('/api/admin/impersonation/init', payload);
|
|
return data;
|
|
}
|
|
|
|
export interface ImpersonationVerifyPayload {
|
|
token_id: number;
|
|
code: string; // 6 цифр
|
|
}
|
|
|
|
export interface ImpersonationVerifyResponse {
|
|
token_id: number;
|
|
tenant_id: number;
|
|
used_at: string;
|
|
message: string;
|
|
}
|
|
|
|
export async function impersonationVerify(payload: ImpersonationVerifyPayload): Promise<ImpersonationVerifyResponse> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.post<ImpersonationVerifyResponse>('/api/admin/impersonation/verify', payload);
|
|
return data;
|
|
}
|
|
|
|
export interface ImpersonationEndResponse {
|
|
token_id: number;
|
|
session_ended_at: string;
|
|
message: string;
|
|
}
|
|
|
|
export async function impersonationEnd(tokenId: number): Promise<ImpersonationEndResponse> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.post<ImpersonationEndResponse>('/api/admin/impersonation/end', {
|
|
token_id: tokenId,
|
|
});
|
|
return data;
|
|
}
|
|
|
|
export interface ImpersonationActiveSession {
|
|
token_id: number;
|
|
tenant_id: number;
|
|
tenant_name: string | null;
|
|
requested_by: number;
|
|
reason: string;
|
|
sent_to_email: string;
|
|
used_at: string;
|
|
expires_at: string;
|
|
}
|
|
|
|
export interface ImpersonationRecentSession {
|
|
token_id: number;
|
|
tenant_id: number;
|
|
tenant_name: string | null;
|
|
requested_by: number;
|
|
reason: string;
|
|
used_at: string;
|
|
session_ended_at: string;
|
|
duration_seconds: number | null;
|
|
}
|
|
|
|
export async function impersonationActive(): Promise<ImpersonationActiveSession[]> {
|
|
const { data } = await apiClient.get<{ sessions: ImpersonationActiveSession[] }>('/api/admin/impersonation/active');
|
|
return data.sessions;
|
|
}
|
|
|
|
export async function impersonationRecent(): Promise<ImpersonationRecentSession[]> {
|
|
const { data } = await apiClient.get<{ sessions: ImpersonationRecentSession[] }>('/api/admin/impersonation/recent');
|
|
return data.sessions;
|
|
}
|
|
|
|
// === SaaS-admin → Тенанты: lookup для AdminTenantsView ===
|
|
|
|
export interface AdminTenant {
|
|
id: number;
|
|
subdomain: string;
|
|
organization_name: string;
|
|
contact_email: string;
|
|
status: string;
|
|
balance_rub: string;
|
|
balance_leads: number;
|
|
is_trial: boolean;
|
|
last_activity_at: string | null;
|
|
tariff_id: number | null;
|
|
tariff_name: string | null;
|
|
/** price_monthly активного тарифа если не-trial; иначе null. */
|
|
mrr_rub: string | null;
|
|
desired_daily_numbers: number | null;
|
|
chargeback_unrecovered_rub: string;
|
|
created_at: string | null;
|
|
}
|
|
|
|
interface AdminTenantsStats {
|
|
total: number;
|
|
active: number;
|
|
trial: number;
|
|
overdue: number;
|
|
}
|
|
|
|
export interface ListAdminTenantsParams {
|
|
status?: string;
|
|
search?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export interface ListAdminTenantsResponse {
|
|
tenants: AdminTenant[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
stats: AdminTenantsStats;
|
|
}
|
|
|
|
export async function listAdminTenants(params: ListAdminTenantsParams = {}): Promise<ListAdminTenantsResponse> {
|
|
const { data } = await apiClient.get<ListAdminTenantsResponse>('/api/admin/tenants', { params });
|
|
return data;
|
|
}
|
|
|
|
// === SaaS-admin → Тенанты → детали (для AdminTenantDetailView) ===
|
|
|
|
export interface ApiTenantUser {
|
|
id: number;
|
|
email: string;
|
|
first_name: string | null;
|
|
last_name: string | null;
|
|
is_active: boolean;
|
|
totp_enabled: boolean;
|
|
last_active_at: string | null;
|
|
last_login_at: string | null;
|
|
}
|
|
|
|
export interface ApiTenantProject {
|
|
id: number;
|
|
name: string;
|
|
tag: string | null;
|
|
is_active: boolean;
|
|
daily_limit_target: number;
|
|
suppliers_count: number;
|
|
leads_today: number;
|
|
}
|
|
|
|
export interface ApiTenantBalanceTx {
|
|
id: number;
|
|
type: string;
|
|
amount_rub: string;
|
|
amount_leads: number;
|
|
balance_rub_after: string | null;
|
|
description: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ApiTenantActivityEvent {
|
|
id: number;
|
|
event: string;
|
|
deal_id: number;
|
|
actor_email: string | null;
|
|
context: Record<string, unknown> | null;
|
|
created_at: string;
|
|
}
|
|
|
|
interface ApiTenantMetrics {
|
|
leads_today: number;
|
|
leads_this_week: number;
|
|
leads_this_month: number;
|
|
avg_lead_cost_rub: number | null;
|
|
runway_days: number | null;
|
|
}
|
|
|
|
export interface AdminTenantDetailResponse {
|
|
tenant: AdminTenant;
|
|
users: ApiTenantUser[];
|
|
projects: ApiTenantProject[];
|
|
balance_history: ApiTenantBalanceTx[];
|
|
activity: ApiTenantActivityEvent[];
|
|
metrics: ApiTenantMetrics;
|
|
}
|
|
|
|
export async function getAdminTenantDetail(subdomain: string): Promise<AdminTenantDetailResponse> {
|
|
const { data } = await apiClient.get<AdminTenantDetailResponse>(
|
|
`/api/admin/tenants/${encodeURIComponent(subdomain)}`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
// === SaaS-admin → Биллинг: aggregates пополнений/списаний ===
|
|
|
|
export interface ApiAdminBillingTenant {
|
|
id: number;
|
|
subdomain: string;
|
|
organization_name: string;
|
|
contact_email: string;
|
|
status: string;
|
|
balance_rub: string;
|
|
tariff_id: number | null;
|
|
tariff_name: string | null;
|
|
mrr_rub: string;
|
|
monthly_topups_rub: string;
|
|
monthly_charges_rub: string;
|
|
last_payment_at: string | null;
|
|
chargeback_unrecovered_rub: string;
|
|
}
|
|
|
|
interface ApiAdminBillingSummary {
|
|
total_mrr_rub: string;
|
|
monthly_revenue_rub: string;
|
|
overdue_count: number;
|
|
refunds_count_30d: number;
|
|
}
|
|
|
|
export interface ListAdminBillingResponse {
|
|
tenants: ApiAdminBillingTenant[];
|
|
summary: ApiAdminBillingSummary;
|
|
}
|
|
|
|
export async function listAdminBilling(search = ''): Promise<ListAdminBillingResponse> {
|
|
const { data } = await apiClient.get<ListAdminBillingResponse>('/api/admin/billing', {
|
|
params: { search },
|
|
});
|
|
return data;
|
|
}
|
|
|
|
// === SaaS-admin → Инциденты ===
|
|
|
|
export interface ApiAdminIncident {
|
|
id: number;
|
|
incident_id: string;
|
|
type: string;
|
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
summary: string;
|
|
started_at: string;
|
|
detected_at: string;
|
|
resolved_at: string | null;
|
|
status: 'open' | 'investigating' | 'resolved';
|
|
affected_tenants_count: number;
|
|
affected_users_count: number | null;
|
|
rkn_notified: boolean;
|
|
rkn_notified_at: string | null;
|
|
rkn_deadline_at: string | null;
|
|
}
|
|
|
|
interface ApiAdminIncidentsSummary {
|
|
open: number;
|
|
investigating: number;
|
|
rkn_pending: number;
|
|
total_unresolved: number;
|
|
}
|
|
|
|
export interface ListAdminIncidentsParams {
|
|
type?: string;
|
|
severity?: string;
|
|
unresolved_only?: boolean;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export interface ListAdminIncidentsResponse {
|
|
incidents: ApiAdminIncident[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
summary: ApiAdminIncidentsSummary;
|
|
}
|
|
|
|
export async function listAdminIncidents(params: ListAdminIncidentsParams = {}): Promise<ListAdminIncidentsResponse> {
|
|
const { data } = await apiClient.get<ListAdminIncidentsResponse>('/api/admin/incidents', { params });
|
|
return data;
|
|
}
|
|
|
|
// === SaaS-admin → Система: system_settings edit-flow ===
|
|
|
|
export interface SystemSetting {
|
|
key: string;
|
|
value: string;
|
|
type: 'int' | 'string' | 'decimal' | 'bool' | 'json';
|
|
description: string | null;
|
|
updated_at: string;
|
|
updated_by: number | null;
|
|
}
|
|
|
|
export async function listSystemSettings(): Promise<SystemSetting[]> {
|
|
const { data } = await apiClient.get<{ settings: SystemSetting[] }>('/api/admin/system-settings');
|
|
return data.settings;
|
|
}
|
|
|
|
export interface UpdateSystemSettingPayload {
|
|
value: string;
|
|
reason: string; // ≥30 chars
|
|
admin_user_id: number; // на prod удалится
|
|
}
|
|
|
|
export interface UpdateSystemSettingResponse {
|
|
key: string;
|
|
value: string;
|
|
previous_value: string;
|
|
updated_at: string;
|
|
message: string;
|
|
}
|
|
|
|
export async function updateSystemSetting(
|
|
key: string,
|
|
payload: UpdateSystemSettingPayload,
|
|
): Promise<UpdateSystemSettingResponse> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.put<UpdateSystemSettingResponse>(
|
|
`/api/admin/system-settings/${encodeURIComponent(key)}`,
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
// === SaaS-admin → Биллинг: row-actions (Sprint 3D G4) ===
|
|
|
|
export interface AdminTariffPlan {
|
|
id: number;
|
|
name: string;
|
|
price_monthly: string;
|
|
}
|
|
|
|
export async function listAdminTariffPlans(): Promise<AdminTariffPlan[]> {
|
|
const { data } = await apiClient.get<{ plans: AdminTariffPlan[] }>('/api/admin/billing/tariff-plans');
|
|
return data.plans;
|
|
}
|
|
|
|
export async function updateTenantStatus(
|
|
id: number,
|
|
status: 'active' | 'suspended',
|
|
reason: string,
|
|
): Promise<{ id: number; status: string }> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.patch<{ id: number; status: string }>(
|
|
`/api/admin/billing/tenants/${id}/status`,
|
|
{ status, reason },
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function refundTenant(
|
|
id: number,
|
|
amountRub: number,
|
|
reason: string,
|
|
): Promise<{ id: number; balance_rub: string; transaction_id: number }> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.post<{ id: number; balance_rub: string; transaction_id: number }>(
|
|
`/api/admin/billing/tenants/${id}/refund`,
|
|
{ amount_rub: amountRub, reason },
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function changeTenantTariff(
|
|
id: number,
|
|
tariffId: number,
|
|
reason: string,
|
|
): Promise<{ id: number; tariff_id: number; tariff_name: string }> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.patch<{ id: number; tariff_id: number; tariff_name: string }>(
|
|
`/api/admin/billing/tenants/${id}/tariff`,
|
|
{ tariff_id: tariffId, reason },
|
|
);
|
|
return data;
|
|
}
|
|
|
|
// === SaaS-admin → Инциденты: detail-view + РКН-notify (Sprint 3D G5/G6) ===
|
|
|
|
export interface ApiIncidentAffectedTenant {
|
|
id: number;
|
|
organization_name: string;
|
|
}
|
|
|
|
export interface ApiAdminIncidentDetail {
|
|
id: number;
|
|
incident_id: string;
|
|
type: string;
|
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
summary: string;
|
|
root_cause: string | null;
|
|
postmortem_url: string | null;
|
|
started_at: string;
|
|
detected_at: string;
|
|
resolved_at: string | null;
|
|
status: 'open' | 'investigating' | 'resolved';
|
|
affected_tenants: ApiIncidentAffectedTenant[];
|
|
affected_users_count: number | null;
|
|
notification_sent_at: string | null;
|
|
rkn_notified: boolean;
|
|
rkn_notified_at: string | null;
|
|
rkn_deadline_at: string | null;
|
|
created_by_admin: string | null;
|
|
closed_by_admin: string | null;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export async function getAdminIncidentDetail(id: number): Promise<ApiAdminIncidentDetail> {
|
|
const { data } = await apiClient.get<{ incident: ApiAdminIncidentDetail }>(`/api/admin/incidents/${id}`);
|
|
return data.incident;
|
|
}
|
|
|
|
export async function notifyIncidentRkn(id: number): Promise<ApiAdminIncidentDetail> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.post<{ incident: ApiAdminIncidentDetail }>(
|
|
`/api/admin/incidents/${id}/rkn-notify`,
|
|
{},
|
|
);
|
|
return data.incident;
|
|
}
|
|
|
|
// === SaaS-admin → Тарифная сетка (Plan 4 / Sprint 5C G3) ===
|
|
|
|
export interface AdminPricingTier {
|
|
tier_no: number;
|
|
leads_in_tier: number | null;
|
|
price_per_lead_kopecks: number;
|
|
effective_from: string;
|
|
}
|
|
|
|
export interface PricingTiersResponse {
|
|
active: AdminPricingTier[];
|
|
scheduled: Record<string, AdminPricingTier[]>;
|
|
}
|
|
|
|
export interface PricingTierEditorRow {
|
|
tier_no: number;
|
|
leads_in_tier: number | null;
|
|
price_rub: string;
|
|
}
|
|
|
|
export async function getPricingTiers(): Promise<PricingTiersResponse> {
|
|
const { data } = await apiClient.get<{ data: PricingTiersResponse }>('/api/admin/pricing-tiers');
|
|
return { active: data.data.active, scheduled: data.data.scheduled ?? {} };
|
|
}
|
|
|
|
export async function createPricingTiers(
|
|
tiers: PricingTierEditorRow[],
|
|
effectiveFrom?: string,
|
|
): Promise<{ effective_from: string }> {
|
|
await ensureCsrfCookie();
|
|
const payload: { tiers: PricingTierEditorRow[]; effective_from?: string } = { tiers };
|
|
if (effectiveFrom) payload.effective_from = effectiveFrom;
|
|
const { data } = await apiClient.post<{ effective_from: string }>('/api/admin/pricing-tiers', payload);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteScheduledPricingTier(effectiveFrom: string): Promise<void> {
|
|
await ensureCsrfCookie();
|
|
await apiClient.delete(`/api/admin/pricing-tiers/scheduled/${effectiveFrom}`);
|
|
}
|
|
|
|
// === SaaS-admin → Цены поставщиков (Plan 4 / Sprint 5C G3) ===
|
|
|
|
export interface AdminSupplier {
|
|
id: number;
|
|
code: string;
|
|
name: string;
|
|
cost_rub: string;
|
|
quality_score: string;
|
|
is_active: boolean;
|
|
}
|
|
|
|
export async function getAdminSuppliers(): Promise<AdminSupplier[]> {
|
|
const { data } = await apiClient.get<{ data: AdminSupplier[] }>('/api/admin/suppliers');
|
|
return data.data;
|
|
}
|
|
|
|
export async function updateAdminSupplier(
|
|
id: number,
|
|
payload: { cost_rub: string; quality_score: string; is_active: boolean },
|
|
): Promise<AdminSupplier> {
|
|
await ensureCsrfCookie();
|
|
const { data } = await apiClient.patch<{ data: AdminSupplier }>(`/api/admin/suppliers/${id}`, payload);
|
|
return data.data;
|
|
}
|