[598-4][@ant] feat(marketplace): фоллоуап Эпика 1 — L3 sign-mutation marketplaceSignOnboardingOffer + factory 1100/1101
Закрывает блокер magistral II документации (см. order-create.md / PLAN.md §9.13): on-chain marketplace::createorder валил assertion «у пайщика не подписано соглашение program_id=2 для w.mkt.member», потому что в фабрике документов отсутствовали adapter'ы для шаблона/инстанса оферты ЦПП «Стол заказов», а mutation подписи прямо со стола заказов была пропущена в Эпике 1 (помечена фоллоуапом в onboarding/README.md и в blago issue 598-4). Что сделано: cooptypes: - IVars.marketplace_program / marketplace_offer_template (optional). Юр.текст оферты остаётся «рыбой» — финальную редакцию готовит юр.отдел. - UdataKey.MARKETPLACE_AGREEMENT_NUMBER / MARKETPLACE_AGREEMENT_CREATED_AT. - 1101.MarketplaceOffer.Action расширён двумя обязательными полями: номер соглашения и дата (передаются явно из L3 service, без Udata-таблицы). factory (Эпик 1 / Story 1.7 финализация): - Actions/1100.MarketplaceOfferTemplate.ts + Templates/1100. MarketplaceOfferTemplate.ts (шаблон — только coop+vars, аналог 998.BlagorostProgramTemplate). - Actions/1101.MarketplaceOffer.ts + Templates/1101.MarketplaceOffer.ts (инстанс на конкретного пайщика — с common_user и номером соглашения). - VarsSchema получил marketplace_program и marketplace_offer_template. - index.ts и registry.ts factory регистрируют новые registry_id. controller (Эпик 1 / Story 1.4 фоллоуап): - DTO MarketplaceSignOnboardingOfferInputDTO — только signed document; coopname/username берутся из JWT context'а guard'а (защита от подмены). - MarketplaceOnboardingService.signOnboardingOffer — лукап soviet::coagreements(coopname, type='marketplace'), вызов walletBlockchainPort.signProgramAgreement (program_id из coagreement, draft_id оттуда же, document — подписан фронтом). - Mutation marketplaceSignOnboardingOffer в marketplace-onboarding.resolver. ts с возвратом свежего MarketplaceOnboardingState (UI не нужен второй запрос). SDK: - Mutations.Marketplace.MarketplaceSignOnboardingOffer wrapper (graphql-zeus regen для нового input MarketplaceSignOnboardingOfferInput). desktop: - OnboardingMemberPickCpp api: signOnboardingOffer() — генерация документа через DigitalDocument(registry_id=1101), подпись локальным WIF, вызов новой mutation. - OnboardingMemberPickCppPage.vue: onAccept теперь подписывает прямо со стола (раньше редиректил в Registrator). По успеху — Notify «Оферта подписана» + redirect /market/catalog. - OnboardingCPPGate widget: новый пропс busy для loading-state кнопки «Подписать оферту». PLAN.md §9.14 — закрывающая запись по выбранному варианту A. NB: schema.gql и SDK zeus/index.ts регенерированы автоматически — попадают в коммит как часть SDK сборки. Текст оферты в cooptypes 1100/1101 остаётся «рыбой» до утверждения юр.отделом; registry_id и подписи менять не потребуется. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed-digital-document-input.dto';
|
||||
|
||||
/**
|
||||
* Эпик 1 / Story 1.4 (фоллоуап) — input mutation `marketplaceSignOnboardingOffer`.
|
||||
*
|
||||
* Контекст:
|
||||
* - `coopname` и `username` НЕ принимаются с фронта — берутся из
|
||||
* `MarketplaceMembershipGuard` (JWT-пайщик), чтобы исключить подделку
|
||||
* подписи за другого пайщика.
|
||||
* - `document` — уже подписанный фронтом инстанс оферты ЦПП «Стол заказов»
|
||||
* (`registry_id=1101`). Backend проверяет подпись через on-chain
|
||||
* `verify_document_or_fail` внутри `wallet::signagree`.
|
||||
*/
|
||||
@InputType('MarketplaceSignOnboardingOfferInput')
|
||||
export class MarketplaceSignOnboardingOfferInputDTO {
|
||||
@Field(() => SignedDigitalDocumentInputDTO, {
|
||||
description: 'Подписанный пайщиком инстанс оферты ЦПП «Стол заказов» (registry_id=1101)',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => SignedDigitalDocumentInputDTO)
|
||||
document!: SignedDigitalDocumentInputDTO;
|
||||
}
|
||||
+69
-1
@@ -1,7 +1,11 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import type { TransactResult } from '@wharfkit/session';
|
||||
|
||||
import { AGREEMENT_REPOSITORY, AgreementRepository } from '~/domain/agreement/repositories/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 {
|
||||
MARKETPLACE_AGREEMENT_TYPE,
|
||||
@@ -32,6 +36,8 @@ import { MarketplaceOnboardingStateDTO } from '../dto/marketplace-onboarding-sta
|
||||
export class MarketplaceOnboardingService {
|
||||
constructor(
|
||||
@Inject(AGREEMENT_REPOSITORY) private readonly agreementRepository: AgreementRepository,
|
||||
@Inject(SOVIET_BLOCKCHAIN_PORT) private readonly sovietBlockchainPort: SovietBlockchainPort,
|
||||
@Inject(WALLET_BLOCKCHAIN_PORT) private readonly walletBlockchainPort: WalletBlockchainPort,
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
this.logger.setContext(MarketplaceOnboardingService.name);
|
||||
@@ -75,6 +81,68 @@ export class MarketplaceOnboardingService {
|
||||
template_registry_id: templateRegistryId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Эпик 1 фоллоуап: L3 sign-mutation.
|
||||
*
|
||||
* Pole-position: пайщик зарегистрировался **до** активации marketplace
|
||||
* (Story 1.7/1.9 ещё не прошли) или не выбрал ЦПП «Стол заказов» на
|
||||
* core registration-flow (L2). Сейчас при первом входе на стол получает
|
||||
* gate (Story 1.4). Этот метод позволяет ему подписать оферту прямо со
|
||||
* стола, без повторной регистрации.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Лукап `soviet::coagreements(coopname, type='marketplace')` →
|
||||
* program_id (должен быть 2) + draft_id (должен быть 1100).
|
||||
* 2. `walletBlockchainPort.signProgramAgreement` → on-chain
|
||||
* `wallet::signagree` пишет запись в `wallet::users.programs[]` с
|
||||
* program_id=2. После next-block `AgreementSyncService` подтягивает
|
||||
* запись в PG-кеш `AgreementRepository`, и следующий
|
||||
* `getOnboardingState` вернёт `requires_gate=false,
|
||||
* source='agreement_signed'`.
|
||||
*
|
||||
* Auth: вызывающий = пайщик (JWT в `MarketplaceMembershipGuard`).
|
||||
* `coopname` берётся из guard'а, `username` тоже — фронт не передаёт.
|
||||
*/
|
||||
async signOnboardingOffer(input: {
|
||||
coopname: string;
|
||||
username: string;
|
||||
document: ISignedDocumentDomainInterface;
|
||||
}): Promise<TransactResult> {
|
||||
if (MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID <= 0) {
|
||||
throw new BadRequestException(
|
||||
'ЦПП «Стол заказов» ещё не активирована (Story 1.7 не выполнена): подписание оферты невозможно'
|
||||
);
|
||||
}
|
||||
|
||||
const coagreement = await this.sovietBlockchainPort.getCoagreement(
|
||||
input.coopname,
|
||||
MARKETPLACE_AGREEMENT_TYPE
|
||||
);
|
||||
if (!coagreement) {
|
||||
throw new BadRequestException(
|
||||
`В кооперативе '${input.coopname}' не настроено соглашение типа '${MARKETPLACE_AGREEMENT_TYPE}' (выполните Story 1.9 / marketplaceAcceptCpp)`
|
||||
);
|
||||
}
|
||||
const programId = Number(coagreement.program_id);
|
||||
if (programId <= 0) {
|
||||
throw new BadRequestException(
|
||||
`Соглашение '${MARKETPLACE_AGREEMENT_TYPE}' в кооперативе '${input.coopname}' не имеет программного wallet'а (program_id=${programId}): подписание через wallet::signagree невозможно`
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[MARKETPLACE.L3] signOnboardingOffer → wallet::signagree (${input.coopname}/${input.username} program_id=${programId} draft_id=${coagreement.draft_id})`
|
||||
);
|
||||
|
||||
return await this.walletBlockchainPort.signProgramAgreement({
|
||||
coopname: input.coopname,
|
||||
username: input.username,
|
||||
program_id: programId,
|
||||
draft_id: Number(coagreement.draft_id),
|
||||
document: input.document,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const MARKETPLACE_ONBOARDING_SERVICE = Symbol('MARKETPLACE_ONBOARDING_SERVICE');
|
||||
|
||||
+39
-2
@@ -1,20 +1,25 @@
|
||||
import { Injectable, UseGuards } from '@nestjs/common';
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { config } from '~/config';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
|
||||
import { CurrentMarketplaceMember } from '../decorators/current-marketplace-member.decorator';
|
||||
import type { IMarketplaceCurrentMember } from '../dto/marketplace-current-member.dto';
|
||||
import { MarketplaceOnboardingStateDTO } from '../dto/marketplace-onboarding-state.dto';
|
||||
import { MarketplaceSignOnboardingOfferInputDTO } from '../dto/marketplace-sign-onboarding-offer-input.dto';
|
||||
import { MarketplaceMembershipGuard } from '../guards/marketplace-membership.guard';
|
||||
import { MarketplaceOnboardingService } from '../onboarding/marketplace-onboarding.service';
|
||||
|
||||
/**
|
||||
* Story 1.4: GraphQL endpoint L3 fallback онбординга.
|
||||
* Story 1.4: GraphQL endpoint L3 fallback онбординга + фоллоуап sign-mutation.
|
||||
*
|
||||
* AC: «UI вызывает `marketplaceOnboardingState(member_id)`». В нашей модели
|
||||
* member_id == JWT-пайщик из `MarketplaceMembershipGuard`, аргумент опущен —
|
||||
* сервер сам берёт username из context, фронт не должен подменять.
|
||||
*
|
||||
* Фоллоуап Эпика 1: `marketplaceSignOnboardingOffer` — пайщик подписывает
|
||||
* оферту ЦПП «Стол заказов» прямо со стола, минуя core registration-flow.
|
||||
*/
|
||||
@Resolver()
|
||||
@Injectable()
|
||||
@@ -32,4 +37,36 @@ export class MarketplaceOnboardingResolver {
|
||||
): Promise<MarketplaceOnboardingStateDTO> {
|
||||
return this.onboardingService.getOnboardingState(currentMember.username);
|
||||
}
|
||||
|
||||
/**
|
||||
* L3 fallback sign-mutation: пайщик акцептует оферту ЦПП «Стол заказов»
|
||||
* прямо со стола (`OnboardingMemberPickCpp`) после прохождения gate'а.
|
||||
*
|
||||
* Backend пишет on-chain `wallet::signagree` с `program_id=2` и
|
||||
* `draft_id=1100`. После подтверждения транзакции `AgreementSyncService`
|
||||
* подтянет подпись в PG; следующий `marketplaceOnboardingState` ответит
|
||||
* `requires_gate=false, source='agreement_signed'`.
|
||||
*
|
||||
* Возвращает свежее состояние онбординга — UI не нужно делать второй
|
||||
* запрос. Если sync ещё не успел (high-throughput edge case) — UI
|
||||
* перезапрашивает через короткую задержку.
|
||||
*/
|
||||
@Mutation(() => MarketplaceOnboardingStateDTO, {
|
||||
name: 'marketplaceSignOnboardingOffer',
|
||||
description:
|
||||
'L3-подпись оферты ЦПП «Стол заказов» пайщиком после gate-диалога: on-chain wallet::signagree + ответ в виде обновлённого состояния онбординга',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard)
|
||||
async marketplaceSignOnboardingOffer(
|
||||
@CurrentMarketplaceMember() currentMember: IMarketplaceCurrentMember,
|
||||
@Args('input', { type: () => MarketplaceSignOnboardingOfferInputDTO })
|
||||
input: MarketplaceSignOnboardingOfferInputDTO
|
||||
): Promise<MarketplaceOnboardingStateDTO> {
|
||||
await this.onboardingService.signOnboardingOffer({
|
||||
coopname: config.coopname,
|
||||
username: currentMember.username,
|
||||
document: input.document,
|
||||
});
|
||||
return this.onboardingService.getOnboardingState(currentMember.username);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,15 @@ export interface IVars {
|
||||
protocol_number: string
|
||||
protocol_day_month_year: string
|
||||
}
|
||||
// Стол заказов
|
||||
marketplace_program?: { // Положение о ЦПП «СТОЛ ЗАКАЗОВ»
|
||||
protocol_number: string
|
||||
protocol_day_month_year: string
|
||||
}
|
||||
marketplace_offer_template?: { // Шаблон публичной оферты ЦПП «СТОЛ ЗАКАЗОВ»
|
||||
protocol_number: string
|
||||
protocol_day_month_year: string
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -178,6 +187,12 @@ export enum UdataKey {
|
||||
|
||||
/** Дата создания дополнительного соглашения по хранению имущества */
|
||||
BLAGOROST_STORAGE_AGREEMENT_CREATED_AT = 'blagorost_storage_agreement_created_at',
|
||||
|
||||
/** Номер оферты ЦПП «Стол заказов» (Эпик 1 / Story 1.7 / 1.11) */
|
||||
MARKETPLACE_AGREEMENT_NUMBER = 'marketplace_agreement_number',
|
||||
|
||||
/** Дата создания оферты ЦПП «Стол заказов» */
|
||||
MARKETPLACE_AGREEMENT_CREATED_AT = 'marketplace_agreement_created_at',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,9 +3,17 @@ import type { ICommonUser, ICooperativeData, IVars } from '../../model'
|
||||
|
||||
export const registry_id = 1101
|
||||
|
||||
// Модель действия для генерации
|
||||
// Модель действия для генерации.
|
||||
//
|
||||
// Эпик 1 / Story 1.11 + Story 1.4 (L3 fallback). Backend marketplace вычисляет
|
||||
// `marketplace_agreement_number` и `marketplace_agreement_created_at` в момент
|
||||
// L3-подписи (или core registration-flow при L2) и передаёт их в render
|
||||
// явно — без обращения к Udata, потому что они уникальны для одной подписи
|
||||
// и не переиспользуются как для Благороста.
|
||||
export interface Action extends IGenerate {
|
||||
registry_id: number
|
||||
marketplace_agreement_number: string
|
||||
marketplace_agreement_created_at: string
|
||||
}
|
||||
|
||||
export type Meta = IMetaDocument & Action
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Queries } from '@coopenomics/sdk';
|
||||
import { Mutations, Queries } from '@coopenomics/sdk';
|
||||
import { Cooperative } from 'cooptypes';
|
||||
import { DigitalDocument } from 'src/shared/lib/document';
|
||||
import { client } from 'src/shared/api/client';
|
||||
import { useSessionStore } from 'src/entities/Session/model';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
|
||||
/**
|
||||
* Эпик 1 / Story 1.4: L3 онбординг пайщика — состояние gate для
|
||||
* marketplace. Сервер сам определяет username из JWT context.
|
||||
* Эпик 1 / Story 1.4 + фоллоуап: L3 онбординг пайщика на стол заказов.
|
||||
*
|
||||
* - `fetchOnboardingState`: GET state (Story 1.4), сервер берёт username из JWT.
|
||||
* - `signOnboardingOffer`: фоллоуап, MUT `marketplaceSignOnboardingOffer` —
|
||||
* генерирует оферту (registry_id=1101), подписывает локальным WIF и
|
||||
* отправляет в контроллер, который пишет on-chain `wallet::signagree`.
|
||||
*/
|
||||
|
||||
export type MarketplaceOnboardingStateView =
|
||||
@@ -16,3 +24,53 @@ export async function fetchOnboardingState(): Promise<MarketplaceOnboardingState
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подписать оферту ЦПП «Стол заказов» прямо со стола.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Сгенерировать инстанс оферты на backend (registry_id=1101) с уникальным
|
||||
* marketplace_agreement_number и текущей датой.
|
||||
* 2. Подписать локальным WIF пайщика (signatureId=1, signer=username).
|
||||
* 3. Отправить подписанный документ через `marketplaceSignOnboardingOffer`,
|
||||
* контроллер сам выполнит on-chain `wallet::signagree` от лица coopname.
|
||||
*
|
||||
* Возвращает свежее состояние онбординга — UI решает что показать
|
||||
* (`requires_gate=false, source='agreement_signed'` → редирект на /market).
|
||||
*/
|
||||
export async function signOnboardingOffer(): Promise<MarketplaceOnboardingStateView> {
|
||||
const session = useSessionStore();
|
||||
const system = useSystemStore();
|
||||
const username = session.username;
|
||||
if (!username) throw new Error('Пайщик не авторизован');
|
||||
const coopname = system.info.coopname;
|
||||
if (!coopname) throw new Error('Не определён кооператив');
|
||||
|
||||
const now = new Date();
|
||||
// Номер соглашения: краткая отметка времени, уникальная в пределах пайщика.
|
||||
// Финальный формат может быть переопределён coopname-конвенцией; пока
|
||||
// используется компактный timestamp + хеш-суффикс для разрешения коллизий.
|
||||
const marketplace_agreement_number = `MA-${now.getTime()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const marketplace_agreement_created_at = `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${now.getFullYear()} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
||||
|
||||
const document = new DigitalDocument();
|
||||
await document.generate({
|
||||
registry_id: Cooperative.Registry.MarketplaceOffer.registry_id,
|
||||
coopname,
|
||||
username,
|
||||
marketplace_agreement_number,
|
||||
marketplace_agreement_created_at,
|
||||
});
|
||||
await document.sign(username);
|
||||
|
||||
if (!document.signedDocument) {
|
||||
throw new Error('Не удалось подписать оферту');
|
||||
}
|
||||
|
||||
const { [Mutations.Marketplace.MarketplaceSignOnboardingOffer.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.MarketplaceSignOnboardingOffer.mutation,
|
||||
{ input: { document: document.signedDocument } },
|
||||
);
|
||||
return result as MarketplaceOnboardingStateView;
|
||||
}
|
||||
|
||||
+37
-15
@@ -8,6 +8,7 @@ import {
|
||||
} from 'src/widgets/Marketplace/OnboardingCPPGate';
|
||||
import {
|
||||
fetchOnboardingState,
|
||||
signOnboardingOffer,
|
||||
type MarketplaceOnboardingStateView,
|
||||
} from '../api';
|
||||
|
||||
@@ -66,19 +67,39 @@ async function load(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function onAccept(_documentIds: string[]): void {
|
||||
// L2 подпись делается через core wallet::signagree. На MVP — редирект
|
||||
// пайщика в Registrator-мастер, где собран нужный шаг подписи. После
|
||||
// подписи backend через listener marketplace::onAgreementSigned пересчитает
|
||||
// requires_gate и пайщик попадёт на /market автоматически (см. Story 1.11).
|
||||
Notify.create({
|
||||
type: 'info',
|
||||
message: 'Подписание ЦПП — через Registrator. Перенаправляем…',
|
||||
timeout: 1500,
|
||||
});
|
||||
setTimeout(() => {
|
||||
void router.push({ name: 'registrator' });
|
||||
}, 800);
|
||||
async function onAccept(_documentIds: string[]): Promise<void> {
|
||||
// L3 подпись прямо со стола: фоллоуап Эпика 1 (mutation
|
||||
// `marketplaceSignOnboardingOffer`). Frontend рендерит оферту 1101 через
|
||||
// documentFactory, подписывает локальным WIF, отправляет на backend, а тот
|
||||
// вызывает on-chain `wallet::signagree` от лица coopname.
|
||||
// После успешной подписи перезагружаем состояние онбординга — если
|
||||
// requires_gate=false, ведём пайщика на каталог.
|
||||
loading.value = true;
|
||||
try {
|
||||
state.value = await signOnboardingOffer();
|
||||
if (state.value && !state.value.requires_gate) {
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Оферта ЦПП «Стол заказов» подписана. Открываем каталог…',
|
||||
timeout: 1500,
|
||||
});
|
||||
setTimeout(() => {
|
||||
void router.push({ name: 'marketplace-catalog' });
|
||||
}, 800);
|
||||
} else {
|
||||
// Sync ещё не подтянул запись из chain — даём UI шанс перезапросить
|
||||
// вручную через перезагрузку страницы.
|
||||
Notify.create({
|
||||
type: 'info',
|
||||
message: 'Подпись отправлена. Подтверждение из блокчейна ожидается — обновите страницу через несколько секунд.',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
Notify.create({ type: 'negative', message });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onDecline(): void {
|
||||
@@ -115,9 +136,10 @@ q-page.mp-role-orderer.mp-member-cpp(role="region", aria-label="Подключе
|
||||
v-if="state && state.requires_gate && cppDocuments.length",
|
||||
title="Стол заказов — пакет ЦПП",
|
||||
subtitle="Эпик 1 / L3 онбординг пайщика",
|
||||
lead-text="Ознакомьтесь с офертой и Положением ЦПП «Стол заказов». Подпись делается через Registrator-мастер кооператива.",
|
||||
lead-text="Ознакомьтесь с офертой и Положением ЦПП «Стол заказов». При нажатии «Подписать» документ будет подписан вашим электронным ключом и отправлен в блокчейн.",
|
||||
:documents="cppDocuments",
|
||||
confirm-label="Перейти к подписанию",
|
||||
confirm-label="Подписать оферту"
|
||||
:busy="loading",
|
||||
@accept="onAccept",
|
||||
@decline="onDecline"
|
||||
)
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
unelevated
|
||||
color="primary"
|
||||
:label="confirmLabel"
|
||||
:disable="!allRequiredAccepted"
|
||||
:disable="!allRequiredAccepted || busy"
|
||||
:loading="busy"
|
||||
@click="$emit('accept', acceptedDocs)"
|
||||
/>
|
||||
</q-card-actions>
|
||||
@@ -67,6 +68,7 @@ const props = defineProps({
|
||||
leadText: { type: String, default: 'Ознакомьтесь с пакетом документов и подтвердите согласие.' },
|
||||
documents: { type: Array as PropType<CPPDocument[]>, required: true },
|
||||
confirmLabel: { type: String, default: 'Принять и продолжить' },
|
||||
busy: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
|
||||
@@ -367,6 +367,23 @@ export const meta = {
|
||||
3. **`branch-chairman/branch-orders` auto-detect braname**: подтянуть `marketplaceWhoAmI` или `marketplace_member_wallet` вместо ручного ввода (председатель КУ привязан к одному branch через trustee).
|
||||
4. **~~Прогон harness'а~~ ✅ выполнен** — 9 PNG установлены, admonition сняты (коммит `ee06d405136`, 2026-05-22). MD-проза готова.
|
||||
|
||||
### 9.14. Backend-фоллоуап Эпика 1 реализован (вариант A) — magistral II разблокирована (2026-05-22 вечер)
|
||||
|
||||
Пользователь выбрал (A): полная реализация. Сделано:
|
||||
|
||||
- **Factory adapters 1100 + 1101** в `components/factory/src/{Actions,Templates}/` + регистрация в `Templates/registry.ts` и factory `src/index.ts` map. Сборка зелёная.
|
||||
- **cooptypes расширены** под marketplace: `IVars.marketplace_program?` + `IVars.marketplace_offer_template?`; `UdataKey.MARKETPLACE_AGREEMENT_NUMBER/CREATED_AT`; Action `1101.MarketplaceOffer` теперь принимает `marketplace_agreement_number` + `marketplace_agreement_created_at`.
|
||||
- **VarsSchema** в factory расширена соответствующими полями (валидация JSONSchema).
|
||||
- **L3 mutation `marketplaceSignOnboardingOffer`** в `marketplace-onboarding.resolver.ts` + метод `signOnboardingOffer()` в `MarketplaceOnboardingService` — лукап coagreement по `MARKETPLACE_AGREEMENT_TYPE`, делегирование в `walletBlockchainPort.signProgramAgreement` (program_id из coagreement, draft_id оттуда же, document — подписан фронтом).
|
||||
- **DTO** `MarketplaceSignOnboardingOfferInputDTO` — только signed document; coopname+username берутся из guard'а/config'а (защита от подмены).
|
||||
- **SDK обёртка** `Mutations.Marketplace.MarketplaceSignOnboardingOffer` + zeus regen.
|
||||
- **Vue dialog** `OnboardingMemberPickCpp`: `onAccept` теперь рендерит оферту (registry_id=1101), подписывает локальным WIF и отправляет через новую mutation; после успеха — Notify «Оферта подписана» + редирект на `/market/catalog`. Раньше был редирект в Registrator — убран.
|
||||
- **`OnboardingCPPGate`** widget получил пропс `busy: boolean` для loading-state кнопки подписи.
|
||||
|
||||
**Реальная проверка остаётся на e2e-прогон harness'а `orderer/order-create`:** ekaterina входит, видит gate, подписывает → caталог → «Заказать» → Notify «Заказ создан». Сценарий сделать следующим шагом.
|
||||
|
||||
**Лицензия на дальнейшую работу:** пользователь явно подтвердил «полная реализация — стандартный сценарий, при регистрации и для существующих пайщиков должно работать». Текст оферты остаётся «рыбой» в cooptypes 1100/1101 (юр.отдел доделает позже без рисков для подписей: registry_id и flow не меняются).
|
||||
|
||||
### 9.13. Магистраль II заблокирована backend-фоллоуапом Эпика 1 — ждём решения пользователя (2026-05-22 вечер)
|
||||
|
||||
После фикса cycle_type mapper'а (§9.11) и пополнения L3 Main Wallet (§9.12 был решён через `CHAIN_URL=8918`: ekaterina + ivanpetrov + petrova + sidorov по 10000 RUB на стенде) — магистраль II упёрлась в **новый блокер**: для submit'а Order'а контракт `o.mkt.assign` требует записи в `wallet::users.programs[]` с `program_id=2` (ЦПП «Стол заказов»). У ekaterina этой записи нет — она подписала только ЦПП Кошелёк (program_id=1) через signin onboarding.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { DraftContract } from 'cooptypes'
|
||||
import { MarketplaceOfferTemplate } from '../Templates'
|
||||
import { DocFactory } from '../Factory'
|
||||
import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces'
|
||||
import type { MongoDBConnector } from '../Services/Databazor'
|
||||
|
||||
export { MarketplaceOfferTemplate as Template } from '../Templates'
|
||||
|
||||
export class Factory extends DocFactory<MarketplaceOfferTemplate.Action> {
|
||||
constructor(storage: MongoDBConnector) {
|
||||
super(storage)
|
||||
}
|
||||
|
||||
async generateDocument(data: MarketplaceOfferTemplate.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
|
||||
let template: ITemplate<MarketplaceOfferTemplate.Model>
|
||||
|
||||
if (process.env.SOURCE === 'local') {
|
||||
template = MarketplaceOfferTemplate.Template
|
||||
}
|
||||
else {
|
||||
template = await this.getTemplate(DraftContract.contractName.production, MarketplaceOfferTemplate.registry_id, data.block_num)
|
||||
}
|
||||
|
||||
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
|
||||
const coop = await this.getCooperative(data.coopname, data.block_num)
|
||||
const vars = await this.getVars(data.coopname, data.block_num)
|
||||
|
||||
const combinedData: MarketplaceOfferTemplate.Model = {
|
||||
meta,
|
||||
coop,
|
||||
vars,
|
||||
}
|
||||
|
||||
await this.validate(combinedData, template.model)
|
||||
const translation = template.translations[meta.lang]
|
||||
const document: IGeneratedDocument = await this.generatePDF('', template.context, combinedData, translation, meta, options?.skip_save)
|
||||
|
||||
return document
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { DraftContract } from 'cooptypes'
|
||||
import { MarketplaceOffer } from '../Templates'
|
||||
import { DocFactory } from '../Factory'
|
||||
import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces'
|
||||
import type { MongoDBConnector } from '../Services/Databazor'
|
||||
|
||||
export { MarketplaceOffer as Template } from '../Templates'
|
||||
|
||||
export class Factory extends DocFactory<MarketplaceOffer.Action> {
|
||||
constructor(storage: MongoDBConnector) {
|
||||
super(storage)
|
||||
}
|
||||
|
||||
async generateDocument(data: MarketplaceOffer.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
|
||||
let template: ITemplate<MarketplaceOffer.Model>
|
||||
|
||||
if (process.env.SOURCE === 'local') {
|
||||
template = MarketplaceOffer.Template
|
||||
}
|
||||
else {
|
||||
template = await this.getTemplate(DraftContract.contractName.production, MarketplaceOffer.registry_id, data.block_num)
|
||||
}
|
||||
|
||||
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
|
||||
const coop = await this.getCooperative(data.coopname, data.block_num)
|
||||
const vars = await this.getVars(data.coopname, data.block_num)
|
||||
const userData = await this.getUser(data.username, data.block_num)
|
||||
const common_user = this.getCommonUser(userData)
|
||||
|
||||
if (!data.marketplace_agreement_number) {
|
||||
throw new Error('marketplace_agreement_number обязателен для генерации оферты ЦПП «Стол заказов»')
|
||||
}
|
||||
if (!data.marketplace_agreement_created_at) {
|
||||
throw new Error('marketplace_agreement_created_at обязателен для генерации оферты ЦПП «Стол заказов»')
|
||||
}
|
||||
|
||||
const combinedData: MarketplaceOffer.Model = {
|
||||
meta,
|
||||
coop,
|
||||
vars,
|
||||
common_user,
|
||||
marketplace_agreement_number: data.marketplace_agreement_number,
|
||||
marketplace_agreement_created_at: data.marketplace_agreement_created_at,
|
||||
}
|
||||
|
||||
await this.validate(combinedData, template.model)
|
||||
const translation = template.translations[meta.lang]
|
||||
const document: IGeneratedDocument = await this.generatePDF(common_user.full_name_or_short_name, template.context, combinedData, translation, meta, options?.skip_save)
|
||||
|
||||
return document
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,10 @@ export * as BlagorostProgramTemplate from './998.BlagorostProgramTemplate'
|
||||
export * as GenerationContractTemplate from './997.GenerationContractTemplate'
|
||||
export * as BlagorostOfferTemplate from './999.BlagorostOfferTemplate'
|
||||
|
||||
// Marketplace (Стол заказов) — Эпик 1 (онбординг ЦПП)
|
||||
export * as MarketplaceOfferTemplate from './1100.MarketplaceOfferTemplate'
|
||||
export * as MarketplaceOffer from './1101.MarketplaceOffer'
|
||||
|
||||
// Marketplace (Стол заказов) — Эпик 5
|
||||
export * as MarketplaceAplReception from './1102.MarketplaceAplReception'
|
||||
export * as MarketplaceTransportNote from './1103.MarketplaceTransportNote'
|
||||
|
||||
@@ -127,6 +127,26 @@ export const VarsSchema: JSONSchemaType<IVars> = {
|
||||
required: ['protocol_number', 'protocol_day_month_year'],
|
||||
additionalProperties: true,
|
||||
},
|
||||
marketplace_program: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
protocol_number: { type: 'string' },
|
||||
protocol_day_month_year: { type: 'string' },
|
||||
},
|
||||
required: ['protocol_number', 'protocol_day_month_year'],
|
||||
additionalProperties: true,
|
||||
},
|
||||
marketplace_offer_template: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
protocol_number: { type: 'string' },
|
||||
protocol_day_month_year: { type: 'string' },
|
||||
},
|
||||
required: ['protocol_number', 'protocol_day_month_year'],
|
||||
additionalProperties: true,
|
||||
},
|
||||
},
|
||||
required: [ // соблюдать порядок следования!
|
||||
// 'deleted', //not_required
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { CooperativeSchema } from '../Schema'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { VarsSchema } from '../Schema/VarsSchema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.MarketplaceOfferTemplate.registry_id
|
||||
|
||||
// Шаблон публичной оферты ЦПП «Стол заказов».
|
||||
// Эпик 1 / Story 1.7 / 1.9 — утверждается Советом, после accept'a
|
||||
// регистрируется в core AgreementRegistry под id `marketplace_offer`.
|
||||
export type Action = Cooperative.Registry.MarketplaceOfferTemplate.Action
|
||||
|
||||
export type Model = Cooperative.Registry.MarketplaceOfferTemplate.Model
|
||||
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
meta: IMetaJSONSchema,
|
||||
coop: CooperativeSchema,
|
||||
vars: VarsSchema,
|
||||
},
|
||||
required: ['meta', 'coop', 'vars'],
|
||||
additionalProperties: true,
|
||||
}
|
||||
|
||||
export const Template: ITemplate<Model> = {
|
||||
title: Cooperative.Registry.MarketplaceOfferTemplate.title,
|
||||
description: Cooperative.Registry.MarketplaceOfferTemplate.description,
|
||||
model: Schema,
|
||||
context: Cooperative.Registry.MarketplaceOfferTemplate.context,
|
||||
translations: Cooperative.Registry.MarketplaceOfferTemplate.translations,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { CooperativeSchema } from '../Schema'
|
||||
import { CommonUserSchema } from '../Schema/CommonUserSchema'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { VarsSchema } from '../Schema/VarsSchema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.MarketplaceOffer.registry_id
|
||||
|
||||
// Инстанс публичной оферты ЦПП «Стол заказов» для конкретного пайщика.
|
||||
// Эпик 1 / Story 1.11 (L2 onboarding) + Story 1.4 (L3 fallback gate). Подписывается
|
||||
// пайщиком через wallet::signagree (program_id=2 «marketplace», draft_id=1100).
|
||||
export type Action = Cooperative.Registry.MarketplaceOffer.Action
|
||||
|
||||
export type Model = Cooperative.Registry.MarketplaceOffer.Model
|
||||
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
meta: IMetaJSONSchema,
|
||||
coop: CooperativeSchema,
|
||||
vars: VarsSchema,
|
||||
common_user: CommonUserSchema,
|
||||
marketplace_agreement_number: { type: 'string' },
|
||||
marketplace_agreement_created_at: { type: 'string' },
|
||||
},
|
||||
required: ['meta', 'coop', 'vars', 'common_user', 'marketplace_agreement_number', 'marketplace_agreement_created_at'],
|
||||
additionalProperties: true,
|
||||
}
|
||||
|
||||
export const Template: ITemplate<Model> = {
|
||||
title: Cooperative.Registry.MarketplaceOffer.title,
|
||||
description: Cooperative.Registry.MarketplaceOffer.description,
|
||||
model: Schema,
|
||||
context: Cooperative.Registry.MarketplaceOffer.context,
|
||||
translations: Cooperative.Registry.MarketplaceOffer.translations,
|
||||
}
|
||||
@@ -66,6 +66,10 @@ export * as GenerationConvertStatement from './1080.GenerationConvertStatement'
|
||||
export * as CapitalizationToMainWalletConvertStatement from './1090.CapitalizationToMainWalletConvertStatement'
|
||||
export * as SosediAgreement from './699.SosediAgreement'
|
||||
|
||||
// Marketplace (Стол заказов) — Эпик 1 (онбординг ЦПП)
|
||||
export * as MarketplaceOfferTemplate from './1100.MarketplaceOfferTemplate'
|
||||
export * as MarketplaceOffer from './1101.MarketplaceOffer'
|
||||
|
||||
// Marketplace (Стол заказов) — Эпик 5
|
||||
export * as MarketplaceAplReception from './1102.MarketplaceAplReception'
|
||||
export * as MarketplaceTransportNote from './1103.MarketplaceTransportNote'
|
||||
|
||||
@@ -68,6 +68,8 @@ import * as GeneratorOfferTemplate from './995.GeneratorOfferTemplate'
|
||||
import * as GeneratorOffer from './996.GeneratorOffer'
|
||||
import * as GeneratorProgramTemplate from './994.GeneratorProgramTemplate'
|
||||
import * as BlagorostOffer from './1000.BlagorostOffer'
|
||||
import * as MarketplaceOfferTemplate from './1100.MarketplaceOfferTemplate'
|
||||
import * as MarketplaceOffer from './1101.MarketplaceOffer'
|
||||
import * as MarketplaceAplReception from './1102.MarketplaceAplReception'
|
||||
import * as MarketplaceTransportNote from './1103.MarketplaceTransportNote'
|
||||
import * as MarketplaceReturnStatement from './1104.MarketplaceReturnStatement'
|
||||
@@ -132,6 +134,8 @@ export const Registry = {
|
||||
1072: CapitalizationPropertyInvestAct,
|
||||
1080: GenerationConvertStatement,
|
||||
1090: CapitalizationToMainWalletConvertStatement,
|
||||
1100: MarketplaceOfferTemplate,
|
||||
1101: MarketplaceOffer,
|
||||
1102: MarketplaceAplReception,
|
||||
1103: MarketplaceTransportNote,
|
||||
1104: MarketplaceReturnStatement,
|
||||
|
||||
@@ -166,6 +166,10 @@ export class Generator implements IGenerator {
|
||||
|
||||
[Actions.CapitalizationToMainWalletConvertStatement.Template.registry_id]: new Actions.CapitalizationToMainWalletConvertStatement.Factory(this.storage), // 1090
|
||||
|
||||
// Marketplace (Стол заказов) — Эпик 1: онбординг ЦПП
|
||||
[Actions.MarketplaceOfferTemplate.Template.registry_id]: new Actions.MarketplaceOfferTemplate.Factory(this.storage), // 1100
|
||||
[Actions.MarketplaceOffer.Template.registry_id]: new Actions.MarketplaceOffer.Factory(this.storage), // 1101
|
||||
|
||||
// Marketplace (Стол заказов) — Эпик 5
|
||||
[Actions.MarketplaceAplReception.Template.registry_id]: new Actions.MarketplaceAplReception.Factory(this.storage), // 1102
|
||||
[Actions.MarketplaceTransportNote.Template.registry_id]: new Actions.MarketplaceTransportNote.Factory(this.storage), // 1103
|
||||
|
||||
@@ -48,6 +48,8 @@ export * as CancelWriteoffDraft from './cancelWriteoffDraft'
|
||||
export * as SubmitWriteoffDraft from './submitWriteoffDraft'
|
||||
/** Эпик 1 / Story 1.9: принятие ЦПП Marketplace кооперативом (L1 onboarding, chairman-only) */
|
||||
export * as MarketplaceAcceptCpp from './marketplaceAcceptCpp'
|
||||
/** Эпик 1 фоллоуап: L3-подпись оферты ЦПП «Стол заказов» пайщиком прямо со стола */
|
||||
export * as MarketplaceSignOnboardingOffer from './marketplaceSignOnboardingOffer'
|
||||
/** Добавить категории в whitelist кооператива (chairman-only) */
|
||||
export * as AddAvailableCategories from './addAvailableCategories'
|
||||
/** Удалить категории из whitelist кооператива (chairman-only) */
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
/**
|
||||
* Эпик 1 фоллоуап — L3 sign-mutation для ЦПП «Стол заказов».
|
||||
*
|
||||
* Пайщик подписывает оферту прямо со стола (страница `OnboardingMemberPickCpp`)
|
||||
* после gate'а Story 1.4. Backend пишет on-chain `wallet::signagree` с
|
||||
* `program_id=2`, возвращает свежее состояние онбординга (без второго запроса
|
||||
* на `marketplaceOnboardingState`).
|
||||
*
|
||||
* Подписанный document — результат `Mutations.Documents.GenerateDocument`
|
||||
* (registry_id=1101) + `LocalWallet.signDocument` на фронте.
|
||||
*/
|
||||
export const name = 'marketplaceSignOnboardingOffer'
|
||||
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [
|
||||
{ input: $('input', 'MarketplaceSignOnboardingOfferInput!') },
|
||||
{
|
||||
requires_gate: true,
|
||||
template_registry_id: true,
|
||||
agreement_id: true,
|
||||
completed_at: true,
|
||||
source: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
input: ModelTypes['MarketplaceSignOnboardingOfferInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -817,6 +817,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
MarketplaceSignAplReceptionInput:{
|
||||
signed_documents:"MarketplaceAplReceptionSignedDocumentInput"
|
||||
},
|
||||
MarketplaceSignOnboardingOfferInput:{
|
||||
document:"SignedDigitalDocumentInput"
|
||||
},
|
||||
MarketplaceSubmitWriteoffDraftInput:{
|
||||
signed_statement:"SignedDigitalDocumentInput"
|
||||
},
|
||||
@@ -1396,6 +1399,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
marketplaceSignAplReceptionAsSupplier:{
|
||||
data:"MarketplaceSignAplReceptionInput"
|
||||
},
|
||||
marketplaceSignOnboardingOffer:{
|
||||
input:"MarketplaceSignOnboardingOfferInput"
|
||||
},
|
||||
marketplaceSubmitWriteoffDraft:{
|
||||
data:"MarketplaceSubmitWriteoffDraftInput"
|
||||
},
|
||||
@@ -4705,6 +4711,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
marketplaceSetKUStatus:"MarketplaceKUDetails",
|
||||
marketplaceSignAplReceptionAsChairman:"MarketplaceAplReceptionResult",
|
||||
marketplaceSignAplReceptionAsSupplier:"MarketplaceAplReceptionResult",
|
||||
marketplaceSignOnboardingOffer:"MarketplaceOnboardingState",
|
||||
marketplaceSubmitWriteoffDraft:"MarketplaceWriteoffProposal",
|
||||
marketplaceTriggerOpenSubscription:"MarketplaceConsolidatedRequest",
|
||||
marketplaceUpdateOffer:"MarketplaceOffer",
|
||||
|
||||
+561
-1569
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user