Files
portal/app/resources/js/api/admin.ts
T
Дмитрий fa11c7b223 phase2(admin-tenants-mrr): mrr_rub в /api/admin/tenants (этап 7)
Закрывает gap из v1.66 — mock-форма имеет mrrRub, но API возвращал null.
Теперь AdminTenantsView показывает реальную колонку MRR.

Backend (AdminTenantsController::index):
- Добавлено tariff_plans.price_monthly as tariff_price_monthly в select.
- mrr_rub в response: price_monthly (string) если не-trial; иначе null.
- Aggregate-формат как у /admin/billing — string чтобы decimal не терял
  точность при передаче через JSON.

Pest +3 (AdminTenantsIndexTest):
- mrr_rub='990.00' для активного тарифа не-trial.
- mrr_rub=null для trial (даже если тариф есть).
- mrr_rub=null если current_tariff_id отсутствует.

Frontend:
- ApiAdminTenant.mrr_rub: string | null в типе.
- mapApiAdminTenant: parseFloat(api.mrr_rub) или null (вместо hardcoded
  null из v1.66).
- AdminTenantsView: formatRub(item.mrrRub) для консистентности с другими
  ₽-полями.

Vitest +2:
- mrr_rub строка → number.
- mrr_rub=null → mrrRub null.

PHPStan baseline регенерирован. cspell-glossary +консистентности.

Регресс:
- Lint+type-check+format passed.
- Vitest 313/313 за 18.83 сек (+2 от 311).
- Vite build 947 ms.
- Pint + PHPStan passed.
- Pest 266/266 за 28.39 сек (+3 от 263, 1001 assertion).

Реестр v1.70→v1.71 / CLAUDE.md v1.61→v1.62.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:08:12 +03:00

268 lines
7.6 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;
}
export 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 → Биллинг: 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;
}
export 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;
}
export 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;
}