fix(registration): закрывать signup до готовности vars и сериализовать их patch
Добавляем расчётный флаг settings.is_registration_open в getSystemInfo и заглушку на странице signup, чтобы пайщик не попадал в ошибку генерации документов. Параллельные утверждения решений больше не затирают vars при read-modify-write. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,7 +39,10 @@ export class SettingsDTO implements SettingsDomainInterface {
|
||||
@Field(() => Date, { description: 'Дата последнего обновления' })
|
||||
updated_at!: Date;
|
||||
|
||||
constructor(data: SettingsDomainInterface) {
|
||||
@Field(() => Boolean, { description: 'Открыта ли регистрация новых пайщиков' })
|
||||
is_registration_open!: boolean;
|
||||
|
||||
constructor(data: SettingsDomainInterface, isRegistrationOpen = false) {
|
||||
this.coopname = data.coopname;
|
||||
this.authorized_default_workspace = data.authorized_default_workspace;
|
||||
this.authorized_default_route = data.authorized_default_route;
|
||||
@@ -48,6 +51,7 @@ export class SettingsDTO implements SettingsDomainInterface {
|
||||
this.provider_name = data.provider_name;
|
||||
this.created_at = data.created_at;
|
||||
this.updated_at = data.updated_at;
|
||||
this.is_registration_open = isRegistrationOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SymbolsDTO } from './symbols.dto';
|
||||
import { SettingsDTO } from './settings.dto';
|
||||
import { BoardMemberDTO } from './board-member.dto';
|
||||
import { SystemFeaturesDTO } from './system-features.dto';
|
||||
import { isRegistrationOpen } from '~/domain/system/utils/is-registration-open.util';
|
||||
|
||||
@ObjectType('SystemInfo')
|
||||
export class SystemInfoDTO {
|
||||
@@ -79,7 +80,7 @@ export class SystemInfoDTO {
|
||||
this.blockchain_info = new BlockchainInfoDTO(entity.blockchain_info);
|
||||
this.system_status = entity.system_status || 'install';
|
||||
this.symbols = entity.symbols;
|
||||
this.settings = new SettingsDTO(entity.settings);
|
||||
this.settings = new SettingsDTO(entity.settings, isRegistrationOpen(entity.vars));
|
||||
this.blockchain_account = entity.blockchain_account;
|
||||
this.cooperator_account = new CooperativeOperatorAccountDTO({
|
||||
...entity.cooperator_account,
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
PaymentMethodDomainPort,
|
||||
} from '~/domain/payment-method/ports/payment-method-domain.port';
|
||||
import { LoadContactsInteractor } from './load-contacts.interactor';
|
||||
import { isRegistrationOpen } from '~/domain/system/utils/is-registration-open.util';
|
||||
|
||||
@Injectable()
|
||||
export class SystemInteractor {
|
||||
@@ -307,4 +308,9 @@ export class SystemInteractor {
|
||||
async updateSettings(updates: UpdateSettingsInputDomainInterface): Promise<SettingsDomainEntity> {
|
||||
return this.settingsDomainPort.updateSettings(updates);
|
||||
}
|
||||
|
||||
async getRegistrationOpenStatus(): Promise<boolean> {
|
||||
const vars = await this.varsRepository.get();
|
||||
return isRegistrationOpen(vars);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,11 @@ export class SystemService {
|
||||
* Обновляет настройки системы
|
||||
*/
|
||||
public async updateSettings(data: UpdateSettingsInputDTO): Promise<SettingsDTO> {
|
||||
const settings = await this.systemInteractor.updateSettings(data);
|
||||
return new SettingsDTO(settings);
|
||||
const [settings, isRegistrationOpenStatus] = await Promise.all([
|
||||
this.systemInteractor.updateSettings(data),
|
||||
this.systemInteractor.getRegistrationOpenStatus(),
|
||||
]);
|
||||
return new SettingsDTO(settings, isRegistrationOpenStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { AgreementNumberDomainInterface } from '~/domain/agreement/interfaces/agreement-number.interface';
|
||||
import type { VarsDomainInterface } from '~/domain/system/interfaces/vars-domain.interface';
|
||||
|
||||
const REGISTRATION_REQUIRED_AGREEMENT_VARS = [
|
||||
'wallet_agreement',
|
||||
'signature_agreement',
|
||||
'privacy_agreement',
|
||||
'user_agreement',
|
||||
'participant_application',
|
||||
] as const satisfies ReadonlyArray<keyof VarsDomainInterface>;
|
||||
|
||||
function isAgreementVarFilled(agreement?: AgreementNumberDomainInterface | null): boolean {
|
||||
if (!agreement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const protocolNumber = agreement.protocol_number?.trim();
|
||||
const protocolDate = agreement.protocol_day_month_year?.trim();
|
||||
|
||||
return Boolean(protocolNumber) && Boolean(protocolDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация пайщиков доступна, когда заполнены реквизиты протоколов
|
||||
* базовых соглашений кооператива — без них factory не сгенерирует пакет документов.
|
||||
*/
|
||||
export function isRegistrationOpen(vars: VarsDomainInterface | null | undefined): boolean {
|
||||
if (!vars) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return REGISTRATION_REQUIRED_AGREEMENT_VARS.every((field) =>
|
||||
isAgreementVarFilled(vars[field] as AgreementNumberDomainInterface | null | undefined)
|
||||
);
|
||||
}
|
||||
+12
-2
@@ -23,6 +23,9 @@ import { TrackingRuleRepository } from '../repositories/tracking-rule.repository
|
||||
*/
|
||||
@Injectable()
|
||||
export class DecisionTrackingAdapter implements DecisionTrackingPort, OnModuleInit {
|
||||
/** Сериализует patch vars — иначе параллельные newdecision перезаписывают друг друга. */
|
||||
private varsUpdateChain: Promise<void> = Promise.resolve();
|
||||
|
||||
constructor(
|
||||
private readonly repository: TrackingRuleRepository,
|
||||
@Inject(VARS_DATA_PORT) private readonly varsPort: VarsDataPort,
|
||||
@@ -239,10 +242,11 @@ export class DecisionTrackingAdapter implements DecisionTrackingPort, OnModuleIn
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет поле в vars
|
||||
* Обновляет одно поле vars. Запросы идут в очереди: при быстром утверждении
|
||||
* нескольких решений подряд каждый patch читает актуальный snapshot.
|
||||
*/
|
||||
private async updateVars(varsField: string, decisionId: string, decisionDate: string): Promise<void> {
|
||||
try {
|
||||
const applyPatch = async (): Promise<void> => {
|
||||
const currentVars = await this.varsPort.get();
|
||||
if (!currentVars) {
|
||||
this.logger.warn('Vars не найдены, пропускаем обновление');
|
||||
@@ -261,6 +265,12 @@ export class DecisionTrackingAdapter implements DecisionTrackingPort, OnModuleIn
|
||||
|
||||
await this.varsPort.create(updatedVars);
|
||||
this.logger.info(`Обновлено поле vars: ${varsField} = ${decisionId} от ${decisionDate}`);
|
||||
};
|
||||
|
||||
this.varsUpdateChain = this.varsUpdateChain.then(applyPatch, applyPatch);
|
||||
|
||||
try {
|
||||
await this.varsUpdateChain;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка при обновлении vars: ${error.message}`, error.stack);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { isRegistrationOpen } from '~/domain/system/utils/is-registration-open.util';
|
||||
import type { VarsDomainInterface } from '~/domain/system/interfaces/vars-domain.interface';
|
||||
|
||||
describe('isRegistrationOpen', () => {
|
||||
const filledAgreement = {
|
||||
protocol_number: '1',
|
||||
protocol_day_month_year: '01.01.2026',
|
||||
};
|
||||
|
||||
const baseVars = {
|
||||
coopname: 'testcoop',
|
||||
wallet_agreement: filledAgreement,
|
||||
signature_agreement: filledAgreement,
|
||||
privacy_agreement: filledAgreement,
|
||||
user_agreement: filledAgreement,
|
||||
participant_application: filledAgreement,
|
||||
} as VarsDomainInterface;
|
||||
|
||||
it('returns false when vars are missing', () => {
|
||||
expect(isRegistrationOpen(null)).toBe(false);
|
||||
expect(isRegistrationOpen(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when any required agreement var is incomplete', () => {
|
||||
expect(
|
||||
isRegistrationOpen({
|
||||
...baseVars,
|
||||
wallet_agreement: { protocol_number: '', protocol_day_month_year: '01.01.2026' },
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when all required agreement vars are filled', () => {
|
||||
expect(isRegistrationOpen(baseVars)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -5232,6 +5232,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
authorized_default_workspace:"String",
|
||||
coopname:"String",
|
||||
created_at:"DateTime",
|
||||
is_registration_open:"Boolean",
|
||||
non_authorized_default_route:"String",
|
||||
non_authorized_default_workspace:"String",
|
||||
provider_name:"String",
|
||||
|
||||
@@ -10754,6 +10754,8 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
|
||||
coopname?:boolean | `@${string}`,
|
||||
/** Дата создания */
|
||||
created_at?:boolean | `@${string}`,
|
||||
/** Открыта ли регистрация новых пайщиков */
|
||||
is_registration_open?:boolean | `@${string}`,
|
||||
/** Маршрут по умолчанию для неавторизованных пользователей */
|
||||
non_authorized_default_route?:boolean | `@${string}`,
|
||||
/** Рабочий стол по умолчанию для неавторизованных пользователей */
|
||||
@@ -21009,6 +21011,8 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
|
||||
coopname?:boolean | `@${string}`,
|
||||
/** Дата создания */
|
||||
created_at?:boolean | `@${string}`,
|
||||
/** Открыта ли регистрация новых пайщиков */
|
||||
is_registration_open?:boolean | `@${string}`,
|
||||
/** Маршрут по умолчанию для неавторизованных пользователей */
|
||||
non_authorized_default_route?:boolean | `@${string}`,
|
||||
/** Рабочий стол по умолчанию для неавторизованных пользователей */
|
||||
@@ -31810,6 +31814,8 @@ export type ModelTypes = {
|
||||
coopname: string,
|
||||
/** Дата создания */
|
||||
created_at: ModelTypes["DateTime"],
|
||||
/** Открыта ли регистрация новых пайщиков */
|
||||
is_registration_open: boolean,
|
||||
/** Маршрут по умолчанию для неавторизованных пользователей */
|
||||
non_authorized_default_route: string,
|
||||
/** Рабочий стол по умолчанию для неавторизованных пользователей */
|
||||
@@ -43122,6 +43128,8 @@ export type GraphQLTypes = {
|
||||
coopname: string,
|
||||
/** Дата создания */
|
||||
created_at: GraphQLTypes["DateTime"],
|
||||
/** Открыта ли регистрация новых пайщиков */
|
||||
is_registration_open: boolean,
|
||||
/** Маршрут по умолчанию для неавторизованных пользователей */
|
||||
non_authorized_default_route: string,
|
||||
/** Рабочий стол по умолчанию для неавторизованных пользователей */
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<template lang="pug">
|
||||
.signup-page
|
||||
AuthCard.signup-page__card(:max-width='720', title='Вступить в пайщики')
|
||||
template(v-if='isRegistrationClosed')
|
||||
EmptyState.signup-page__closed(
|
||||
title='Регистрация временно недоступна',
|
||||
body='Кооператив завершает подготовку к приёму новых пайщиков. Пожалуйста, зайдите позже.'
|
||||
)
|
||||
|
||||
q-stepper.signup-page__stepper(
|
||||
v-else,
|
||||
v-model='store.step',
|
||||
vertical,
|
||||
animated,
|
||||
@@ -26,7 +33,7 @@
|
||||
|
||||
WaitingRegistration
|
||||
|
||||
.signup-page__restart
|
||||
.signup-page__restart(v-if='!isRegistrationClosed')
|
||||
q-btn(@click='out', dense, size='sm', flat color='grey') начать с начала
|
||||
</template>
|
||||
|
||||
@@ -42,6 +49,7 @@ import PayInitial from './PayInitial.vue';
|
||||
import WaitingRegistration from './WaitingRegistration.vue';
|
||||
import SelectBranch from './SelectBranch.vue';
|
||||
import { AuthCard } from 'src/shared/ui/domain/AuthCard';
|
||||
import { EmptyState } from 'src/shared/ui/base/EmptyState';
|
||||
|
||||
import { useRegistratorStore } from 'src/entities/Registrator';
|
||||
import { useLogoutUser } from 'src/features/User/Logout';
|
||||
@@ -65,6 +73,8 @@ const desktops = useDesktopStore();
|
||||
const system = useSystemStore();
|
||||
const { info } = system;
|
||||
|
||||
const isRegistrationClosed = computed(() => info.settings?.is_registration_open === false);
|
||||
|
||||
// Диалог разрешения уведомлений
|
||||
const { showDialog } = useNotificationPermissionDialog();
|
||||
|
||||
@@ -306,4 +316,7 @@ const isBranched = computed(() => info.cooperator_account.is_branched);
|
||||
margin-top: var(--p-4, 16px);
|
||||
text-align: center;
|
||||
}
|
||||
.signup-page__closed {
|
||||
padding: var(--p-4, 16px) 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,6 +65,7 @@ export const rawSettingsSelector = {
|
||||
authorized_default_route: true,
|
||||
non_authorized_default_workspace: true,
|
||||
non_authorized_default_route: true,
|
||||
is_registration_open: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
}
|
||||
|
||||
@@ -5232,6 +5232,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
authorized_default_workspace:"String",
|
||||
coopname:"String",
|
||||
created_at:"DateTime",
|
||||
is_registration_open:"Boolean",
|
||||
non_authorized_default_route:"String",
|
||||
non_authorized_default_workspace:"String",
|
||||
provider_name:"String",
|
||||
|
||||
@@ -10754,6 +10754,8 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
|
||||
coopname?:boolean | `@${string}`,
|
||||
/** Дата создания */
|
||||
created_at?:boolean | `@${string}`,
|
||||
/** Открыта ли регистрация новых пайщиков */
|
||||
is_registration_open?:boolean | `@${string}`,
|
||||
/** Маршрут по умолчанию для неавторизованных пользователей */
|
||||
non_authorized_default_route?:boolean | `@${string}`,
|
||||
/** Рабочий стол по умолчанию для неавторизованных пользователей */
|
||||
@@ -21009,6 +21011,8 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
|
||||
coopname?:boolean | `@${string}`,
|
||||
/** Дата создания */
|
||||
created_at?:boolean | `@${string}`,
|
||||
/** Открыта ли регистрация новых пайщиков */
|
||||
is_registration_open?:boolean | `@${string}`,
|
||||
/** Маршрут по умолчанию для неавторизованных пользователей */
|
||||
non_authorized_default_route?:boolean | `@${string}`,
|
||||
/** Рабочий стол по умолчанию для неавторизованных пользователей */
|
||||
@@ -31810,6 +31814,8 @@ export type ModelTypes = {
|
||||
coopname: string,
|
||||
/** Дата создания */
|
||||
created_at: ModelTypes["DateTime"],
|
||||
/** Открыта ли регистрация новых пайщиков */
|
||||
is_registration_open: boolean,
|
||||
/** Маршрут по умолчанию для неавторизованных пользователей */
|
||||
non_authorized_default_route: string,
|
||||
/** Рабочий стол по умолчанию для неавторизованных пользователей */
|
||||
@@ -43122,6 +43128,8 @@ export type GraphQLTypes = {
|
||||
coopname: string,
|
||||
/** Дата создания */
|
||||
created_at: GraphQLTypes["DateTime"],
|
||||
/** Открыта ли регистрация новых пайщиков */
|
||||
is_registration_open: boolean,
|
||||
/** Маршрут по умолчанию для неавторизованных пользователей */
|
||||
non_authorized_default_route: string,
|
||||
/** Рабочий стол по умолчанию для неавторизованных пользователей */
|
||||
|
||||
Reference in New Issue
Block a user