fix(marketplace): гейт стола заказчика для председателя + подпись оферты долетает
Два бага L3-онбординга заказчика: 1) Председатель (и совет) видели рабочие страницы стола заказчика (Каталог/Мои заказы/Сводный) до подписи оферты: страницы требовали Offer:read / Order:read:own, а admin/board дают голый Offer:read и Order:read:all, который разворачивается в :own — гранты «протекали» на стол заказчика. Фикс: рабочие страницы требуют orderer-эксклюзивный Order:create (нет у admin/board, не покрывается :all-разворотом) — стол заказчика гейтится персональной подписью для любой роли. 2) Подпись оферты не долетала: getOnboardingState читал AgreementRepository (soviet::agreements3), а ЦПП «Стол заказов» — ПРОГРАММА (program_id=2), и wallet::signagree пишет подпись в wallet::users.programs[]. Источник истины — UserAgreementRepository.findProgram(program_id) (канон AgreementService.fetchProgrammatic). Из-за чтения не той таблицы requires_gate оставался true навсегда. Фикс: читаем программную подпись. 3) Авто-переключение после подписи: фронт коротко поллит getOnboardingState пока подпись синхронизируется в PG (wallet::signagree уже подтверждён цепочкой), затем loadDesktop+навигация — без ручного refresh. Тесты: onboarding-service переписан под программный источник (6), grants (8) — 14/14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+40
-23
@@ -1,11 +1,15 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import type { TransactResult } from '@wharfkit/session';
|
||||
|
||||
import { AGREEMENT_REPOSITORY, AgreementRepository } from '~/domain/agreement/repositories/agreement.repository';
|
||||
import {
|
||||
USER_AGREEMENT_REPOSITORY,
|
||||
UserAgreementRepository,
|
||||
} from '~/domain/wallet/repositories/user-agreement.repository';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { SOVIET_BLOCKCHAIN_PORT, SovietBlockchainPort } from '~/domain/common/ports/soviet-blockchain.port';
|
||||
import { WALLET_BLOCKCHAIN_PORT, WalletBlockchainPort } from '~/domain/wallet/ports/wallet-blockchain.port';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
import { config } from '~/config';
|
||||
|
||||
import {
|
||||
MARKETPLACE_AGREEMENT_TYPE,
|
||||
@@ -18,24 +22,26 @@ import { MarketplaceOnboardingStateDTO } from '../dto/marketplace-onboarding-sta
|
||||
* Story 1.4: L3 fallback gate marketplace.
|
||||
*
|
||||
* Контракт: расширение само не хранит «подписал/не подписал». Источник правды —
|
||||
* `soviet::agreements3`, синхронизированная в `AgreementRepository` через
|
||||
* `AgreementSyncService` (см. CLAUDE.md read-path: только PG-repository).
|
||||
* блокчейн, синхронизированный в PG (read-path: только PG-repository).
|
||||
*
|
||||
* ВАЖНО — правильный источник: ЦПП «Стол заказов» это ПРОГРАММА (program_id=2),
|
||||
* и подпись пайщика делается через `wallet::signagree` в `wallet::users.programs[]`,
|
||||
* а НЕ в `soviet::agreements3`. Поэтому состояние читаем из
|
||||
* `UserAgreementRepository` (синк `user-agreement-sync.service` из `wallet::users`),
|
||||
* а не из `AgreementRepository` (синк `agreement-sync.service` из `agreements3`).
|
||||
* Это тот же канон, что в `AgreementService.fetchProgrammatic`. Раньше здесь
|
||||
* читался `AgreementRepository`, куда программная подпись НИКОГДА не попадает —
|
||||
* из-за чего `requires_gate` оставался `true` навсегда после подписи.
|
||||
*
|
||||
* Локальная таблица `marketplace_onboarding_state` из PRD не заводится:
|
||||
* - `AgreementRepository.findByUsername` уже даёт быстрый доступ к подписям
|
||||
* (PG индекс по username);
|
||||
* - различение source ('registration_flow' vs 'extension_gate') в текущей
|
||||
* `agreements3` отсутствует — оба пути пишут одинаковую запись с
|
||||
* `agreement_type = 'marketplace'`. Story 1.11 при добавлении L2 при
|
||||
* необходимости заведёт локальный source-маркер.
|
||||
*
|
||||
* TTL 60s из PRD AC тоже опускаем — `AgreementRepository` сам синхронизирован
|
||||
* с blockchain, локальный staleness ничем не отличается от core data.
|
||||
* быстрый доступ уже даёт PG-кеш `wallet::users`. TTL 60s из PRD AC опускаем —
|
||||
* репозиторий синхронизирован с blockchain, локальный staleness не отличается
|
||||
* от core data.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MarketplaceOnboardingService {
|
||||
constructor(
|
||||
@Inject(AGREEMENT_REPOSITORY) private readonly agreementRepository: AgreementRepository,
|
||||
@Inject(USER_AGREEMENT_REPOSITORY) private readonly userAgreementRepository: UserAgreementRepository,
|
||||
@Inject(SOVIET_BLOCKCHAIN_PORT) private readonly sovietBlockchainPort: SovietBlockchainPort,
|
||||
@Inject(WALLET_BLOCKCHAIN_PORT) private readonly walletBlockchainPort: WalletBlockchainPort,
|
||||
private readonly logger: WinstonLoggerService
|
||||
@@ -55,23 +61,34 @@ export class MarketplaceOnboardingService {
|
||||
});
|
||||
}
|
||||
|
||||
const agreements = await this.agreementRepository.findByUsername(username);
|
||||
const signed = agreements.find(
|
||||
(a) =>
|
||||
a.type === MARKETPLACE_AGREEMENT_TYPE &&
|
||||
(a.draft_id === undefined ||
|
||||
a.draft_id === null ||
|
||||
Number(a.draft_id) === templateRegistryId)
|
||||
// program_id ЦПП «Стол заказов» из `soviet::coagreements` (тот же лукап, что
|
||||
// и при подписи). Без настроенной как программа ЦПП гейтить нечем — пропускаем.
|
||||
const coopname = config.coopname;
|
||||
const coagreement = await this.sovietBlockchainPort.getCoagreement(
|
||||
coopname,
|
||||
MARKETPLACE_AGREEMENT_TYPE
|
||||
);
|
||||
const programId = coagreement ? Number(coagreement.program_id) : 0;
|
||||
if (!coagreement || programId <= 0) {
|
||||
return new MarketplaceOnboardingStateDTO({
|
||||
requires_gate: false,
|
||||
source: 'not_configured',
|
||||
template_registry_id: templateRegistryId,
|
||||
});
|
||||
}
|
||||
|
||||
if (signed) {
|
||||
// Подпись программной оферты живёт в `wallet::users.programs[]`.
|
||||
const owner = await this.userAgreementRepository.findByUsername(coopname, username);
|
||||
const program =
|
||||
owner && owner.present !== false ? owner.findProgram(programId) : undefined;
|
||||
|
||||
if (program) {
|
||||
const source: MarketplaceOnboardingSource = 'agreement_signed';
|
||||
return new MarketplaceOnboardingStateDTO({
|
||||
requires_gate: false,
|
||||
source,
|
||||
template_registry_id: templateRegistryId,
|
||||
completed_at: signed.updated_at ? String(signed.updated_at) : undefined,
|
||||
agreement_id: signed.id ?? undefined,
|
||||
completed_at: program.signed_at ? String(program.signed_at) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+80
-81
@@ -1,31 +1,53 @@
|
||||
/**
|
||||
* Unit-тесты MarketplaceOnboardingService (Story 1.4).
|
||||
* Unit-тесты MarketplaceOnboardingService.getOnboardingState (Story 1.4).
|
||||
*
|
||||
* Покрывают AC:
|
||||
* (a) MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID = 0 (Story 1.7 не выполнена)
|
||||
* → requires_gate=false, source='not_configured';
|
||||
* (b) есть подписанная оферта marketplace в core `soviet::agreements3`
|
||||
* → requires_gate=false, source='agreement_signed', completed_at/agreement_id
|
||||
* заполнены;
|
||||
* (c) подписи нет → requires_gate=true, source='gate_required'.
|
||||
* ЦПП «Стол заказов» — ПРОГРАММА (program_id=2): подпись пайщика делается через
|
||||
* `wallet::signagree` в `wallet::users.programs[]`, поэтому состояние читается
|
||||
* из `UserAgreementRepository` (НЕ из `agreements3`/`AgreementRepository`).
|
||||
*
|
||||
* Для случая (b/c) подменяем константу через jest.doMock — она captured
|
||||
* сервисом в момент вызова, не на import-time.
|
||||
* Покрывают:
|
||||
* (a) MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID = 0 → not_configured;
|
||||
* (b) ЦПП не настроена как программа (нет коагримента / program_id<=0)
|
||||
* → not_configured (гейтить нечем);
|
||||
* (c) пайщик подписал программу → requires_gate=false, source=agreement_signed,
|
||||
* completed_at из signed_at;
|
||||
* (d) подписи нет (owner=null или нет нужной program_id) → requires_gate=true;
|
||||
* (e) owner.present=false (откат форком) → подпись не засчитывается.
|
||||
*/
|
||||
|
||||
const makeAgreement = (overrides: any = {}) => ({
|
||||
id: 42,
|
||||
type: 'marketplace',
|
||||
draft_id: 7,
|
||||
username: 'alice',
|
||||
coopname: 'voskhod',
|
||||
updated_at: '2026-05-14T12:00:00Z',
|
||||
const PROGRAM_ID = 2;
|
||||
|
||||
const makeProgram = (overrides: any = {}) => ({
|
||||
program_id: PROGRAM_ID,
|
||||
doc_hash: 'abc',
|
||||
version: 1,
|
||||
draft_id: 1100,
|
||||
signed_at: '2026-05-14T12:00:00Z',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeRepo = (agreements: any[]) =>
|
||||
const makeOwner = (programs: any[], present = true) =>
|
||||
({
|
||||
findByUsername: jest.fn().mockResolvedValue(agreements),
|
||||
present,
|
||||
programs,
|
||||
findProgram(pid: number | string) {
|
||||
return programs.find((p) => Number(p.program_id) === Number(pid));
|
||||
},
|
||||
} as any);
|
||||
|
||||
const makeUserAgreementRepo = (owner: any) =>
|
||||
({
|
||||
findByUsername: jest.fn().mockResolvedValue(owner),
|
||||
} as any);
|
||||
|
||||
const makeSovietPort = (coagreement: any) =>
|
||||
({
|
||||
getCoagreement: jest.fn().mockResolvedValue(coagreement),
|
||||
} as any);
|
||||
|
||||
const makeWalletPort = () =>
|
||||
({
|
||||
signProgramAgreement: jest.fn(),
|
||||
} as any);
|
||||
|
||||
const makeLogger = () =>
|
||||
@@ -37,17 +59,7 @@ const makeLogger = () =>
|
||||
debug: jest.fn(),
|
||||
} as any);
|
||||
|
||||
// getOnboardingState не обращается к blockchain-портам (только agreementRepository),
|
||||
// поэтому для этих кейсов достаточно заглушек методов, используемых в других путях.
|
||||
const makeSovietPort = () =>
|
||||
({
|
||||
getCoagreement: jest.fn(),
|
||||
} as any);
|
||||
|
||||
const makeWalletPort = () =>
|
||||
({
|
||||
signProgramAgreement: jest.fn(),
|
||||
} as any);
|
||||
const COAGREEMENT = { program_id: PROGRAM_ID, draft_id: 1100, type: 'marketplace' };
|
||||
|
||||
describe('MarketplaceOnboardingService.getOnboardingState', () => {
|
||||
afterEach(() => {
|
||||
@@ -65,96 +77,83 @@ describe('MarketplaceOnboardingService.getOnboardingState', () => {
|
||||
const { MarketplaceOnboardingService } = await import(
|
||||
'~/extensions/marketplace/application/onboarding/marketplace-onboarding.service'
|
||||
);
|
||||
const repo = makeRepo([]);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(), makeWalletPort(), makeLogger());
|
||||
const repo = makeUserAgreementRepo(null);
|
||||
const soviet = makeSovietPort(COAGREEMENT);
|
||||
const service = new MarketplaceOnboardingService(repo, soviet, makeWalletPort(), makeLogger());
|
||||
|
||||
const state = await service.getOnboardingState('alice');
|
||||
expect(state.requires_gate).toBe(false);
|
||||
expect(state.source).toBe('not_configured');
|
||||
expect(state.template_registry_id).toBe(0);
|
||||
expect(soviet.getCoagreement).not.toHaveBeenCalled();
|
||||
expect(repo.findByUsername).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Story 1.7 размещена (template_registry_id=1100 из cooptypes), подписи нет → requires_gate=true', async () => {
|
||||
it('ЦПП не настроена как программа (нет коагримента) → not_configured', async () => {
|
||||
jest.dontMock('~/extensions/marketplace/constants/marketplace-agreement-ids');
|
||||
const { MarketplaceOnboardingService } = await import(
|
||||
'~/extensions/marketplace/application/onboarding/marketplace-onboarding.service'
|
||||
);
|
||||
const repo = makeRepo([]);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(), makeWalletPort(), makeLogger());
|
||||
const repo = makeUserAgreementRepo(null);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(null), makeWalletPort(), makeLogger());
|
||||
|
||||
const state = await service.getOnboardingState('alice');
|
||||
expect(state.template_registry_id).toBe(1100);
|
||||
expect(state.requires_gate).toBe(true);
|
||||
expect(state.source).toBe('gate_required');
|
||||
expect(state.requires_gate).toBe(false);
|
||||
expect(state.source).toBe('not_configured');
|
||||
expect(repo.findByUsername).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('подпись marketplace есть → requires_gate=false, source=agreement_signed, completed_at/agreement_id заполнены', async () => {
|
||||
jest.doMock('~/extensions/marketplace/constants/marketplace-agreement-ids', () => ({
|
||||
__esModule: true,
|
||||
MARKETPLACE_EXTENSION_NAME: 'market',
|
||||
MARKETPLACE_OFFER_AGREEMENT_ID: 'marketplace_offer',
|
||||
MARKETPLACE_AGREEMENT_TYPE: 'marketplace',
|
||||
MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID: 7,
|
||||
}));
|
||||
|
||||
it('подпись программы есть → requires_gate=false, source=agreement_signed, completed_at из signed_at', async () => {
|
||||
jest.dontMock('~/extensions/marketplace/constants/marketplace-agreement-ids');
|
||||
const { MarketplaceOnboardingService } = await import(
|
||||
'~/extensions/marketplace/application/onboarding/marketplace-onboarding.service'
|
||||
);
|
||||
const repo = makeRepo([makeAgreement({ draft_id: 7 })]);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(), makeWalletPort(), makeLogger());
|
||||
const owner = makeOwner([makeProgram()]);
|
||||
const repo = makeUserAgreementRepo(owner);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(COAGREEMENT), makeWalletPort(), makeLogger());
|
||||
|
||||
const state = await service.getOnboardingState('alice');
|
||||
expect(state.requires_gate).toBe(false);
|
||||
expect(state.source).toBe('agreement_signed');
|
||||
expect(state.template_registry_id).toBe(7);
|
||||
expect(state.agreement_id).toBe(42);
|
||||
expect(state.completed_at).toBe('2026-05-14T12:00:00Z');
|
||||
expect(repo.findByUsername).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
|
||||
it('подписи нет → requires_gate=true, source=gate_required', async () => {
|
||||
jest.doMock('~/extensions/marketplace/constants/marketplace-agreement-ids', () => ({
|
||||
__esModule: true,
|
||||
MARKETPLACE_EXTENSION_NAME: 'market',
|
||||
MARKETPLACE_OFFER_AGREEMENT_ID: 'marketplace_offer',
|
||||
MARKETPLACE_AGREEMENT_TYPE: 'marketplace',
|
||||
MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID: 7,
|
||||
}));
|
||||
|
||||
it('подписи нет (owner=null) → requires_gate=true, source=gate_required', async () => {
|
||||
jest.dontMock('~/extensions/marketplace/constants/marketplace-agreement-ids');
|
||||
const { MarketplaceOnboardingService } = await import(
|
||||
'~/extensions/marketplace/application/onboarding/marketplace-onboarding.service'
|
||||
);
|
||||
const repo = makeRepo([
|
||||
makeAgreement({ type: 'capital', draft_id: 999 }), // не marketplace — игнор
|
||||
makeAgreement({ type: 'marketplace', draft_id: 8 }), // другой шаблон — игнор
|
||||
]);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(), makeWalletPort(), makeLogger());
|
||||
const repo = makeUserAgreementRepo(null);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(COAGREEMENT), makeWalletPort(), makeLogger());
|
||||
|
||||
const state = await service.getOnboardingState('alice');
|
||||
expect(state.requires_gate).toBe(true);
|
||||
expect(state.source).toBe('gate_required');
|
||||
expect(state.template_registry_id).toBe(7);
|
||||
expect(state.agreement_id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('запись marketplace без draft_id (если контракт не проставил) считается совпадающей по type', async () => {
|
||||
jest.doMock('~/extensions/marketplace/constants/marketplace-agreement-ids', () => ({
|
||||
__esModule: true,
|
||||
MARKETPLACE_EXTENSION_NAME: 'market',
|
||||
MARKETPLACE_OFFER_AGREEMENT_ID: 'marketplace_offer',
|
||||
MARKETPLACE_AGREEMENT_TYPE: 'marketplace',
|
||||
MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID: 7,
|
||||
}));
|
||||
|
||||
it('owner есть, но другой program_id → requires_gate=true', async () => {
|
||||
jest.dontMock('~/extensions/marketplace/constants/marketplace-agreement-ids');
|
||||
const { MarketplaceOnboardingService } = await import(
|
||||
'~/extensions/marketplace/application/onboarding/marketplace-onboarding.service'
|
||||
);
|
||||
const repo = makeRepo([makeAgreement({ type: 'marketplace', draft_id: undefined, id: 99 })]);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(), makeWalletPort(), makeLogger());
|
||||
const owner = makeOwner([makeProgram({ program_id: 1 })]); // capital, не marketplace
|
||||
const repo = makeUserAgreementRepo(owner);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(COAGREEMENT), makeWalletPort(), makeLogger());
|
||||
|
||||
const state = await service.getOnboardingState('alice');
|
||||
expect(state.requires_gate).toBe(false);
|
||||
expect(state.agreement_id).toBe(99);
|
||||
expect(state.requires_gate).toBe(true);
|
||||
});
|
||||
|
||||
it('owner.present=false (откат форком) → подпись не засчитывается, requires_gate=true', async () => {
|
||||
jest.dontMock('~/extensions/marketplace/constants/marketplace-agreement-ids');
|
||||
const { MarketplaceOnboardingService } = await import(
|
||||
'~/extensions/marketplace/application/onboarding/marketplace-onboarding.service'
|
||||
);
|
||||
const owner = makeOwner([makeProgram()], false);
|
||||
const repo = makeUserAgreementRepo(owner);
|
||||
const service = new MarketplaceOnboardingService(repo, makeSovietPort(COAGREEMENT), makeWalletPort(), makeLogger());
|
||||
|
||||
const state = await service.getOnboardingState('alice');
|
||||
expect(state.requires_gate).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,7 +115,15 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
meta: {
|
||||
title: 'Каталог',
|
||||
icon: 'fa-solid fa-store',
|
||||
requires: 'Offer:read',
|
||||
// Рабочие страницы стола заказчика гейтятся подпиской оферты
|
||||
// ЦПП: требуем orderer-эксклюзивный грант `Order:create`. Его
|
||||
// НЕ выдают роли admin/board, и `Order:read:all` в него НЕ
|
||||
// разворачивается (нет `:all`-формы) — поэтому admin-гранты
|
||||
// председателя (или совета) не «протекают» на стол заказчика
|
||||
// до его персональной подписи. Реальный enforcement резолверов
|
||||
// использует узкие права (`Offer:read`/`Order:read:own`) как
|
||||
// прежде; здесь — только видимость стола.
|
||||
requires: 'Order:create',
|
||||
requiresAuth: true,
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
@@ -128,7 +136,7 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
meta: {
|
||||
title: 'Мои заказы',
|
||||
icon: 'fa-solid fa-cart-shopping',
|
||||
requires: 'Order:read:own',
|
||||
requires: 'Order:create',
|
||||
requiresAuth: true,
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
@@ -145,7 +153,7 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
meta: {
|
||||
title: 'Сводный заказ',
|
||||
icon: 'fa-solid fa-layer-group',
|
||||
requires: 'Order:read:own',
|
||||
requires: 'Order:create',
|
||||
requiresAuth: true,
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
@@ -161,7 +169,7 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
meta: {
|
||||
title: 'Готово к получению',
|
||||
icon: 'fa-solid fa-box-open',
|
||||
requires: 'Issuance:read:own',
|
||||
requires: 'Order:create',
|
||||
requiresAuth: true,
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
@@ -177,7 +185,7 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
meta: {
|
||||
title: 'Гарантийные возвраты',
|
||||
icon: 'fa-solid fa-rotate-left',
|
||||
requires: 'ReturnClaim:read:own',
|
||||
requires: 'Order:create',
|
||||
requiresAuth: true,
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
|
||||
+32
-13
@@ -68,27 +68,46 @@ async function load(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* После подписи `wallet::signagree` уже подтверждён цепочкой (мутация
|
||||
* вернулась), но `requires_gate` и гранты завязаны на PG-кеш `wallet::users`,
|
||||
* который parser синхронизирует со следующего блока. Коротко поллим состояние
|
||||
* онбординга, пока подпись не отразится в PG, — тогда и `getDesktop` отдаст
|
||||
* полные orderer-права. Так переключение происходит само, без ручного refresh
|
||||
* (и без поллинга на каждом переходе — только здесь, разово после подписи).
|
||||
*/
|
||||
async function waitForSignatureSynced(): Promise<boolean> {
|
||||
const deadline = Date.now() + 15000;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
state.value = await fetchOnboardingState();
|
||||
if (state.value && !state.value.requires_gate) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function onAccept(_documentIds: string[]): Promise<void> {
|
||||
// L3 подпись прямо со стола: фоллоуап Эпика 1 (mutation
|
||||
// `marketplaceSignOnboardingOffer`). Frontend рендерит оферту 1101 через
|
||||
// documentFactory, подписывает локальным WIF, отправляет на backend, а тот
|
||||
// вызывает on-chain `wallet::signagree` от лица coopname.
|
||||
// После успешной подписи перезагружаем состояние онбординга — если
|
||||
// requires_gate=false, ведём пайщика на каталог.
|
||||
// L3 подпись прямо со стола: mutation `marketplaceSignOnboardingOffer`.
|
||||
// Frontend рендерит оферту 1101 через documentFactory, подписывает локальным
|
||||
// WIF, отправляет на backend, а тот вызывает on-chain `wallet::signagree` от
|
||||
// лица coopname (программная подпись в wallet::users.programs[], program_id=2).
|
||||
loading.value = true;
|
||||
try {
|
||||
state.value = await signOnboardingOffer();
|
||||
if (state.value && !state.value.requires_gate) {
|
||||
// requires_gate=false сразу — редкий случай (PG уже синхронен); иначе ждём
|
||||
// синк подписи в PG коротким поллингом.
|
||||
const confirmed =
|
||||
(!!state.value && !state.value.requires_gate) || (await waitForSignatureSynced());
|
||||
if (confirmed) {
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Оферта ЦПП «Стол заказов» подписана. Открываем стол заказчика…',
|
||||
timeout: 1500,
|
||||
});
|
||||
// Подпись сменила L3-гейт: backend теперь выдаёт полные orderer-права
|
||||
// Подпись синхронизирована: backend теперь выдаёт полные orderer-права
|
||||
// вместо маркера Onboarding:orderer. Перечитываем десктоп (гранты) и
|
||||
// переустанавливаем маршруты, затем ведём на первую доступную страницу
|
||||
// стола (Каталог) — тот же канон refresh, что у EnableButton, без
|
||||
// поллинга и без loadDesktop на каждом переходе.
|
||||
// стола (Каталог) — тот же канон refresh, что у EnableButton.
|
||||
await desktop.loadDesktop();
|
||||
await loadExtensionRoutes('market', router);
|
||||
const coopname = system.info?.coopname;
|
||||
@@ -101,11 +120,11 @@ async function onAccept(_documentIds: string[]): Promise<void> {
|
||||
: { name: 'marketplace-catalog' },
|
||||
);
|
||||
} else {
|
||||
// Sync ещё не подтянул запись из chain — даём UI шанс перезапросить
|
||||
// вручную через перезагрузку страницы.
|
||||
// Синк не успел за отведённое окно — крайне редко; даём пользователю
|
||||
// явный сигнал перезагрузить страницу.
|
||||
Notify.create({
|
||||
type: 'info',
|
||||
message: 'Подпись отправлена. Подтверждение из блокчейна ожидается — обновите страницу через несколько секунд.',
|
||||
message: 'Подпись принята блокчейном и синхронизируется. Обновите страницу через несколько секунд.',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user