feat(lava): автопродление Lava на странице подписки

Парная фронтовая часть к эндпоинтам /cabinet/subscription/lava-recurrent
(бот: 0dea2182). Блок — сиблинг Platega-блока с той же семантикой состояний
(off/pending/active/past_due), поллинг раз в 8с пока привязка PENDING.

Отличие от Platega в подписи: у Lava период задан продуктом в кабинете
провайдера и приезжает числом дней, поэтому канонические значения (30/90/365
и т.д.) показываются словами, а произвольные — как «раз в N дн.».

utils/lavaRecurring.ts: строгое распознавание 403 'Lava recurrent disabled'
(другие 403-guard'ы прятать нельзя), деградация неизвестного статуса в off.
Локали ru/en/zh/fa (+25 строк каждая, плейсхолдеры сверены). 7 тестов утилиты.
This commit is contained in:
Fringg
2026-07-29 16:34:43 +03:00
parent 62aaa5d7c9
commit 2dce539da2
9 changed files with 474 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest';
import { isLavaFeatureDisabledError, lavaPeriodLabelKey, lavaUiState } from './lavaRecurring';
describe('isLavaFeatureDisabledError', () => {
it('detects exactly the disabled-feature 403', () => {
expect(
isLavaFeatureDisabledError({
response: { status: 403, data: { detail: 'Lava recurrent disabled' } },
}),
).toBe(true);
});
it('ignores other 403s and malformed errors', () => {
// Другие guard'ы отдают тот же статус с иным detail — их прятать нельзя
expect(
isLavaFeatureDisabledError({ response: { status: 403, data: { detail: 'Blacklisted' } } }),
).toBe(false);
expect(
isLavaFeatureDisabledError({ response: { status: 403, data: { detail: { code: 'x' } } } }),
).toBe(false);
expect(isLavaFeatureDisabledError({ response: { status: 500, data: {} } })).toBe(false);
expect(isLavaFeatureDisabledError({ response: { status: 403 } })).toBe(false);
expect(isLavaFeatureDisabledError(null)).toBe(false);
expect(isLavaFeatureDisabledError('boom')).toBe(false);
});
});
describe('lavaUiState', () => {
it('hides the block when the feature is disabled', () => {
expect(lavaUiState({ status: 'ACTIVE' }, true)).toBe('hidden');
});
it('maps backend statuses to UI states', () => {
expect(lavaUiState({ status: 'PENDING' }, false)).toBe('pending');
expect(lavaUiState({ status: 'ACTIVE' }, false)).toBe('active');
expect(lavaUiState({ status: 'PAST_DUE' }, false)).toBe('past_due');
expect(lavaUiState({ status: 'none' }, false)).toBe('off');
});
it('degrades unknown or missing state to off instead of hanging', () => {
expect(lavaUiState({ status: 'SOMETHING_NEW' }, false)).toBe('off');
expect(lavaUiState(undefined, false)).toBe('off');
});
});
describe('lavaPeriodLabelKey', () => {
it('labels canonical periods', () => {
expect(lavaPeriodLabelKey(30)).toBe('subscription.lavaRecurring.period.month');
expect(lavaPeriodLabelKey(365)).toBe('subscription.lavaRecurring.period.year');
});
it('returns null for non-canonical periods so the caller falls back to "N days"', () => {
expect(lavaPeriodLabelKey(45)).toBeNull();
expect(lavaPeriodLabelKey(undefined)).toBeNull();
});
});