fix(install): разблокировать мастер при прерванной установке и валидировать состав совета по окружению.
Не показываем заглушку техобслуживания на /install, разрешаем повтор install из maintenance без vars, проверяем минимум членов совета до записи в цепь (3 на prod, 1 на dev). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,9 @@ import { Workflows } from '@coopenomics/notifications';
|
||||
import { TokenApplicationService } from '~/application/token/services/token-application.service';
|
||||
import { normalizeUserEmail } from '~/utils/normalize-user-email';
|
||||
|
||||
/** Минимум членов совета — как MIN_SOVIET_MEMBERS_COUNT в контракте soviet (3 prod / 1 dev). */
|
||||
const MIN_SOVIET_MEMBERS_COUNT = config.min_soviet_members_count;
|
||||
|
||||
@Injectable()
|
||||
export class InstallInteractor {
|
||||
constructor(
|
||||
@@ -118,14 +121,30 @@ export class InstallInteractor {
|
||||
|
||||
async install(data: InstallInputDomainInterface): Promise<void> {
|
||||
const status = await this.monoStatusRepository.getStatus();
|
||||
const savedVars = await this.varsRepository.get();
|
||||
const isIncompleteInstallMaintenance =
|
||||
status === SystemStatus.maintenance && !savedVars?.name;
|
||||
|
||||
// Разрешаем установку ТОЛЬКО если система в статусе 'initialized' (после initSystem)
|
||||
if (status !== SystemStatus.initialized) {
|
||||
// Разрешаем установку если:
|
||||
// - initialized (нормальный прогон)
|
||||
// - maintenance без сохранённых vars (прерванная установка, можно повторить)
|
||||
if (status !== SystemStatus.initialized && !isIncompleteInstallMaintenance) {
|
||||
throw new BadRequestException(
|
||||
`Установка невозможна. Текущий статус: ${status}. Требуется статус: ${SystemStatus.initialized}`
|
||||
);
|
||||
}
|
||||
|
||||
if (data.soviet.length < MIN_SOVIET_MEMBERS_COUNT) {
|
||||
throw new BadRequestException(
|
||||
`Количество членов совета должно быть не менее ${MIN_SOVIET_MEMBERS_COUNT}`
|
||||
);
|
||||
}
|
||||
|
||||
// Повтор после прерванной установки: снимаем maintenance, дальше — обычный прогон.
|
||||
if (isIncompleteInstallMaintenance) {
|
||||
await this.monoStatusRepository.setStatus(SystemStatus.initialized);
|
||||
}
|
||||
|
||||
const info = await this.blockchainPort.getInfo();
|
||||
const coop = await this.blockchainPort.getCooperative(config.coopname);
|
||||
|
||||
|
||||
@@ -189,6 +189,10 @@ const envVarsSchema = z.object({
|
||||
.string()
|
||||
.optional()
|
||||
.describe('База публичного URL контроллера для read-URL; пусто — берётся BACKEND_URL'),
|
||||
MIN_SOVIET_MEMBERS_COUNT: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Минимум членов совета при install; пусто — 3 на production, 1 на development/test'),
|
||||
});
|
||||
|
||||
const envInput = isSchemaGeneration ? { ...SCHEMA_GEN_ENV_DEFAULTS, ...process.env } : process.env;
|
||||
@@ -260,6 +264,11 @@ export default {
|
||||
},
|
||||
},
|
||||
coopname: envVars.data.COOPNAME,
|
||||
min_soviet_members_count: envVars.data.MIN_SOVIET_MEMBERS_COUNT
|
||||
? parseInt(envVars.data.MIN_SOVIET_MEMBERS_COUNT, 10)
|
||||
: envVars.data.NODE_ENV === 'production'
|
||||
? 3
|
||||
: 1,
|
||||
graphql_service: envVars.data.GRAPHQL_SERVICE,
|
||||
provider_base_url: envVars.data.PROVIDER_BASE_URL,
|
||||
union: {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { getMinSovietMembersCount } from './minSovietMembers';
|
||||
@@ -0,0 +1,6 @@
|
||||
import { env } from 'src/shared/config';
|
||||
|
||||
/** Как MIN_SOVIET_MEMBERS_COUNT в контракте soviet: 1 на dev/testnet, 3 на production. */
|
||||
export function getMinSovietMembersCount(): number {
|
||||
return env.NODE_ENV === 'production' ? 3 : 1;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Mutations, Queries } from '@coopenomics/sdk';
|
||||
import { useInstallCooperativeStore } from 'src/entities/Installer/model'
|
||||
import { stripLegacyBankKpp } from 'src/shared/lib/utils/stripLegacyBankKpp';
|
||||
import { getMinSovietMembersCount } from '../lib';
|
||||
import { api } from '../api'
|
||||
|
||||
export type IInstallInput = Mutations.System.InstallSystem.IInput['data']
|
||||
@@ -30,7 +32,15 @@ export const useInstallCooperative = (): {
|
||||
}
|
||||
|
||||
async function initSystem(data: IInitSystemInput): Promise<IInitSystemOutput> {
|
||||
return await api.initSystem(data);
|
||||
const payload: IInitSystemInput = data.organization_data
|
||||
? {
|
||||
...data,
|
||||
// TODO(удалить после 01.09.2026): strip устаревший «КПП банка» из ответа getInstallationStatus
|
||||
organization_data: stripLegacyBankKpp(data.organization_data),
|
||||
}
|
||||
: data;
|
||||
|
||||
return await api.initSystem(payload);
|
||||
}
|
||||
|
||||
async function getInstallationStatus(installCode: string): Promise<IGetInstallationStatusOutput> {
|
||||
@@ -41,8 +51,15 @@ export const useInstallCooperative = (): {
|
||||
if (!store.vars)
|
||||
throw new Error('Переменные не установлены')
|
||||
|
||||
if (!store.soviet || store.soviet.length === 0)
|
||||
throw new Error('Совет не установлен')
|
||||
const minSovietMembers = getMinSovietMembersCount();
|
||||
|
||||
if (!store.soviet || store.soviet.length < minSovietMembers) {
|
||||
throw new Error(
|
||||
minSovietMembers === 1
|
||||
? 'Совет не установлен'
|
||||
: `Совет должен включать не менее ${minSovietMembers} человек (председатель и члены совета)`,
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const soviet = store.soviet.map(({ id, type, ...rest }) => rest);
|
||||
|
||||
@@ -80,6 +80,7 @@ import { FailAlert } from 'src/shared/api';
|
||||
import { extractGraphQLErrorMessages } from 'src/shared/api/errors';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { notEmpty } from 'src/shared/lib/utils';
|
||||
import { stripLegacyBankKpp } from 'src/shared/lib/utils/stripLegacyBankKpp';
|
||||
import { Zeus } from '@coopenomics/sdk';
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton';
|
||||
|
||||
@@ -187,7 +188,7 @@ const loadData = async () => {
|
||||
organizationData.type = organizationData.type.toUpperCase();
|
||||
}
|
||||
|
||||
installStore.organization_data = organizationData;
|
||||
installStore.organization_data = stripLegacyBankKpp(organizationData);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = extractGraphQLErrorMessages(error);
|
||||
|
||||
@@ -43,16 +43,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useInstallCooperativeStore } from 'src/entities/Installer/model';
|
||||
import { IndividualDataForm } from 'src/shared/ui/UserDataForm/IndividualDataForm';
|
||||
import type { IIndividualData } from 'src/shared/lib/types/user/IUserData';
|
||||
import { FailAlert } from 'src/shared/api';
|
||||
import { ref } from 'vue';
|
||||
import { validEmail } from 'src/shared/lib/utils/validEmailRule';
|
||||
import { getMinSovietMembersCount } from '../../lib';
|
||||
import { notEmpty } from 'src/shared/lib/utils';
|
||||
import { validEmail } from 'src/shared/lib/utils/validEmailRule';
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton';
|
||||
|
||||
const installStore = useInstallCooperativeStore();
|
||||
const minSovietMembers = getMinSovietMembersCount();
|
||||
|
||||
installStore.is_finish = false;
|
||||
|
||||
@@ -74,10 +76,16 @@ const back = () => {
|
||||
installStore.current_step = 'init';
|
||||
};
|
||||
|
||||
const MIN_SOVIET_MEMBERS = minSovietMembers;
|
||||
|
||||
const next = async () => {
|
||||
try {
|
||||
if (installStore.soviet.length === 0) {
|
||||
FailAlert('Необходимо добавить хотя бы одного члена совета');
|
||||
if (installStore.soviet.length < MIN_SOVIET_MEMBERS) {
|
||||
FailAlert(
|
||||
MIN_SOVIET_MEMBERS === 1
|
||||
? 'Необходимо добавить хотя бы одного члена совета'
|
||||
: `Необходимо добавить не менее ${MIN_SOVIET_MEMBERS} членов совета (председатель и члены совета)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,8 +98,11 @@ const next = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (installStore.soviet.length == 0)
|
||||
add();
|
||||
if (installStore.soviet.length == 0) {
|
||||
for (let i = 0; i < minSovietMembers; i++) {
|
||||
add();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
|
||||
//- ---------- Шаг 3: члены совета ----------
|
||||
.install-step(v-else-if='step.key === "soviet"')
|
||||
p.install-step__intro
|
||||
| Введите данные председателя и членов совета. Каждому будет создан
|
||||
| аккаунт пайщика и отправлено приглашение на электронную почту.
|
||||
p.install-step__intro {{ sovietStepIntro }}
|
||||
SetSovietForm
|
||||
|
||||
//- ---------- Шаг 4: переменные документов ----------
|
||||
@@ -60,10 +58,17 @@ import { useSystemStore } from 'src/entities/System/model';
|
||||
import { VerticalStepper } from 'src/shared/ui/domain/VerticalStepper';
|
||||
import type { StepperStep } from 'src/shared/ui/domain/VerticalStepper';
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton';
|
||||
import { getMinSovietMembersCount } from 'src/features/Installer/lib';
|
||||
|
||||
const router = useRouter();
|
||||
const systemStore = useSystemStore();
|
||||
const installStore = useInstallCooperativeStore();
|
||||
const minSovietMembers = getMinSovietMembersCount();
|
||||
|
||||
const sovietStepIntro =
|
||||
minSovietMembers === 1
|
||||
? 'Введите данные председателя. Будет создан аккаунт пайщика и отправлено приглашение на электронную почту.'
|
||||
: 'Введите данные председателя и не менее двух членов совета (всего минимум 3 человека). Каждому будет создан аккаунт пайщика и отправлено приглашение на электронную почту.';
|
||||
|
||||
const steps: StepperStep[] = [
|
||||
{ key: 'key', label: 'Ключ установки', description: 'Ключ, выданный при регистрации пайщиком' },
|
||||
|
||||
@@ -26,7 +26,15 @@ export function setupNavigationGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// если требуется установка
|
||||
const allowedRoutesDuringInstall = ['install', 'invite'];
|
||||
if ((systemStore.info.system_status === Zeus.SystemStatus.install || systemStore.info.system_status === Zeus.SystemStatus.initialized) && !allowedRoutesDuringInstall.includes(to.name as string)) {
|
||||
const isIncompleteInstallMaintenance =
|
||||
systemStore.info.system_status === Zeus.SystemStatus.maintenance &&
|
||||
!systemStore.info.vars?.name;
|
||||
const requiresInstallFlow =
|
||||
systemStore.info.system_status === Zeus.SystemStatus.install ||
|
||||
systemStore.info.system_status === Zeus.SystemStatus.initialized ||
|
||||
isIncompleteInstallMaintenance;
|
||||
|
||||
if (requiresInstallFlow && !allowedRoutesDuringInstall.includes(to.name as string)) {
|
||||
next({ name: 'install', params: { coopname: systemStore.info.coopname }, query: to.query });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,18 @@ import { QSpinnerGears, useQuasar } from 'quasar';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { Zeus } from '@coopenomics/sdk';
|
||||
|
||||
/** Мастер установки не должен перекрываться заглушкой техобслуживания. */
|
||||
function isOnInstallPage(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const path = window.location.hash || window.location.pathname;
|
||||
return path.includes('/install');
|
||||
}
|
||||
|
||||
/** maintenance без сохранённых vars — прерванная установка, а не плановое обслуживание. */
|
||||
function isIncompleteInstallMaintenance(systemStatus: string, hasSavedVars: boolean): boolean {
|
||||
return systemStatus === Zeus.SystemStatus.maintenance && !hasSavedVars;
|
||||
}
|
||||
|
||||
export function useDesktopHealthWatcherProcess() {
|
||||
const $q = useQuasar();
|
||||
const systemStore = useSystemStore();
|
||||
@@ -11,6 +23,7 @@ export function useDesktopHealthWatcherProcess() {
|
||||
|
||||
// Создаем computed для лучшей реактивности
|
||||
const systemStatus = computed(() => systemStore.info.system_status);
|
||||
const hasSavedVars = computed(() => Boolean(systemStore.info.vars?.name));
|
||||
|
||||
const enableLoading = () => {
|
||||
$q.loading.show({
|
||||
@@ -25,7 +38,12 @@ export function useDesktopHealthWatcherProcess() {
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
if (systemStatus.value === 'maintenance') {
|
||||
const blockedByMaintenance =
|
||||
systemStatus.value === Zeus.SystemStatus.maintenance &&
|
||||
!isIncompleteInstallMaintenance(systemStatus.value, hasSavedVars.value) &&
|
||||
!isOnInstallPage();
|
||||
|
||||
if (blockedByMaintenance) {
|
||||
enableLoading();
|
||||
} else {
|
||||
disableLoading();
|
||||
|
||||
Reference in New Issue
Block a user