mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-09-16 01:33:09 +00:00
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:
56
src/utils/lavaRecurring.test.ts
Normal file
56
src/utils/lavaRecurring.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
77
src/utils/lavaRecurring.ts
Normal file
77
src/utils/lavaRecurring.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { LavaRecurringInfo } from '../types';
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
function isRecord(value: unknown): value is UnknownRecord {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* true, если ошибка — это ровно "автопродление Lava выключено на бэке"
|
||||
* (403 с detail==='Lava recurrent disabled'). Сравнение строгое: тот же
|
||||
* 403-статус используют другие guard'ы с detail-объектом, и на любой форме
|
||||
* detail (строка/объект/отсутствует) функция не должна падать.
|
||||
*/
|
||||
export function isLavaFeatureDisabledError(error: unknown): boolean {
|
||||
if (!isRecord(error)) return false;
|
||||
|
||||
const response = error.response;
|
||||
if (!isRecord(response)) return false;
|
||||
if (response.status !== 403) return false;
|
||||
|
||||
const data = response.data;
|
||||
if (!isRecord(data)) return false;
|
||||
|
||||
return data.detail === 'Lava recurrent disabled';
|
||||
}
|
||||
|
||||
export type LavaUiState = 'hidden' | 'off' | 'pending' | 'active' | 'past_due';
|
||||
|
||||
/**
|
||||
* Сводит статус автопродления Lava к состоянию UI. Бэк отдаёт из GET только
|
||||
* 'none' | 'PENDING' | 'ACTIVE' | 'PAST_DUE' (CANCELLED/FAILED терминальны и
|
||||
* не возвращаются), но любой нераспознанный статус деградирует в 'off', а не
|
||||
* подвешивает интерфейс.
|
||||
*/
|
||||
export function lavaUiState(
|
||||
info: LavaRecurringInfo | undefined,
|
||||
featureDisabled: boolean,
|
||||
): LavaUiState {
|
||||
if (featureDisabled) return 'hidden';
|
||||
if (!info) return 'off';
|
||||
|
||||
switch (info.status) {
|
||||
case 'PENDING':
|
||||
return 'pending';
|
||||
case 'ACTIVE':
|
||||
return 'active';
|
||||
case 'PAST_DUE':
|
||||
return 'past_due';
|
||||
default:
|
||||
return 'off';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Локализационный ключ периодичности списания. У Lava период задан продуктом
|
||||
* в кабинете провайдера и приезжает числом дней, поэтому точные подписи есть
|
||||
* только для канонических значений; остальное показывается как "N дней".
|
||||
*/
|
||||
export function lavaPeriodLabelKey(chargeDays: number | undefined): string | null {
|
||||
switch (chargeDays) {
|
||||
case 1:
|
||||
return 'subscription.lavaRecurring.period.day';
|
||||
case 7:
|
||||
return 'subscription.lavaRecurring.period.week';
|
||||
case 30:
|
||||
return 'subscription.lavaRecurring.period.month';
|
||||
case 90:
|
||||
return 'subscription.lavaRecurring.period.quarter';
|
||||
case 180:
|
||||
return 'subscription.lavaRecurring.period.halfYear';
|
||||
case 365:
|
||||
return 'subscription.lavaRecurring.period.year';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user