[598-50][@ant] feat: единый путь выдачи через бандл — обычные заказы и докладка одним пропозалом

Оператор подписывает signiss1 и кладёт всё (существующие заказы + докладку со
склада) в один оффчейн-бандл; на цепи до подписи пайщика ничего нет, поэтому
отмена пайщиком = отказ от бандла без он-чейн отката (никакого reopeniss и
второй ветки немедленного signiss1). Пайщик одной кнопкой контрподписывает
получение — backend проводит convert?→stockorder?→signiss1→signiss2. Снесён
мёртвый member-путь ordererTasks/listMyReadyToReceive в гейте.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ant
2026-06-16 09:24:30 +00:00
parent 741103eb48
commit 7e9fa99b60
12 changed files with 463 additions and 314 deletions
@@ -145,26 +145,62 @@ export class MarketplaceCreateStockProposalLineInputDTO {
signiss1_act!: MarketplaceIssueActSignedDocumentInputDTO;
}
@InputType('MarketplaceCreateOrderProposalLineInput')
export class MarketplaceCreateOrderProposalLineInputDTO {
@Field(() => ID, { description: 'Существующий заказ пайщика к выдаче этим бандлом.' })
@IsString()
@IsNotEmpty()
order_id!: string;
@Field(() => Int, { description: 'Фактическое количество к выдаче (сверено оператором).' })
@IsInt()
@Min(1)
actual_quantity!: number;
@Field(() => String, { description: 'Фактическая цена за единицу (оператор мог скорректировать).' })
@IsString()
@IsNotEmpty()
actual_unit_price!: string;
@Field(() => MarketplaceIssueActSignedDocumentInputDTO, {
description: 'Акт приёма-передачи, подписанный оператором КУ первой подписью.',
})
@ValidateNested()
@Type(() => MarketplaceIssueActSignedDocumentInputDTO)
signiss1_act!: MarketplaceIssueActSignedDocumentInputDTO;
}
@InputType('MarketplaceCreateStockProposalInput')
export class MarketplaceCreateStockProposalInputDTO {
@Field(() => String, { description: 'Кооперативный участок, со склада которого идёт докладка.' })
@Field(() => String, { description: 'Кооперативный участок, со склада которого идёт выдача.' })
@IsString()
@IsNotEmpty()
braname!: string;
@Field(() => String, { description: 'Пайщик-адресат докладки.' })
@Field(() => String, { description: 'Пайщик-адресат бандла.' })
@IsString()
@IsNotEmpty()
member_account!: string;
@Field(() => [MarketplaceCreateStockProposalLineInputDTO], {
description: 'Строки докладки с order_hash и подписью оператора (signiss1).',
nullable: true,
description: 'Строки докладки со склада: order_hash и подпись оператора (signiss1).',
})
@IsOptional()
@IsArray()
@ArrayNotEmpty()
@ValidateNested({ each: true })
@Type(() => MarketplaceCreateStockProposalLineInputDTO)
items!: MarketplaceCreateStockProposalLineInputDTO[];
items?: MarketplaceCreateStockProposalLineInputDTO[];
@Field(() => [MarketplaceCreateOrderProposalLineInputDTO], {
nullable: true,
description: 'Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1).',
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => MarketplaceCreateOrderProposalLineInputDTO)
order_items?: MarketplaceCreateOrderProposalLineInputDTO[];
}
@InputType('MarketplaceResolveStockProposalInput')
@@ -223,10 +259,10 @@ export class MarketplaceStockAcceptPayloadDTO {
@InputType('MarketplaceStockFinalizeLineInput')
export class MarketplaceStockFinalizeLineInputDTO {
@Field(() => String, { description: 'Идентификатор предложения позиции (из payloads).' })
@Field(() => String, { description: 'order_hash строки бандла (из payloads).' })
@IsString()
@IsNotEmpty()
public readonly offer_id!: string;
public readonly order_hash!: string;
@Field(() => MarketplaceIssueActSignedDocumentInputDTO, {
description: 'Акт приёма-передачи, контрподписанный пайщиком (подпись получения).',
@@ -159,7 +159,7 @@ export class MarketplaceStockResolver {
@Mutation(() => MarketplaceStockProposalDTO, {
name: 'marketplaceCreateStockProposal',
description:
'Оператор у стойки формирует докладку пайщику со склада кооператива (с уже подписанными им актами передачи) — пайщику немедленно приходит акт на подпись получения.',
'Оператор у стойки формирует бандл выдачи пайщику (существующие заказы и/или докладка со склада), с уже подписанными им актами передачи — пайщику немедленно приходит акт на подпись получения. До его подписи ничего в блокчейне не происходит.',
})
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard, MarketplaceRoleGuard)
@RequireMarketplaceAccess('StockProposal', 'create:own-KU')
@@ -173,12 +173,18 @@ export class MarketplaceStockResolver {
operator_account: member.username,
braname: data.braname,
member_account: data.member_account,
items: data.items.map((i) => ({
items: (data.items ?? []).map((i) => ({
offer_id: i.offer_id,
quantity: i.quantity,
order_hash: i.order_hash,
signiss1_act: i.signiss1_act as unknown as ISignedDocumentDomainInterface,
})),
order_items: (data.order_items ?? []).map((i) => ({
order_id: i.order_id,
actual_quantity: i.actual_quantity,
actual_unit_price: i.actual_unit_price,
signiss1_act: i.signiss1_act as unknown as ISignedDocumentDomainInterface,
})),
});
return toMarketplaceStockProposalDTO(proposal);
}
@@ -42,6 +42,10 @@ import {
MARKETPLACE_OFFER_REPOSITORY,
type MarketplaceOfferDomainRepository,
} from '../../domain/repositories/marketplace-offer.repository';
import {
MARKETPLACE_ORDER_REPOSITORY,
type MarketplaceOrderDomainRepository,
} from '../../domain/repositories/marketplace-order.repository';
import { MarketplaceStockService, MARKETPLACE_STOCK_SERVICE } from './marketplace-stock.service';
import { MarketplaceOfferStatuses } from '../../domain/entities/marketplace-offer.types';
import type { MarketplaceOfferDomainEntity } from '../../domain/entities/marketplace-offer.entity';
@@ -68,12 +72,30 @@ export interface MarketplaceStockProposalCreateLine {
signiss1_act: ISignedDocumentDomainInterface;
}
/**
* Строка обычного заказа в бандле: заказ уже существует (ACCEPTED_TO_COOP),
* оператор сверил факт (кол-во/цену) и подписал signiss1-акт. На финализации
* заказ не создаётся — сразу выдача (openIssuance+signiss2).
*/
export interface MarketplaceOrderProposalCreateLine {
order_id: string;
/** Фактическое количество к выдаче (оператор сверил при открытии). */
actual_quantity: number;
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string;
/** АПП-выдачи, подписанный оператором первой подписью (signiss1). */
signiss1_act: ISignedDocumentDomainInterface;
}
export interface MarketplaceStockProposalCreateInput {
coopname: string;
operator_account: string;
braname: string;
member_account: string;
/** Строки докладки со склада (заказ родится на финализации). */
items: MarketplaceStockProposalCreateLine[];
/** Строки уже существующих обычных заказов пайщика к выдаче. */
order_items?: MarketplaceOrderProposalCreateLine[];
}
export interface MarketplaceStockProposalAcceptResult {
@@ -131,9 +153,9 @@ export interface MarketplaceStockAcceptPayload {
} | null;
}
/** Строка финализации: offer + контрподписанный пайщиком АПП-выдачи (signiss2). */
/** Строка финализации: order_hash + контрподписанный пайщиком АПП-выдачи (signiss2). */
export interface MarketplaceStockFinalizeLine {
offer_id: string;
order_hash: string;
signed_signiss2_act: MarketplaceIssueActSignedDocumentInputDTO;
}
@@ -163,6 +185,8 @@ export class MarketplaceStockProposalService {
private readonly proposalRepo: MarketplaceStockProposalDomainRepository,
@Inject(MARKETPLACE_OFFER_REPOSITORY)
private readonly offerRepo: MarketplaceOfferDomainRepository,
@Inject(MARKETPLACE_ORDER_REPOSITORY)
private readonly orderRepo: MarketplaceOrderDomainRepository,
@Inject(MARKETPLACE_STOCK_SERVICE)
private readonly stockService: MarketplaceStockService,
@Inject(MARKETPLACE_ISSUANCE_SERVICE)
@@ -224,7 +248,13 @@ export class MarketplaceStockProposalService {
order_hash: item.order_hash,
signiss1_aggregate,
});
neededUnits += this.economyService.lineUnits(item.unit_price, item.quantity, feePercent);
// Конвертация паевого нужна ТОЛЬКО под докладку (строка без order_id):
// её полная стоимость блокируется с членского на stockorder. Обычный
// заказ уже профондирован с паевого на createorder — доплату по факту
// (overage) signiss2 берёт с членского сам и фейлится при нехватке.
if (!item.order_id) {
neededUnits += this.economyService.lineUnits(item.unit_price, item.quantity, feePercent);
}
}
// Дефицит = потребность − уже внесённые членские средства. Конвертируем
@@ -294,6 +324,60 @@ export class MarketplaceStockProposalService {
return offer;
}
/**
* Валидация строки обычного заказа в бандле + снапшот для показа пайщику.
* Заказ обязан быть ACCEPTED_TO_COOP без открытой выдачи, принадлежать
* адресату бандла и выдаваться ИМЕННО с этого КУ. order_hash берётся из
* заказа (бэкенд авторитетен), product_name — из оффера заказа.
*/
private async buildOrderItem(
coopname: string,
braname: string,
member_account: string,
line: MarketplaceOrderProposalCreateLine
): Promise<MarketplaceStockProposalItem> {
if (!Number.isInteger(line.actual_quantity) || line.actual_quantity <= 0) {
throw new BadRequestException('Количество в строке заказа должно быть целым больше нуля.');
}
if (Number.parseFloat(line.actual_unit_price) <= 0) {
throw new BadRequestException('Цена в строке заказа должна быть больше нуля.');
}
if (!line.signiss1_act?.signatures?.length) {
throw new BadRequestException(
'Строка заказа без подписи оператора (signiss1) — переподпишите выдачу.'
);
}
const order = await this.orderRepo.findById(line.order_id);
if (!order || order.coopname !== coopname) {
throw new NotFoundException(`Заказ ${line.order_id} не найден.`);
}
if (order.orderer_account !== member_account) {
throw new BadRequestException(
'Заказ принадлежит другому пайщику — нельзя включить его в бандл этого получателя.'
);
}
if (order.delivery_braname !== braname) {
throw new BadRequestException(
`Заказ выдаётся на другом КУ (${order.delivery_braname}), со стойки ${braname} не выдаётся.`
);
}
if (!order.awaits_chairman_issue_open) {
throw new ConflictException(
`Заказ ${order.id} (статус «${order.status}») не готов к открытию выдачи.`
);
}
const offer = await this.offerRepo.findById(order.offer_id);
return {
order_id: order.id,
offer_id: order.offer_id,
quantity: line.actual_quantity,
unit_price: line.actual_unit_price,
product_name: offer?.product_name ?? 'Товар по предложению',
order_hash: order.order_hash,
signiss1_act: line.signiss1_act,
};
}
/**
* Нагрузка оператору для подписи signiss1 при формировании бандла докладки.
* По строке: детерминированный order_hash (он же уйдёт в будущий stockorder) +
@@ -350,16 +434,21 @@ export class MarketplaceStockProposalService {
async createProposal(
input: MarketplaceStockProposalCreateInput
): Promise<MarketplaceStockProposalDomainEntity> {
if (input.items.length === 0) {
throw new BadRequestException('Предложение пустое — добавьте позиции из остатка.');
const orderLines = input.order_items ?? [];
if (input.items.length === 0 && orderLines.length === 0) {
throw new BadRequestException('Бандл пуст — добавьте позиции заказа или докладку со склада.');
}
if (input.member_account === input.operator_account) {
throw new BadRequestException('Нельзя отправить предложение самому себе.');
}
// Снапшоты строк: только активные офферы кооператива ИМЕННО этого КУ,
// количество в пределах остатка. Каждая строка несёт order_hash и
// подписанный оператором signiss1-акт — пайщику останется одна подпись.
// Единый бандл несёт строки двух видов:
// • докладка — активные офферы остатка ИМЕННО этого КУ, кол-во в пределах
// остатка; заказ родится на финализации.
// • обычный заказ — уже существующий ACCEPTED_TO_COOP заказ пайщика
// (профондирован с паевого), сразу к выдаче.
// Каждая строка несёт order_hash и подписанный оператором signiss1-акт —
// пайщику останется одна подпись.
const items: MarketplaceStockProposalItem[] = [];
for (const line of input.items) {
const offer = await this.validateStockLine(
@@ -385,6 +474,11 @@ export class MarketplaceStockProposalService {
signiss1_act: line.signiss1_act,
});
}
for (const line of orderLines) {
items.push(
await this.buildOrderItem(input.coopname, input.braname, input.member_account, line)
);
}
const proposal = await this.proposalRepo.create({
coopname: input.coopname,
@@ -433,9 +527,10 @@ export class MarketplaceStockProposalService {
const feePercent = await this.economyService.getMembershipFeeContractPercent(coopname);
// signiss2-акт по offer_id из подписанной пайщиком нагрузки.
const signiss2ByOffer = new Map(
(input.order_lines ?? []).map((l) => [l.offer_id, l.signed_signiss2_act])
// signiss2-акт по order_hash строки (уникален в бандле; offer_id мог бы
// совпасть у двух обычных заказов одного оффера).
const signiss2ByHash = new Map(
(input.order_lines ?? []).map((l) => [l.order_hash, l.signed_signiss2_act])
);
// ── 0) Контрольная сверка: пайщик подписал РОВНО тот акт, что мы выдали ──
@@ -446,23 +541,26 @@ export class MarketplaceStockProposalService {
for (const item of proposal.items) {
if (!item.order_hash || !item.signiss1_act) {
throw new ConflictException(
'Докладка в устаревшем формате (без подписи оператора) — переформируйте её у стойки.'
'Бандл в устаревшем формате (без подписи оператора) — переформируйте его у стойки.'
);
}
const submitted = signiss2ByOffer.get(item.offer_id);
const submitted = signiss2ByHash.get(item.order_hash);
if (!submitted) {
throw new BadRequestException(
'Состав подписания не совпадает с докладкой — обновите подписание.'
'Состав подписания не совпадает с бандлом — обновите подписание.'
);
}
await this.assertCountersignMatchesStored(item, submitted, member_account);
}
// Потребность и дефицит сверх членских (как в getAcceptSignablePayloads —
// суммы обязаны совпасть с подписанным Заявлением).
// Потребность и дефицит сверх членских — ТОЛЬКО по докладке (как в
// getAcceptSignablePayloads; суммы обязаны совпасть с подписанным Заявлением).
// Обычный заказ профондирован с паевого ранее и в дефицит не входит.
let neededUnits = 0n;
for (const item of proposal.items) {
neededUnits += this.economyService.lineUnits(item.unit_price, item.quantity, feePercent);
if (!item.order_id) {
neededUnits += this.economyService.lineUnits(item.unit_price, item.quantity, feePercent);
}
}
const memberUnits = await this.readMemberUnits(coopname, member_account);
const deficitUnits = neededUnits > memberUnits ? neededUnits - memberUnits : 0n;
@@ -497,21 +595,33 @@ export class MarketplaceStockProposalService {
});
}
// ── 2) Создание заказов из остатка (атомарно: на фейле — отмена всех) ────
const created: Array<{ order_id: string; item: MarketplaceStockProposalItem }> = [];
// ── 2) Заказы к выдаче: докладка → создаём из остатка (атомарно, на фейле —
// отмена созданных); обычный заказ → уже существует, берём его order_id.
const issuables: Array<{
order_id: string;
item: MarketplaceStockProposalItem;
signiss2: MarketplaceIssueActSignedDocumentInputDTO;
}> = [];
const createdStock: Array<{ order_id: string }> = [];
try {
for (const item of proposal.items) {
if (!item.order_hash || !item.signiss1_act) {
throw new ConflictException(
'Докладка в устаревшем формате (без подписи оператора) — переформируйте её у стойки.'
'Бандл в устаревшем формате (без подписи оператора) — переформируйте его у стойки.'
);
}
if (!signiss2ByOffer.get(item.offer_id)) {
const signiss2 = signiss2ByHash.get(item.order_hash);
if (!signiss2) {
throw new BadRequestException(
'Состав подписания не совпадает с докладкой — обновите подписание.'
'Состав подписания не совпадает с бандлом — обновите подписание.'
);
}
// Средства уже в членском (конвертация выше при дефиците) — без Заявления.
if (item.order_id) {
// Обычный заказ — уже ACCEPTED_TO_COOP, создавать нечего.
issuables.push({ order_id: item.order_id, item, signiss2 });
continue;
}
// Докладка: средства уже в членском (конвертация выше при дефиците) — без Заявления.
const { order } = await this.stockService.createStockOrder({
coopname,
orderer_account: member_account,
@@ -520,16 +630,17 @@ export class MarketplaceStockProposalService {
checkout_id: proposal.id,
order_hash: item.order_hash,
});
created.push({ order_id: order.id, item });
createdStock.push({ order_id: order.id });
issuables.push({ order_id: order.id, item, signiss2 });
}
} catch (error) {
for (const { order_id } of created) {
for (const { order_id } of createdStock) {
try {
await this.stockService.cancelStockOrder(
coopname,
order_id,
member_account,
'Подписание докладки не завершилось — строка отменена'
'Подписание бандла не завершилось — строка отменена'
);
} catch (compErr: any) {
this.logger.error(
@@ -542,7 +653,7 @@ export class MarketplaceStockProposalService {
// ── 3) Выдача: signiss1 оператора → signiss2 пайщика (заказ RECEIVED) ────
const order_ids: string[] = [];
for (const { order_id, item } of created) {
for (const { order_id, item, signiss2 } of issuables) {
await this.issuanceService.openIssuance({
coopname,
chairman_account: proposal.operator_account,
@@ -555,7 +666,7 @@ export class MarketplaceStockProposalService {
coopname,
orderer_account: member_account,
order_id,
signed_document: signiss2ByOffer.get(item.offer_id)!,
signed_document: signiss2,
});
order_ids.push(order_id);
}
@@ -23,7 +23,17 @@ export const MarketplaceStockProposalStatuses = {
CANCELLED: 'CANCELLED',
} as const satisfies Record<string, MarketplaceStockProposalStatus>;
/** Строка предложения: опубликованный оффер кооператива + количество. */
/**
* Строка бандла выдачи. Единый путь: бандл несёт И докладку со склада, И уже
* существующие обычные заказы пайщика — отличает их `order_id`:
* - `order_id` отсутствует → ДОКЛАДКА: заказ из остатка родится на финализации
* (stockorder по `order_hash`), средства блокируются с членского.
* - `order_id` задан → ОБЫЧНЫЙ ЗАКАЗ: он уже ACCEPTED_TO_COOP (профондирован с
* паевого на createorder); на финализации создавать нечего — сразу выдача.
* В обоих случаях signiss1 оператора отложен в бандл (оффчейн) и уходит на цепь
* только вместе с signiss2 пайщика при подписи — поэтому отмена = отказ от
* бандла, без он-чейн отката.
*/
export interface MarketplaceStockProposalItem {
offer_id: string;
quantity: number;
@@ -32,9 +42,15 @@ export interface MarketplaceStockProposalItem {
/** Снапшот наименования — для показа пайщику без дозапросов. */
product_name: string;
/**
* Детерминированный order_hash будущего заказа из остатка — рождается при
* Существующий обычный заказ пайщика (ACCEPTED_TO_COOP), выдаваемый этим
* бандлом. Пусто → строка докладки со склада (заказ родится на финализации).
*/
order_id?: string;
/**
* Детерминированный order_hash заказа из остатка (докладка) — рождается при
* формировании бандла (оператором у стойки), чтобы и signiss1-акт оператора,
* и заказ создавались по одному и тому же хэшу.
* и заказ создавались по одному и тому же хэшу. Для строки обычного заказа —
* order_hash самого заказа (берётся бэкендом из заказа).
*/
order_hash?: string;
/**
+5 -1
View File
@@ -736,6 +736,9 @@ export const AllTypesProps: Record<string,any> = {
delivery_points:"MarketplaceOfferDeliveryPointInput",
images:"MarketplaceOfferImageUploadInput"
},
MarketplaceCreateOrderProposalLineInput:{
signiss1_act:"MarketplaceIssueActSignedDocumentInput"
},
MarketplaceCreateReturnClaimInput:{
defect_category:"MarketplaceReturnClaimDefectCategory",
photos:"MarketplaceReturnClaimPhotoUploadInput",
@@ -745,7 +748,8 @@ export const AllTypesProps: Record<string,any> = {
groups:"MarketplaceShipmentGroupInput"
},
MarketplaceCreateStockProposalInput:{
items:"MarketplaceCreateStockProposalLineInput"
items:"MarketplaceCreateStockProposalLineInput",
order_items:"MarketplaceCreateOrderProposalLineInput"
},
MarketplaceCreateStockProposalLineInput:{
signiss1_act:"MarketplaceIssueActSignedDocumentInput"
+79 -30
View File
@@ -6833,6 +6833,16 @@ export type ValueTypes = {
unit_of_measure: string | Variable<any, string>,
unlimited_flag: boolean | Variable<any, string>,
warranty_days: number | Variable<any, string>
};
["MarketplaceCreateOrderProposalLineInput"]: {
/** Фактическое количество к выдаче (сверено оператором). */
actual_quantity: number | Variable<any, string>,
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string | Variable<any, string>,
/** Существующий заказ пайщика к выдаче этим бандлом. */
order_id: ValueTypes["ID"] | Variable<any, string>,
/** Акт приёма-передачи, подписанный оператором КУ первой подписью. */
signiss1_act: ValueTypes["MarketplaceIssueActSignedDocumentInput"] | Variable<any, string>
};
["MarketplaceCreateReturnClaimInput"]: {
/** Возвращаемое количество единиц (по умолчанию — выданное количество). */
@@ -6861,12 +6871,14 @@ export type ValueTypes = {
['...on MarketplaceCreateShipmentResult']?: Omit<ValueTypes["MarketplaceCreateShipmentResult"], "...on MarketplaceCreateShipmentResult">
}>;
["MarketplaceCreateStockProposalInput"]: {
/** Кооперативный участок, со склада которого идёт докладка. */
/** Кооперативный участок, со склада которого идёт выдача. */
braname: string | Variable<any, string>,
/** Строки докладки с order_hash и подписью оператора (signiss1). */
items: Array<ValueTypes["MarketplaceCreateStockProposalLineInput"]> | Variable<any, string>,
/** Пайщик-адресат докладки. */
member_account: string | Variable<any, string>
/** Строки докладки со склада: order_hash и подпись оператора (signiss1). */
items?: Array<ValueTypes["MarketplaceCreateStockProposalLineInput"]> | undefined | null | Variable<any, string>,
/** Пайщик-адресат бандла. */
member_account: string | Variable<any, string>,
/** Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1). */
order_items?: Array<ValueTypes["MarketplaceCreateOrderProposalLineInput"]> | undefined | null | Variable<any, string>
};
["MarketplaceCreateStockProposalLineInput"]: {
/** Предложение кооператива из остатка. */
@@ -8316,8 +8328,8 @@ export type ValueTypes = {
['...on MarketplaceStockAcceptPayload']?: Omit<ValueTypes["MarketplaceStockAcceptPayload"], "...on MarketplaceStockAcceptPayload">
}>;
["MarketplaceStockFinalizeLineInput"]: {
/** Идентификатор предложения позиции (из payloads). */
offer_id: string | Variable<any, string>,
/** order_hash строки бандла (из payloads). */
order_hash: string | Variable<any, string>,
/** Акт приёма-передачи, контрподписанный пайщиком (подпись получения). */
signed_signiss2_act: ValueTypes["MarketplaceIssueActSignedDocumentInput"] | Variable<any, string>
};
@@ -17766,6 +17778,16 @@ export type ResolverInputTypes = {
unit_of_measure: string,
unlimited_flag: boolean,
warranty_days: number
};
["MarketplaceCreateOrderProposalLineInput"]: {
/** Фактическое количество к выдаче (сверено оператором). */
actual_quantity: number,
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string,
/** Существующий заказ пайщика к выдаче этим бандлом. */
order_id: ResolverInputTypes["ID"],
/** Акт приёма-передачи, подписанный оператором КУ первой подписью. */
signiss1_act: ResolverInputTypes["MarketplaceIssueActSignedDocumentInput"]
};
["MarketplaceCreateReturnClaimInput"]: {
/** Возвращаемое количество единиц (по умолчанию — выданное количество). */
@@ -17793,12 +17815,14 @@ export type ResolverInputTypes = {
__typename?: boolean | `@${string}`
}>;
["MarketplaceCreateStockProposalInput"]: {
/** Кооперативный участок, со склада которого идёт докладка. */
/** Кооперативный участок, со склада которого идёт выдача. */
braname: string,
/** Строки докладки с order_hash и подписью оператора (signiss1). */
items: Array<ResolverInputTypes["MarketplaceCreateStockProposalLineInput"]>,
/** Пайщик-адресат докладки. */
member_account: string
/** Строки докладки со склада: order_hash и подпись оператора (signiss1). */
items?: Array<ResolverInputTypes["MarketplaceCreateStockProposalLineInput"]> | undefined | null,
/** Пайщик-адресат бандла. */
member_account: string,
/** Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1). */
order_items?: Array<ResolverInputTypes["MarketplaceCreateOrderProposalLineInput"]> | undefined | null
};
["MarketplaceCreateStockProposalLineInput"]: {
/** Предложение кооператива из остатка. */
@@ -19202,8 +19226,8 @@ export type ResolverInputTypes = {
__typename?: boolean | `@${string}`
}>;
["MarketplaceStockFinalizeLineInput"]: {
/** Идентификатор предложения позиции (из payloads). */
offer_id: string,
/** order_hash строки бандла (из payloads). */
order_hash: string,
/** Акт приёма-передачи, контрподписанный пайщиком (подпись получения). */
signed_signiss2_act: ResolverInputTypes["MarketplaceIssueActSignedDocumentInput"]
};
@@ -28340,6 +28364,16 @@ export type ModelTypes = {
unit_of_measure: string,
unlimited_flag: boolean,
warranty_days: number
};
["MarketplaceCreateOrderProposalLineInput"]: {
/** Фактическое количество к выдаче (сверено оператором). */
actual_quantity: number,
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string,
/** Существующий заказ пайщика к выдаче этим бандлом. */
order_id: ModelTypes["ID"],
/** Акт приёма-передачи, подписанный оператором КУ первой подписью. */
signiss1_act: ModelTypes["MarketplaceIssueActSignedDocumentInput"]
};
["MarketplaceCreateReturnClaimInput"]: {
/** Возвращаемое количество единиц (по умолчанию — выданное количество). */
@@ -28366,12 +28400,14 @@ export type ModelTypes = {
shipments: Array<ModelTypes["MarketplaceShipment"]>
};
["MarketplaceCreateStockProposalInput"]: {
/** Кооперативный участок, со склада которого идёт докладка. */
/** Кооперативный участок, со склада которого идёт выдача. */
braname: string,
/** Строки докладки с order_hash и подписью оператора (signiss1). */
items: Array<ModelTypes["MarketplaceCreateStockProposalLineInput"]>,
/** Пайщик-адресат докладки. */
member_account: string
/** Строки докладки со склада: order_hash и подпись оператора (signiss1). */
items?: Array<ModelTypes["MarketplaceCreateStockProposalLineInput"]> | undefined | null,
/** Пайщик-адресат бандла. */
member_account: string,
/** Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1). */
order_items?: Array<ModelTypes["MarketplaceCreateOrderProposalLineInput"]> | undefined | null
};
["MarketplaceCreateStockProposalLineInput"]: {
/** Предложение кооператива из остатка. */
@@ -29701,8 +29737,8 @@ export type ModelTypes = {
order_lines: Array<ModelTypes["MarketplaceStockAcceptOrderLine"]>
};
["MarketplaceStockFinalizeLineInput"]: {
/** Идентификатор предложения позиции (из payloads). */
offer_id: string,
/** order_hash строки бандла (из payloads). */
order_hash: string,
/** Акт приёма-передачи, контрподписанный пайщиком (подпись получения). */
signed_signiss2_act: ModelTypes["MarketplaceIssueActSignedDocumentInput"]
};
@@ -30828,7 +30864,7 @@ export type ModelTypes = {
marketplaceCreateReturnClaim: ModelTypes["MarketplaceReturnClaimResult"],
/** Сформировать партии поставки из акцептованной заявки. Каждая группа = одна партия (КУ + вариант доставки + опционально подмножество заказов). Покрытие всех КУ не обязательно — допустима частичная отгрузка и догрузка остатка отдельными партиями. */
marketplaceCreateShipment: ModelTypes["MarketplaceCreateShipmentResult"],
/** Оператор у стойки формирует докладку пайщику со склада кооператива (с уже подписанными им актами передачи) — пайщику немедленно приходит акт на подпись получения. */
/** Оператор у стойки формирует бандл выдачи пайщику (существующие заказы и/или докладка со склада), с уже подписанными им актами передачи — пайщику немедленно приходит акт на подпись получения. До его подписи ничего в блокчейне не происходит. */
marketplaceCreateStockProposal: ModelTypes["MarketplaceStockProposal"],
/** Создаёт ручной черновик проекта списания. На кооператив может быть только один открытый черновик и один проект, отправленный в совет. */
marketplaceCreateWriteoffDraft: ModelTypes["MarketplaceWriteoffProposal"],
@@ -39829,6 +39865,16 @@ export type GraphQLTypes = {
unit_of_measure: string,
unlimited_flag: boolean,
warranty_days: number
};
["MarketplaceCreateOrderProposalLineInput"]: {
/** Фактическое количество к выдаче (сверено оператором). */
actual_quantity: number,
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string,
/** Существующий заказ пайщика к выдаче этим бандлом. */
order_id: GraphQLTypes["ID"],
/** Акт приёма-передачи, подписанный оператором КУ первой подписью. */
signiss1_act: GraphQLTypes["MarketplaceIssueActSignedDocumentInput"]
};
["MarketplaceCreateReturnClaimInput"]: {
/** Возвращаемое количество единиц (по умолчанию — выданное количество). */
@@ -39857,12 +39903,14 @@ export type GraphQLTypes = {
['...on MarketplaceCreateShipmentResult']: Omit<GraphQLTypes["MarketplaceCreateShipmentResult"], "...on MarketplaceCreateShipmentResult">
};
["MarketplaceCreateStockProposalInput"]: {
/** Кооперативный участок, со склада которого идёт докладка. */
/** Кооперативный участок, со склада которого идёт выдача. */
braname: string,
/** Строки докладки с order_hash и подписью оператора (signiss1). */
items: Array<GraphQLTypes["MarketplaceCreateStockProposalLineInput"]>,
/** Пайщик-адресат докладки. */
member_account: string
/** Строки докладки со склада: order_hash и подпись оператора (signiss1). */
items?: Array<GraphQLTypes["MarketplaceCreateStockProposalLineInput"]> | undefined | null,
/** Пайщик-адресат бандла. */
member_account: string,
/** Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1). */
order_items?: Array<GraphQLTypes["MarketplaceCreateOrderProposalLineInput"]> | undefined | null
};
["MarketplaceCreateStockProposalLineInput"]: {
/** Предложение кооператива из остатка. */
@@ -41313,8 +41361,8 @@ export type GraphQLTypes = {
['...on MarketplaceStockAcceptPayload']: Omit<GraphQLTypes["MarketplaceStockAcceptPayload"], "...on MarketplaceStockAcceptPayload">
};
["MarketplaceStockFinalizeLineInput"]: {
/** Идентификатор предложения позиции (из payloads). */
offer_id: string,
/** order_hash строки бандла (из payloads). */
order_hash: string,
/** Акт приёма-передачи, контрподписанный пайщиком (подпись получения). */
signed_signiss2_act: GraphQLTypes["MarketplaceIssueActSignedDocumentInput"]
};
@@ -42500,7 +42548,7 @@ export type GraphQLTypes = {
marketplaceCreateReturnClaim: GraphQLTypes["MarketplaceReturnClaimResult"],
/** Сформировать партии поставки из акцептованной заявки. Каждая группа = одна партия (КУ + вариант доставки + опционально подмножество заказов). Покрытие всех КУ не обязательно — допустима частичная отгрузка и догрузка остатка отдельными партиями. */
marketplaceCreateShipment: GraphQLTypes["MarketplaceCreateShipmentResult"],
/** Оператор у стойки формирует докладку пайщику со склада кооператива (с уже подписанными им актами передачи) — пайщику немедленно приходит акт на подпись получения. */
/** Оператор у стойки формирует бандл выдачи пайщику (существующие заказы и/или докладка со склада), с уже подписанными им актами передачи — пайщику немедленно приходит акт на подпись получения. До его подписи ничего в блокчейне не происходит. */
marketplaceCreateStockProposal: GraphQLTypes["MarketplaceStockProposal"],
/** Создаёт ручной черновик проекта списания. На кооператив может быть только один открытый черновик и один проект, отправленный в совет. */
marketplaceCreateWriteoffDraft: GraphQLTypes["MarketplaceWriteoffProposal"],
@@ -46879,6 +46927,7 @@ type ZEUS_VARIABLES = {
["MarketplaceCreateAplReceptionInput"]: ValueTypes["MarketplaceCreateAplReceptionInput"];
["MarketplaceCreateExpressReceptionInput"]: ValueTypes["MarketplaceCreateExpressReceptionInput"];
["MarketplaceCreateOfferInput"]: ValueTypes["MarketplaceCreateOfferInput"];
["MarketplaceCreateOrderProposalLineInput"]: ValueTypes["MarketplaceCreateOrderProposalLineInput"];
["MarketplaceCreateReturnClaimInput"]: ValueTypes["MarketplaceCreateReturnClaimInput"];
["MarketplaceCreateShipmentInput"]: ValueTypes["MarketplaceCreateShipmentInput"];
["MarketplaceCreateStockProposalInput"]: ValueTypes["MarketplaceCreateStockProposalInput"];
@@ -15,12 +15,15 @@ import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
import {
getChairmanSignablePayload,
openIssuance,
getStockIssuancePayloads,
createStockProposal,
type CreateStockProposalInput,
type MarketplaceOrderIssuanceView,
} from '../api';
// Подписанный signiss1-акт в строке бандла — тип берём прямо из SDK-входа.
type StockBundleStockLine = NonNullable<CreateStockProposalInput['items']>[number];
type StockBundleOrderLine = NonNullable<CreateStockProposalInput['order_items']>[number];
import StockPickDialog, { type StockPickLine } from './StockPickDialog.vue';
/**
@@ -34,14 +37,19 @@ import StockPickDialog, { type StockPickLine } from './StockPickDialog.vue';
* позиции и сохраняется на заказе. Финальная подпись заказчика факт не
* редактирует.
*
* ЕДИНЫЙ ПУТЬ ВЫДАЧИ: оператор подписывает АПП-выдачи первой подписью (signiss1)
* по выдаваемым заказам И докладке со склада и складывает всё в ОДИН бандл
* (оффчейн, через БД). На цепи до подписи пайщика НИЧЕГО не происходит, поэтому
* отмена пайщиком = отказ от бандла без он-чейн отката, а выдача (signiss1+
* signiss2) уходит на цепь только при контрподписи получения пайщиком у стойки.
*
* Поток:
* 1. Оператор корректирует количество/цену в сводной таблице сверки (строка на
* позицию, предзаполнена заказом).
* 2. «Открыть выдачу» — UI ЦИКЛОМ по всем позициям: формирует акт на
* фактическое количество, подписывает ключом председателя сессии
* (signatureId=1) и отправляет в `openIssuance`. Каждая позиция = свой акт
* (результаты могут различаться). Заказы переходят в «Готово к получению»,
* заказчику — уведомление; финальную подпись он поставит сам.
* позицию, предзаполнена заказом) и при желании добавляет докладку со склада.
* 2. «Подписать и отправить пайщику» — UI по каждой позиции формирует акт на
* фактическое количество, подписывает ключом оператора (signatureId=1) и
* отправляет единым `createStockProposal`. Пайщику немедленно приходит один
* акт на подпись получения (карточка в гейте «подпись на месте»).
*/
const props = defineProps<{
@@ -322,52 +330,51 @@ async function confirm(): Promise<void> {
FailAlert(new Error('Приватный ключ не найден. Войдите в кооператив.'));
return;
}
if (!recipientAccount.value || !issueBraname.value) {
FailAlert(new Error('Не определён получатель или пункт выдачи.'));
return;
}
signing.value = true;
const docSigner = new Classes.Document(wifKey);
const failed: string[] = [];
let ok = 0;
// По выдаваемым позициям ПАРАЛЛЕЛЬНО: каждая — отдельный акт (председатель →
// openIssuance, signiss1). Невыбранные позиции в выдачу не идут — остаются на
// складе в текущем статусе (частичная выдача). У заказов разные хэши — на цепи
// это разные документы, гонок подписи нет, и все позиции взводятся в
// READY_TO_RECEIVE почти в один блок.
await Promise.all(
includedOrders.value.map(async (o) => {
try {
// ЕДИНЫЙ ПУТЬ: оператор подписывает АПП-выдачи первой подписью (signiss1) по
// выдаваемым позициям И докладке, и кладёт всё в ОДИН бандл (оффчейн, в БД).
// На цепи до подписи пайщика НИЧЕГО не происходит — поэтому отмена пайщиком
// = отказ от бандла, без он-чейн отката. Выдача (signiss1+signiss2) уходит
// в цепь только когда пайщик контрподписывает акт у стойки.
// 1) Обычные заказы пайщика → строки бандла order_items.
const order_items: StockBundleOrderLine[] = [];
for (const o of includedOrders.value) {
const f = facts.value[o.id];
const priceStr = String(f.price);
try {
const generated = await getChairmanSignablePayload({
order_id: o.id,
actual_quantity: f.qty,
actual_unit_price: priceStr,
});
const signed = await docSigner.signDocument(generated, globalStore.username, 1);
await openIssuance({
order_id: o.id,
actual_quantity: f.qty,
actual_unit_price: priceStr,
signed_document: signed,
});
ok += 1;
} catch (e) {
console.error('openIssuance failed for order', o.id, e);
failed.push(o.product_name || o.id.slice(0, 8));
}
}),
);
const generated = await getChairmanSignablePayload({
order_id: o.id,
actual_quantity: f.qty,
actual_unit_price: priceStr,
});
const signiss1_act = (await docSigner.signDocument(
generated,
globalStore.username,
1,
)) as StockBundleOrderLine['signiss1_act'];
order_items.push({
order_id: o.id,
actual_quantity: f.qty,
actual_unit_price: priceStr,
signiss1_act,
});
}
// Докладка со склада: оператор формирует бандл и СРАЗУ подписывает акты
// приёма-передачи первой подписью (signiss1) — пайщику докладка уйдёт в гейт
// готовым актом с одной кнопкой «Подписать» (получение), без отдельного
// «Принять». Заказ из остатка и сама выдача рождаются на подписи пайщика.
if (restockLines.value.length && recipientAccount.value && issueBraname.value) {
try {
// 2) Докладка со склада → строки бандла items (заказ родится на подписи пайщика).
let items: StockBundleStockLine[] = [];
if (restockLines.value.length) {
const payloads = await getStockIssuancePayloads({
braname: issueBraname.value,
member_account: recipientAccount.value,
items: restockLines.value.map((l) => ({ offer_id: l.offer_id, quantity: l.quantity })),
});
const items = await Promise.all(
items = await Promise.all(
payloads.map(async (p) => ({
offer_id: p.offer_id,
quantity: p.quantity,
@@ -376,37 +383,29 @@ async function confirm(): Promise<void> {
p.signiss1_document,
globalStore.username,
1,
)) as CreateStockProposalInput['items'][number]['signiss1_act'],
)) as StockBundleStockLine['signiss1_act'],
})),
);
await createStockProposal({
braname: issueBraname.value,
member_account: recipientAccount.value,
items,
});
restockLines.value = [];
} catch (e) {
FailAlert(e, 'Не удалось доложить со склада');
}
}
signing.value = false;
// 3) Один бандл — пайщику уйдёт один акт на подпись (получение).
await createStockProposal({
braname: issueBraname.value,
member_account: recipientAccount.value,
items,
order_items,
});
restockLines.value = [];
if (ok > 0) emit('opened');
if (failed.length === 0) {
const tail = leftCount.value > 0
? ` Осталось на складе позиц.: ${leftCount.value}.`
: '';
SuccessAlert(
`Выдача открыта по ${ok} позиц. — ждём подтверждение заказчика.${tail}`,
);
signing.value = false;
emit('opened');
const total = order_items.length + items.length;
const tail = leftCount.value > 0 ? ` Осталось на складе позиц.: ${leftCount.value}.` : '';
SuccessAlert(`Акт отправлен пайщику на подпись (${total} позиц.) — ждём подтверждение получения.${tail}`);
emit('update:modelValue', false);
} else {
FailAlert(
new Error(
`Открыто ${ok} из ${includedCount.value}. Не удалось: ${failed.join(', ')}. Повторите по оставшимся.`,
),
);
} catch (e) {
signing.value = false;
FailAlert(e, 'Не удалось отправить акт пайщику на подпись');
}
}
@@ -506,7 +505,7 @@ TakeoverDialog(
)
template(#icon-left)
q-icon(name="draw" size="16px")
| Подписать и открыть выдачу
| Подписать и отправить пайщику
</template>
<style scoped lang="scss">
@@ -1,3 +1,3 @@
export * from './ui';
export { useOnsiteSignatureGate, type OrdererPickupTask } from './model/useOnsiteSignatureGate';
export { useOnsiteSignatureGate } from './model/useOnsiteSignatureGate';
export { createMarketplaceEventsSubscription } from './model/marketplaceEventsSubscription';
@@ -9,13 +9,10 @@ import {
} from 'src/pages/Marketplace/OffererPendingAplReceptions/api';
import { Classes } from '@coopenomics/sdk';
import {
listMyReadyToReceive,
finalizeOrdererIssuance,
listStockProposals,
finalizeStockIssuance,
declineStockProposal,
getStockProposalSignablePayloads,
type MarketplaceOrderIssuanceView,
type MarketplaceStockProposalView,
type IStockConvertSigned,
type IStockFinalizeOrderLine,
@@ -48,16 +45,6 @@ import {
* откатил — после ближайшей дочитки список пустеет. Поллинга нет.
*/
interface OrdererPickupTask {
/** Ключ группировки — braname пункта выдачи. */
key: string;
pointName: string;
pointAddress: string;
orders: MarketplaceOrderIssuanceView[];
/** Совокупная сумма к получению по всем позициям пункта. */
totalCost: string;
}
const PENDING_SUPPLIER_SIGN = 'PENDING_SUPPLIER_SIGN';
// Очная приёмка кодируется как 'A' / 'IN_PERSON' в зависимости от слоя — гейт
// блокирует ТОЛЬКО её; экспедиторскую (B / EXPEDITOR) не трогает.
@@ -65,9 +52,9 @@ const IN_PERSON_VARIANTS = new Set(['A', 'IN_PERSON']);
// Singleton-состояние (как у SelectBranchOverlay): один гейт на всё приложение.
const supplierReceptions = ref<MarketplaceAplReceptionView[]>([]);
const ordererOrders = ref<MarketplaceOrderIssuanceView[]>([]);
// Входящие предложения имущества со склада кооператива (докладка): оператор
// накинул у стойки — пайщик решает прямо в гейте (принять / отказаться).
// Единый бандл выдачи: оператор у стойки подписал акты (signiss1) по заказам
// и/или докладке и отправил пайщику — пайщик решает прямо в гейте (подписать
// получение / отменить). До его подписи на цепи ничего нет.
const stockProposals = ref<MarketplaceStockProposalView[]>([]);
// Разложение стоимости докладки по предложению: сколько спишется с уже внесённых
// членских и сколько уйдёт в конвертацию с паевого (по Заявлению). Предзагружается,
@@ -85,40 +72,11 @@ const supplierTasks = computed<ReceptionGroup<MarketplaceAplReceptionView>[]>(()
),
);
const ordererTasks = computed<OrdererPickupTask[]>(() => {
// listMyReadyToReceive отдаёт заказы со статусом READY_TO_RECEIVE; фильтруем
// ещё не подписанные заказчиком и группируем по пункту выдачи — одна подпись
// на пункт (как в карточке «Моих заказов», задача #53).
const pending = ordererOrders.value.filter((o) => !o.orderer_signed_at);
const byPoint = new Map<string, MarketplaceOrderIssuanceView[]>();
for (const o of pending) {
const key = o.delivery_braname || o.delivery_point_name || o.id;
const arr = byPoint.get(key) ?? [];
arr.push(o);
byPoint.set(key, arr);
}
return [...byPoint.entries()].map(([key, orders]) => ({
key,
pointName: orders[0]?.delivery_point_name ?? '',
pointAddress: orders[0]?.delivery_point_address ?? '',
orders,
totalCost: orders
.reduce(
(sum, o) => sum + Number.parseFloat(o.issuance_fact?.fact_cost ?? o.total_cost ?? '0'),
0,
)
.toFixed(4),
}));
});
const proposalTasks = computed(() => stockProposals.value);
/** Гейт виден, пока есть хоть одна личная подпись/решение, которых ждут от пайщика. */
const isVisible = computed(
() =>
supplierTasks.value.length > 0 ||
ordererTasks.value.length > 0 ||
proposalTasks.value.length > 0,
() => supplierTasks.value.length > 0 || proposalTasks.value.length > 0,
);
/**
@@ -130,22 +88,19 @@ async function refresh(source = 'ручной'): Promise<void> {
if (!global.wif) {
// Не залогинен — гейта нет, очищаем возможный хвост.
supplierReceptions.value = [];
ordererOrders.value = [];
stockProposals.value = [];
return;
}
const wasVisible = isVisible.value;
loading.value = true;
try {
const [receptions, orders, proposals] = await Promise.all([
const [receptions, proposals] = await Promise.all([
listAplReceptionsAsSupplier().catch(() => [] as MarketplaceAplReceptionView[]),
listMyReadyToReceive().catch(() => [] as MarketplaceOrderIssuanceView[]),
listStockProposals({ statuses: ['PROPOSED'] }).catch(
() => [] as MarketplaceStockProposalView[],
),
]);
supplierReceptions.value = receptions;
ordererOrders.value = orders;
stockProposals.value = proposals;
// Подтягиваем разложение сумм (с членского / с паевого) для показа в карточке —
// не блокирует дочитку; кэш по id, ушедшие предложения чистятся.
@@ -158,7 +113,7 @@ async function refresh(source = 'ручной'): Promise<void> {
// сокет; если перед этим был «POLL» — сработала страховочная дочитка.
const appeared = !wasVisible && isVisible.value;
console.info(
`%c[OnsiteGate] refresh ← ${source}: поставщик=${supplierTasks.value.length}, заказчик=${ordererTasks.value.length}, предложений=${proposalTasks.value.length}, гейт виден=${isVisible.value}${appeared ? ' (ВСПЛЫЛ только что)' : ''}`,
`%c[OnsiteGate] refresh ← ${source}: поставщик=${supplierTasks.value.length}, актов на подпись=${proposalTasks.value.length}, гейт виден=${isVisible.value}${appeared ? ' (ВСПЛЫЛ только что)' : ''}`,
appeared ? 'color:#16a34a;font-weight:bold' : 'color:#64748b',
);
}
@@ -214,34 +169,8 @@ async function signSupplier(group: ReceptionGroup<MarketplaceAplReceptionView>):
}
}
async function signOrderer(task: OrdererPickupTask): Promise<void> {
const global = useGlobalStore();
const wif = global.wif?.toString();
if (!wif) {
FailAlert(new Error('Приватный ключ не найден. Войдите в кооператив.'));
return;
}
signingKey.value = task.key;
try {
const { ok, failed } = await finalizeOrdererIssuance(task.orders, wif, global.username);
if (failed.length === 0) {
SuccessAlert(`Имущество получено по ${ok} позиц. Заказы закрыты.`);
} else {
const names = failed.map((f) => f.order.product_name || f.order.id.slice(0, 8));
FailAlert(
new Error(
`Получено ${ok} из ${task.orders.length}. Не удалось: ${names.join(', ')}. Повторите по оставшимся.`,
),
);
}
await refresh();
} finally {
signingKey.value = null;
}
}
/**
* Пайщик ОДНОЙ подписью утверждает докладку как акт получения. Подписывает:
* Пайщик ОДНОЙ подписью утверждает бандл как акт получения. Подписывает:
* при дефиците членских — единое Заявление о конвертации (paевой → членский),
* затем по каждой строке контрподписывает АПП-выдачи (signiss2) поверх подписи
* оператора. Backend создаёт заказы из остатка и проводит выдачу — имущество
@@ -275,7 +204,7 @@ async function signProposal(task: MarketplaceStockProposalView): Promise<void> {
// document с подписью оператора (канон 2-подписи).
const order_lines: IStockFinalizeOrderLine[] = await Promise.all(
payload.order_lines.map(async (line) => ({
offer_id: line.offer_id,
order_hash: line.order_hash,
signed_signiss2_act: (await signer.signDocument(
line.signiss1_aggregate.rawDocument,
global.username,
@@ -286,7 +215,7 @@ async function signProposal(task: MarketplaceStockProposalView): Promise<void> {
);
const { order_ids } = await finalizeStockIssuance(task.id, order_lines, signed_convert);
SuccessAlert(`Имущество получено по ${order_ids.length} позиц. со склада. Акт подписан.`);
SuccessAlert(`Имущество получено по ${order_ids.length} позиц. Акт подписан.`);
} catch (error) {
FailAlert(error);
} finally {
@@ -299,7 +228,7 @@ async function declineProposal(task: MarketplaceStockProposalView): Promise<void
signingKey.value = task.id;
try {
await declineStockProposal(task.id);
SuccessAlert(редложение отклонено.');
SuccessAlert(олучение отменено. Оператор сформирует акт заново.');
} catch (error) {
FailAlert(error);
} finally {
@@ -314,16 +243,13 @@ export function useOnsiteSignatureGate() {
loading,
signingKey,
supplierTasks,
ordererTasks,
proposalTasks,
proposalSums,
refresh,
signSupplier,
signOrderer,
signProposal,
declineProposal,
};
}
export type { OrdererPickupTask };
export type { MarketplaceStockProposalView };
@@ -7,7 +7,7 @@ import { type ReceptionGroup } from 'src/shared/lib/marketplace';
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import type { MarketplaceAplReceptionView } from 'src/pages/Marketplace/OffererPendingAplReceptions/api';
import { useOnsiteSignatureGate, type OrdererPickupTask } from '../model/useOnsiteSignatureGate';
import { useOnsiteSignatureGate } from '../model/useOnsiteSignatureGate';
/**
* Глобальный оверлей подписи на месте — монтируется один раз в App.vue рядом с
@@ -24,12 +24,10 @@ const {
isVisible,
signingKey,
supplierTasks,
ordererTasks,
proposalTasks,
proposalSums,
refresh,
signSupplier,
signOrderer,
signProposal,
declineProposal,
} = useOnsiteSignatureGate();
@@ -57,21 +55,11 @@ function kuAddr(braname: string): string {
return kuStore.details.find((d) => d.coreBraname === braname)?.addressFull ?? '';
}
function ordererPointLabel(task: OrdererPickupTask): string {
return [task.pointName, task.pointAddress].filter(Boolean).join(' · ') || task.key;
}
function factQty(o: OrdererPickupTask['orders'][number]): number {
return o.issuance_fact?.actual_quantity ?? o.quantity ?? 0;
}
function factCost(o: OrdererPickupTask['orders'][number]): string {
return o.issuance_fact?.fact_cost ?? o.total_cost ?? '0';
}
function proposalLineCost(i: { quantity: number; unit_price: string }): string {
return (i.quantity * Number.parseFloat(i.unit_price)).toFixed(4);
}
const supplierBusy = (g: ReceptionGroup<MarketplaceAplReceptionView>) => signingKey.value === g.key;
const ordererBusy = (t: OrdererPickupTask) => signingKey.value === t.key;
const proposalBusy = (id: string) => signingKey.value === id;
const anySigning = computed(() => signingKey.value !== null);
@@ -140,54 +128,15 @@ BaseDialog(
q-icon(name='draw', size='18px')
| Подписать поставку
//- Заказчик: финальная (закрывающая) подпись получения, сводно по пункту.
BaseCard.onsite-gate__card(v-for='t in ordererTasks', :key='t.key')
template(#head)
.onsite-gate__head
q-icon(name='inventory_2', size='28px')
.onsite-gate__ident
span.onsite-gate__name Получение в пункте выдачи
span.onsite-gate__addr
q-icon(name='place', size='14px')
| {{ ordererPointLabel(t) }}
span.onsite-gate__sub Подтвердите получение имущества финальная подпись заказчика
table.onsite-gate__table
thead
tr
th Позиция
th.num К получению
th.num Сумма
tbody
tr(v-for='o in t.orders', :key='o.id')
td {{ o.product_name || 'Товар по предложению' }}
td.num {{ factQty(o) }} {{ marketplaceUnitShort(o.unit_of_measure) }}
td.num {{ formatAsset2Digits(factCost(o)) }}
tfoot
tr
td Итого к получению
td.num
td.num {{ formatAsset2Digits(t.totalCost) }}
.onsite-gate__foot
BaseButton(
variant='primary',
:loading='ordererBusy(t)',
:disabled='anySigning && !ordererBusy(t)',
@click='signOrderer(t)'
)
template(#icon-left)
q-icon(name='draw', size='18px')
| Подписать и получить
//- Докладка со склада кооператива: оператор уже подписал акт передачи —
//- пайщику остаётся одна подпись получения (принятие = подпись акта).
//- Бандл выдачи: оператор уже подписал акт передачи (по заказам и/или
//- докладке со склада) — пайщику остаётся одна подпись получения; до неё на
//- цепи ничего нет, поэтому «Отменить» = отказ от бандла (оператор повторит).
BaseCard.onsite-gate__card(v-for='p in proposalTasks', :key='p.id')
template(#head)
.onsite-gate__head
q-icon(name='inventory_2', size='28px')
.onsite-gate__ident
span.onsite-gate__name Получение со склада кооператива
span.onsite-gate__name Получение в пункте выдачи
span.onsite-gate__sub Подтвердите получение имущества ваша подпись акта
table.onsite-gate__table
@@ -220,7 +169,7 @@ BaseDialog(
variant='ghost',
:disabled='anySigning',
@click='declineProposal(p)'
) Отказаться
) Отменить
BaseButton(
variant='primary',
:loading='proposalBusy(p.id)',
@@ -229,7 +178,7 @@ BaseDialog(
)
template(#icon-left)
q-icon(name='draw', size='18px')
| Подписать
| Подписать и получить
</template>
<style scoped lang="scss">
+5 -1
View File
@@ -736,6 +736,9 @@ export const AllTypesProps: Record<string,any> = {
delivery_points:"MarketplaceOfferDeliveryPointInput",
images:"MarketplaceOfferImageUploadInput"
},
MarketplaceCreateOrderProposalLineInput:{
signiss1_act:"MarketplaceIssueActSignedDocumentInput"
},
MarketplaceCreateReturnClaimInput:{
defect_category:"MarketplaceReturnClaimDefectCategory",
photos:"MarketplaceReturnClaimPhotoUploadInput",
@@ -745,7 +748,8 @@ export const AllTypesProps: Record<string,any> = {
groups:"MarketplaceShipmentGroupInput"
},
MarketplaceCreateStockProposalInput:{
items:"MarketplaceCreateStockProposalLineInput"
items:"MarketplaceCreateStockProposalLineInput",
order_items:"MarketplaceCreateOrderProposalLineInput"
},
MarketplaceCreateStockProposalLineInput:{
signiss1_act:"MarketplaceIssueActSignedDocumentInput"
+79 -30
View File
@@ -6833,6 +6833,16 @@ export type ValueTypes = {
unit_of_measure: string | Variable<any, string>,
unlimited_flag: boolean | Variable<any, string>,
warranty_days: number | Variable<any, string>
};
["MarketplaceCreateOrderProposalLineInput"]: {
/** Фактическое количество к выдаче (сверено оператором). */
actual_quantity: number | Variable<any, string>,
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string | Variable<any, string>,
/** Существующий заказ пайщика к выдаче этим бандлом. */
order_id: ValueTypes["ID"] | Variable<any, string>,
/** Акт приёма-передачи, подписанный оператором КУ первой подписью. */
signiss1_act: ValueTypes["MarketplaceIssueActSignedDocumentInput"] | Variable<any, string>
};
["MarketplaceCreateReturnClaimInput"]: {
/** Возвращаемое количество единиц (по умолчанию — выданное количество). */
@@ -6861,12 +6871,14 @@ export type ValueTypes = {
['...on MarketplaceCreateShipmentResult']?: Omit<ValueTypes["MarketplaceCreateShipmentResult"], "...on MarketplaceCreateShipmentResult">
}>;
["MarketplaceCreateStockProposalInput"]: {
/** Кооперативный участок, со склада которого идёт докладка. */
/** Кооперативный участок, со склада которого идёт выдача. */
braname: string | Variable<any, string>,
/** Строки докладки с order_hash и подписью оператора (signiss1). */
items: Array<ValueTypes["MarketplaceCreateStockProposalLineInput"]> | Variable<any, string>,
/** Пайщик-адресат докладки. */
member_account: string | Variable<any, string>
/** Строки докладки со склада: order_hash и подпись оператора (signiss1). */
items?: Array<ValueTypes["MarketplaceCreateStockProposalLineInput"]> | undefined | null | Variable<any, string>,
/** Пайщик-адресат бандла. */
member_account: string | Variable<any, string>,
/** Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1). */
order_items?: Array<ValueTypes["MarketplaceCreateOrderProposalLineInput"]> | undefined | null | Variable<any, string>
};
["MarketplaceCreateStockProposalLineInput"]: {
/** Предложение кооператива из остатка. */
@@ -8316,8 +8328,8 @@ export type ValueTypes = {
['...on MarketplaceStockAcceptPayload']?: Omit<ValueTypes["MarketplaceStockAcceptPayload"], "...on MarketplaceStockAcceptPayload">
}>;
["MarketplaceStockFinalizeLineInput"]: {
/** Идентификатор предложения позиции (из payloads). */
offer_id: string | Variable<any, string>,
/** order_hash строки бандла (из payloads). */
order_hash: string | Variable<any, string>,
/** Акт приёма-передачи, контрподписанный пайщиком (подпись получения). */
signed_signiss2_act: ValueTypes["MarketplaceIssueActSignedDocumentInput"] | Variable<any, string>
};
@@ -17766,6 +17778,16 @@ export type ResolverInputTypes = {
unit_of_measure: string,
unlimited_flag: boolean,
warranty_days: number
};
["MarketplaceCreateOrderProposalLineInput"]: {
/** Фактическое количество к выдаче (сверено оператором). */
actual_quantity: number,
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string,
/** Существующий заказ пайщика к выдаче этим бандлом. */
order_id: ResolverInputTypes["ID"],
/** Акт приёма-передачи, подписанный оператором КУ первой подписью. */
signiss1_act: ResolverInputTypes["MarketplaceIssueActSignedDocumentInput"]
};
["MarketplaceCreateReturnClaimInput"]: {
/** Возвращаемое количество единиц (по умолчанию — выданное количество). */
@@ -17793,12 +17815,14 @@ export type ResolverInputTypes = {
__typename?: boolean | `@${string}`
}>;
["MarketplaceCreateStockProposalInput"]: {
/** Кооперативный участок, со склада которого идёт докладка. */
/** Кооперативный участок, со склада которого идёт выдача. */
braname: string,
/** Строки докладки с order_hash и подписью оператора (signiss1). */
items: Array<ResolverInputTypes["MarketplaceCreateStockProposalLineInput"]>,
/** Пайщик-адресат докладки. */
member_account: string
/** Строки докладки со склада: order_hash и подпись оператора (signiss1). */
items?: Array<ResolverInputTypes["MarketplaceCreateStockProposalLineInput"]> | undefined | null,
/** Пайщик-адресат бандла. */
member_account: string,
/** Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1). */
order_items?: Array<ResolverInputTypes["MarketplaceCreateOrderProposalLineInput"]> | undefined | null
};
["MarketplaceCreateStockProposalLineInput"]: {
/** Предложение кооператива из остатка. */
@@ -19202,8 +19226,8 @@ export type ResolverInputTypes = {
__typename?: boolean | `@${string}`
}>;
["MarketplaceStockFinalizeLineInput"]: {
/** Идентификатор предложения позиции (из payloads). */
offer_id: string,
/** order_hash строки бандла (из payloads). */
order_hash: string,
/** Акт приёма-передачи, контрподписанный пайщиком (подпись получения). */
signed_signiss2_act: ResolverInputTypes["MarketplaceIssueActSignedDocumentInput"]
};
@@ -28340,6 +28364,16 @@ export type ModelTypes = {
unit_of_measure: string,
unlimited_flag: boolean,
warranty_days: number
};
["MarketplaceCreateOrderProposalLineInput"]: {
/** Фактическое количество к выдаче (сверено оператором). */
actual_quantity: number,
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string,
/** Существующий заказ пайщика к выдаче этим бандлом. */
order_id: ModelTypes["ID"],
/** Акт приёма-передачи, подписанный оператором КУ первой подписью. */
signiss1_act: ModelTypes["MarketplaceIssueActSignedDocumentInput"]
};
["MarketplaceCreateReturnClaimInput"]: {
/** Возвращаемое количество единиц (по умолчанию — выданное количество). */
@@ -28366,12 +28400,14 @@ export type ModelTypes = {
shipments: Array<ModelTypes["MarketplaceShipment"]>
};
["MarketplaceCreateStockProposalInput"]: {
/** Кооперативный участок, со склада которого идёт докладка. */
/** Кооперативный участок, со склада которого идёт выдача. */
braname: string,
/** Строки докладки с order_hash и подписью оператора (signiss1). */
items: Array<ModelTypes["MarketplaceCreateStockProposalLineInput"]>,
/** Пайщик-адресат докладки. */
member_account: string
/** Строки докладки со склада: order_hash и подпись оператора (signiss1). */
items?: Array<ModelTypes["MarketplaceCreateStockProposalLineInput"]> | undefined | null,
/** Пайщик-адресат бандла. */
member_account: string,
/** Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1). */
order_items?: Array<ModelTypes["MarketplaceCreateOrderProposalLineInput"]> | undefined | null
};
["MarketplaceCreateStockProposalLineInput"]: {
/** Предложение кооператива из остатка. */
@@ -29701,8 +29737,8 @@ export type ModelTypes = {
order_lines: Array<ModelTypes["MarketplaceStockAcceptOrderLine"]>
};
["MarketplaceStockFinalizeLineInput"]: {
/** Идентификатор предложения позиции (из payloads). */
offer_id: string,
/** order_hash строки бандла (из payloads). */
order_hash: string,
/** Акт приёма-передачи, контрподписанный пайщиком (подпись получения). */
signed_signiss2_act: ModelTypes["MarketplaceIssueActSignedDocumentInput"]
};
@@ -30828,7 +30864,7 @@ export type ModelTypes = {
marketplaceCreateReturnClaim: ModelTypes["MarketplaceReturnClaimResult"],
/** Сформировать партии поставки из акцептованной заявки. Каждая группа = одна партия (КУ + вариант доставки + опционально подмножество заказов). Покрытие всех КУ не обязательно — допустима частичная отгрузка и догрузка остатка отдельными партиями. */
marketplaceCreateShipment: ModelTypes["MarketplaceCreateShipmentResult"],
/** Оператор у стойки формирует докладку пайщику со склада кооператива (с уже подписанными им актами передачи) — пайщику немедленно приходит акт на подпись получения. */
/** Оператор у стойки формирует бандл выдачи пайщику (существующие заказы и/или докладка со склада), с уже подписанными им актами передачи — пайщику немедленно приходит акт на подпись получения. До его подписи ничего в блокчейне не происходит. */
marketplaceCreateStockProposal: ModelTypes["MarketplaceStockProposal"],
/** Создаёт ручной черновик проекта списания. На кооператив может быть только один открытый черновик и один проект, отправленный в совет. */
marketplaceCreateWriteoffDraft: ModelTypes["MarketplaceWriteoffProposal"],
@@ -39829,6 +39865,16 @@ export type GraphQLTypes = {
unit_of_measure: string,
unlimited_flag: boolean,
warranty_days: number
};
["MarketplaceCreateOrderProposalLineInput"]: {
/** Фактическое количество к выдаче (сверено оператором). */
actual_quantity: number,
/** Фактическая цена за единицу (оператор мог скорректировать). */
actual_unit_price: string,
/** Существующий заказ пайщика к выдаче этим бандлом. */
order_id: GraphQLTypes["ID"],
/** Акт приёма-передачи, подписанный оператором КУ первой подписью. */
signiss1_act: GraphQLTypes["MarketplaceIssueActSignedDocumentInput"]
};
["MarketplaceCreateReturnClaimInput"]: {
/** Возвращаемое количество единиц (по умолчанию — выданное количество). */
@@ -39857,12 +39903,14 @@ export type GraphQLTypes = {
['...on MarketplaceCreateShipmentResult']: Omit<GraphQLTypes["MarketplaceCreateShipmentResult"], "...on MarketplaceCreateShipmentResult">
};
["MarketplaceCreateStockProposalInput"]: {
/** Кооперативный участок, со склада которого идёт докладка. */
/** Кооперативный участок, со склада которого идёт выдача. */
braname: string,
/** Строки докладки с order_hash и подписью оператора (signiss1). */
items: Array<GraphQLTypes["MarketplaceCreateStockProposalLineInput"]>,
/** Пайщик-адресат докладки. */
member_account: string
/** Строки докладки со склада: order_hash и подпись оператора (signiss1). */
items?: Array<GraphQLTypes["MarketplaceCreateStockProposalLineInput"]> | undefined | null,
/** Пайщик-адресат бандла. */
member_account: string,
/** Строки уже существующих заказов пайщика к выдаче (подпись оператора signiss1). */
order_items?: Array<GraphQLTypes["MarketplaceCreateOrderProposalLineInput"]> | undefined | null
};
["MarketplaceCreateStockProposalLineInput"]: {
/** Предложение кооператива из остатка. */
@@ -41313,8 +41361,8 @@ export type GraphQLTypes = {
['...on MarketplaceStockAcceptPayload']: Omit<GraphQLTypes["MarketplaceStockAcceptPayload"], "...on MarketplaceStockAcceptPayload">
};
["MarketplaceStockFinalizeLineInput"]: {
/** Идентификатор предложения позиции (из payloads). */
offer_id: string,
/** order_hash строки бандла (из payloads). */
order_hash: string,
/** Акт приёма-передачи, контрподписанный пайщиком (подпись получения). */
signed_signiss2_act: GraphQLTypes["MarketplaceIssueActSignedDocumentInput"]
};
@@ -42500,7 +42548,7 @@ export type GraphQLTypes = {
marketplaceCreateReturnClaim: GraphQLTypes["MarketplaceReturnClaimResult"],
/** Сформировать партии поставки из акцептованной заявки. Каждая группа = одна партия (КУ + вариант доставки + опционально подмножество заказов). Покрытие всех КУ не обязательно — допустима частичная отгрузка и догрузка остатка отдельными партиями. */
marketplaceCreateShipment: GraphQLTypes["MarketplaceCreateShipmentResult"],
/** Оператор у стойки формирует докладку пайщику со склада кооператива (с уже подписанными им актами передачи) — пайщику немедленно приходит акт на подпись получения. */
/** Оператор у стойки формирует бандл выдачи пайщику (существующие заказы и/или докладка со склада), с уже подписанными им актами передачи — пайщику немедленно приходит акт на подпись получения. До его подписи ничего в блокчейне не происходит. */
marketplaceCreateStockProposal: GraphQLTypes["MarketplaceStockProposal"],
/** Создаёт ручной черновик проекта списания. На кооператив может быть только один открытый черновик и один проект, отправленный в совет. */
marketplaceCreateWriteoffDraft: GraphQLTypes["MarketplaceWriteoffProposal"],
@@ -46879,6 +46927,7 @@ type ZEUS_VARIABLES = {
["MarketplaceCreateAplReceptionInput"]: ValueTypes["MarketplaceCreateAplReceptionInput"];
["MarketplaceCreateExpressReceptionInput"]: ValueTypes["MarketplaceCreateExpressReceptionInput"];
["MarketplaceCreateOfferInput"]: ValueTypes["MarketplaceCreateOfferInput"];
["MarketplaceCreateOrderProposalLineInput"]: ValueTypes["MarketplaceCreateOrderProposalLineInput"];
["MarketplaceCreateReturnClaimInput"]: ValueTypes["MarketplaceCreateReturnClaimInput"];
["MarketplaceCreateShipmentInput"]: ValueTypes["MarketplaceCreateShipmentInput"];
["MarketplaceCreateStockProposalInput"]: ValueTypes["MarketplaceCreateStockProposalInput"];