Files
portal/app/tests/Frontend/adminTenantDetailMapper.spec.ts
T

360 lines
13 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { mapAdminTenantDetail } from '../../resources/js/composables/adminTenantDetailMapper';
import type { AdminTenantDetailResponse } from '../../resources/js/api/admin';
function makeApi(overrides: Partial<AdminTenantDetailResponse> = {}): AdminTenantDetailResponse {
return {
tenant: {
id: 1,
subdomain: 'test-co',
organization_name: 'Test ООО',
contact_email: 'admin@test-co.ru',
status: 'active',
balance_rub: '10000.00',
balance_leads: 5,
is_trial: false,
last_activity_at: null,
tariff_id: 1,
tariff_name: 'Команда',
mrr_rub: '990.00',
desired_daily_numbers: 10,
chargeback_unrecovered_rub: '0.00',
created_at: '2025-01-01T00:00:00+00:00',
aliveness_state: 'active',
},
users: [],
projects: [],
balance_history: [],
aliveness: {
state: 'no_data',
last_login_at: null,
days_since_login: null,
last_login_ip: null,
last_login_user_agent: null,
days: [],
counters: { visits_30d: 0, active_days_30d: 0, actions_30d: 0, failed_logins_30d: 0 },
},
activity: { items: [], offset: 0, limit: 50, has_more: false },
metrics: {
leads_today: 5,
leads_this_week: 30,
leads_this_month: 120,
avg_lead_cost_rub: 250,
runway_days: 40,
},
...overrides,
};
}
describe('adminTenantDetailMapper', () => {
it('маппит code = subdomain', () => {
const ui = mapAdminTenantDetail(makeApi());
expect(ui.code).toBe('test-co');
});
it('маппит name = organization_name', () => {
expect(mapAdminTenantDetail(makeApi()).name).toBe('Test ООО');
});
it('inn/contact_phone/legal_address пустые (нет в schema)', () => {
const ui = mapAdminTenantDetail(makeApi());
expect(ui.inn).toBe('');
expect(ui.contact_phone).toBe('');
expect(ui.legal_address).toBe('');
});
it('balanceRub парсится из string в number', () => {
expect(mapAdminTenantDetail(makeApi()).balanceRub).toBe(10000);
});
it('mrrRub: парсится в число для не-trial; null для trial', () => {
expect(mapAdminTenantDetail(makeApi()).mrrRub).toBe(990);
const trial = mapAdminTenantDetail(makeApi({ tenant: { ...makeApi().tenant, is_trial: true, mrr_rub: null } }));
expect(trial.mrrRub).toBeNull();
});
it('status derive: trial при is_trial=true', () => {
const ui = mapAdminTenantDetail(makeApi({ tenant: { ...makeApi().tenant, is_trial: true, mrr_rub: null } }));
expect(ui.status).toBe('trial');
expect(ui.statusText).toBe('Trial');
});
it('status derive: overdue при balance<0 или chargeback>0', () => {
const a = mapAdminTenantDetail(makeApi({ tenant: { ...makeApi().tenant, balance_rub: '-100.00' } }));
expect(a.status).toBe('overdue');
const b = mapAdminTenantDetail(
makeApi({ tenant: { ...makeApi().tenant, chargeback_unrecovered_rub: '500.00' } }),
);
expect(b.status).toBe('overdue');
});
it('status derive: suspended → suspended', () => {
const ui = mapAdminTenantDetail(makeApi({ tenant: { ...makeApi().tenant, status: 'suspended' } }));
expect(ui.status).toBe('suspended');
});
it('tariff: из tariff_name (или Trial если is_trial)', () => {
expect(mapAdminTenantDetail(makeApi()).tariff).toBe('Команда');
const trial = mapAdminTenantDetail(makeApi({ tenant: { ...makeApi().tenant, is_trial: true, mrr_rub: null } }));
expect(trial.tariff).toBe('Trial');
});
it('tariff fallback Trial если name не из known list', () => {
const ui = mapAdminTenantDetail(makeApi({ tenant: { ...makeApi().tenant, tariff_name: 'NotInList' } }));
expect(ui.tariff).toBe('Trial');
});
it('todayActual = metrics.leads_today; todayDesired = desired_daily_numbers', () => {
const ui = mapAdminTenantDetail(makeApi());
expect(ui.todayActual).toBe(5);
expect(ui.todayDesired).toBe(10);
});
it('users mapping: fullName из first+last; fallback на email', () => {
const ui = mapAdminTenantDetail(
makeApi({
users: [
{
id: 1,
email: 'a@b.ru',
first_name: 'Иван',
last_name: 'Петров',
is_active: true,
totp_enabled: false,
last_active_at: null,
last_login_at: null,
},
{
id: 2,
email: 'c@d.ru',
first_name: null,
last_name: null,
is_active: false,
totp_enabled: false,
last_active_at: null,
last_login_at: null,
},
],
}),
);
expect(ui.users[0]!.fullName).toBe('Иван Петров');
expect(ui.users[1]!.fullName).toBe('c@d.ru');
expect(ui.users[0]!.role).toBe('manager');
});
it('projects mapping: slug=tag, suppliers=suppliers_count, leadsToday/desiredToday', () => {
const ui = mapAdminTenantDetail(
makeApi({
projects: [
{
id: 1,
name: 'P1',
tag: 'p1-slug',
is_active: true,
daily_limit_target: 8,
suppliers_count: 3,
leads_today: 5,
},
],
}),
);
expect(ui.projects[0]!.slug).toBe('p1-slug');
expect(ui.projects[0]!.suppliers).toBe(3);
expect(ui.projects[0]!.leadsToday).toBe(5);
expect(ui.projects[0]!.desiredToday).toBe(8);
});
it('balanceHistory: id префиксуется TX-, amount парсится в number', () => {
const ui = mapAdminTenantDetail(
makeApi({
balance_history: [
{
id: 347,
type: 'topup',
amount_rub: '5000.00',
amount_leads: 0,
balance_rub_after: '15000.00',
description: 'X',
created_at: '2026-01-01T00:00:00Z',
},
],
}),
);
expect(ui.balanceHistory[0]!.id).toBe('TX-347');
expect(ui.balanceHistory[0]!.amount).toBe(5000);
expect(ui.balanceHistory[0]!.type).toBe('topup');
});
it('balanceHistory: chargeback_writedown → manual_adjustment, chargeback_repayment → topup', () => {
const ui = mapAdminTenantDetail(
makeApi({
balance_history: [
{
id: 1,
type: 'chargeback_writedown',
amount_rub: '-500',
amount_leads: 0,
balance_rub_after: null,
description: '',
created_at: '2026-01-01T00:00:00Z',
},
{
id: 2,
type: 'chargeback_repayment',
amount_rub: '500',
amount_leads: 0,
balance_rub_after: null,
description: '',
created_at: '2026-01-01T00:00:00Z',
},
],
}),
);
expect(ui.balanceHistory[0]!.type).toBe('manual_adjustment');
expect(ui.balanceHistory[1]!.type).toBe('topup');
});
// 07.08.2026: лента отдаётся страницей структурных дел, а не плоским
// массивом событий сделок с готовой строкой summary. Русскую фразу теперь
// складывает tenantActivityFormatters, маппер только переносит поля.
it('activity: переносит дела страницей и хранит признак «есть ещё»', () => {
const ui = mapAdminTenantDetail(
makeApi({
activity: {
items: [
{
id: 'tx-9',
at: '2026-01-01T00:00:00Z',
kind: 'money',
event: 'balance.topup',
actor: 'user@test.io',
subject: null,
amount_rub: '1000.00',
bulk_count: 0,
},
],
offset: 0,
limit: 50,
has_more: true,
},
}),
);
expect(ui.activity).toHaveLength(1);
expect(ui.activity[0]!.id).toBe('tx-9');
expect(ui.activity[0]!.kind).toBe('money');
expect(ui.activity[0]!.actor).toBe('user@test.io');
expect(ui.activity[0]!.amountRub).toBe(1000);
expect(ui.activityHasMore).toBe(true);
});
it('metrics: leadsThisMonth/Week/avgLeadCost/runwayDays', () => {
const ui = mapAdminTenantDetail(makeApi());
expect(ui.leadsThisMonth).toBe(120);
expect(ui.leadsThisWeek).toBe(30);
expect(ui.avgLeadCost).toBe(250);
expect(ui.runwayDays).toBe(40);
});
it('avgLeadCost / runwayDays default 0 если null', () => {
const ui = mapAdminTenantDetail(
makeApi({
metrics: {
leads_today: 0,
leads_this_week: 0,
leads_this_month: 0,
avg_lead_cost_rub: null,
runway_days: null,
},
}),
);
expect(ui.avgLeadCost).toBe(0);
expect(ui.runwayDays).toBe(0);
});
it('activitySince: «N мин назад» / «не активен» если null', () => {
const now = new Date('2026-05-09T12:00:00Z');
const ui1 = mapAdminTenantDetail(
makeApi({
tenant: {
...makeApi().tenant,
last_activity_at: '2026-05-09T11:30:00Z',
},
}),
now,
);
expect(ui1.activitySince).toBe('30 мин назад');
const ui2 = mapAdminTenantDetail(makeApi(), now);
expect(ui2.activitySince).toBe('не активен');
});
});
describe('маппер: блок «жив ли клиент»', () => {
const base = {
tenant: {
id: 11,
subdomain: 'ekb96',
organization_name: 'ekb96',
contact_email: 'ekb96@well.ru',
status: 'active',
balance_rub: '1320.00',
balance_leads: 0,
is_trial: true,
last_activity_at: null,
tariff_id: null,
tariff_name: null,
mrr_rub: null,
desired_daily_numbers: null,
chargeback_unrecovered_rub: '0.00',
created_at: '2026-08-05T07:04:34+00:00',
aliveness_state: 'cooling',
},
users: [],
projects: [],
balance_history: [],
aliveness: {
state: 'cooling',
last_login_at: '2026-08-06T03:23:12+00:00',
days_since_login: 1,
last_login_ip: '5.166.40.65',
last_login_user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
days: [{ date: '2026-08-06', visits: 1 }],
counters: { visits_30d: 2, active_days_30d: 2, actions_30d: 2, failed_logins_30d: 0 },
},
activity: {
items: [
{
id: 'ops-1',
at: '2026-08-05T07:34:48+00:00',
kind: 'project',
event: 'project.created',
actor: 'ekb96@well.ru',
subject: 'Окна ЕКБ',
amount_rub: null,
bulk_count: 0,
},
],
offset: 0,
limit: 50,
has_more: false,
},
metrics: {
leads_today: 0,
leads_this_week: 12,
leads_this_month: 12,
avg_lead_cost_rub: 15.25,
runway_days: 59,
},
} as never;
it('переносит вердикт и ленту без потерь', () => {
const mapped = mapAdminTenantDetail(base);
expect(mapped.aliveness.state).toBe('cooling');
expect(mapped.aliveness.daysSinceLogin).toBe(1);
expect(mapped.aliveness.days).toHaveLength(1);
expect(mapped.activity).toHaveLength(1);
expect(mapped.activity[0].subject).toBe('Окна ЕКБ');
expect(mapped.activityHasMore).toBe(false);
});
});