275 lines
10 KiB
TypeScript
275 lines
10 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',
|
|
},
|
|
users: [],
|
|
projects: [],
|
|
balance_history: [],
|
|
activity: [],
|
|
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');
|
|
});
|
|
|
|
it('activity: actor=actor_email; null → system', () => {
|
|
const ui = mapAdminTenantDetail(
|
|
makeApi({
|
|
activity: [
|
|
{
|
|
id: 1,
|
|
event: 'webhook.received',
|
|
deal_id: 100,
|
|
actor_email: null,
|
|
context: null,
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
},
|
|
{
|
|
id: 2,
|
|
event: 'deal.status_changed',
|
|
deal_id: 200,
|
|
actor_email: 'user@test.io',
|
|
context: { from: 'new', to: 'in_progress' },
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
expect(ui.activity[0]!.actor).toBe('system');
|
|
expect(ui.activity[1]!.actor).toBe('user@test.io');
|
|
expect(ui.activity[1]!.summary).toBe('Сделка #200: new → in_progress');
|
|
});
|
|
|
|
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('не активен');
|
|
});
|
|
});
|