[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:
ant
2026-05-22 09:14:53 +00:00
parent c63093eaa4
commit e7144e9f3d
22 changed files with 1084 additions and 1592 deletions
@@ -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;
}
@@ -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');
@@ -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;
}
@@ -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
}
}
+4
View File
@@ -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,
+4
View File
@@ -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>
+7
View File
@@ -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",
File diff suppressed because it is too large Load Diff