360 lines
13 KiB
TypeScript
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);
|
|
});
|
|
});
|