[598-50][@ant] feat: докладка у пайщика — готовый акт и одна кнопка «Подписать», без «Принять»

Оператор в окне выдачи («Со склада») теперь получает АПП-выдачи по строкам
корзины и подписывает их первой подписью (signiss1) сразу при формировании
докладки (getStockIssuancePayloads + createStockProposal с подписанными актами).

Пайщику докладка приходит в гейт готовым актом (пункты имущества, кол-во, цена,
две суммы: с членского / конвертация с паевого). Одна кнопка «Подписать»
(+«Отказаться»): подписывает Заявление о конвертации при дефиците и
контрподписывает signiss2 по строкам — finalizeStockIssuance создаёт заказы и
выдаёт имущество. Кнопка «Принять предложение» убрана: принятие = подпись акта.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ant
2026-06-16 07:40:56 +00:00
parent 5b9d8fad67
commit 94c1ef77c9
4 changed files with 101 additions and 38 deletions
@@ -159,19 +159,37 @@ export async function cancelStockProposal(proposal_id: string): Promise<Marketpl
return result as MarketplaceStockProposalView;
}
/** Строка корзины докладки для подготовки актов оператору (offer_id + quantity). */
export type StockIssuancePayloadInput = Queries.Marketplace.StockIssuancePayloads.IInput['data'];
/** Строка к подписи оператором: order_hash + АПП-выдачи (signiss1_document). */
export type IStockIssuanceOperatorLine =
Queries.Marketplace.StockIssuancePayloads.IOutput['marketplaceStockIssuancePayloads'][number];
/**
* Нагрузка к принятию предложения со склада: строки-заказы + ОДНО Заявление о
* конвертации на весь дефицит (convert_document пустой — членских средств
* хватает, при замене из высвобожденных подписывать нечего).
* Нагрузка к ОДНОЙ подписи пайщика: по строке — order_hash и подписанный
* оператором АПП-выдачи (signiss1_aggregate, для контрподписи получения), плюс
* ОДНО Заявление о конвертации на весь дефицит (convert_document пустой —
* членских средств хватает, подписывать нужно только сами акты).
*/
export type IStockProposalAcceptPayload =
Queries.Marketplace.StockProposalSignablePayloads.IOutput['marketplaceStockProposalSignablePayloads'];
export type IStockAcceptInput = Mutations.Marketplace.AcceptStockProposal.IInput['data'];
/** Строка-заказ принятия (offer_id + order_hash). */
export type IStockAcceptOrderLine = IStockAcceptInput['order_lines'][number];
export type IStockFinalizeInput = Mutations.Marketplace.FinalizeStockIssuance.IInput['data'];
/** Строка финализации (offer_id + контрподписанный пайщиком signiss2-акт). */
export type IStockFinalizeOrderLine = IStockFinalizeInput['order_lines'][number];
/** Подписанное единое Заявление о конвертации (если был дефицит). */
export type IStockConvertSigned = NonNullable<IStockAcceptInput['signed_convert']>;
export type IStockConvertSigned = NonNullable<IStockFinalizeInput['signed_convert']>;
/** Акты приёма-передачи к подписи оператором при формировании докладки. */
export async function getStockIssuancePayloads(
data: StockIssuancePayloadInput,
): Promise<IStockIssuanceOperatorLine[]> {
const { [Queries.Marketplace.StockIssuancePayloads.name]: result } = await client.Query(
Queries.Marketplace.StockIssuancePayloads.query,
{ variables: { data } },
);
return result as IStockIssuanceOperatorLine[];
}
export async function getStockProposalSignablePayloads(
proposal_id: string,
@@ -184,14 +202,22 @@ export async function getStockProposalSignablePayloads(
return result as IStockProposalAcceptPayload;
}
export async function acceptStockProposal(
/**
* Пайщик одной подписью утверждает докладку: по строкам — контрподписанные
* signiss2-акты, при дефиците — единое подписанное Заявление о конвертации.
*/
export async function finalizeStockIssuance(
proposal_id: string,
order_lines: IStockAcceptOrderLine[],
order_lines: IStockFinalizeOrderLine[],
signed_convert?: IStockConvertSigned | null,
): Promise<{ proposal: MarketplaceStockProposalView; order_ids: string[] }> {
const data: IStockAcceptInput = { proposal_id, order_lines, signed_convert: signed_convert ?? null };
const { [Mutations.Marketplace.AcceptStockProposal.name]: result } = await client.Mutation(
Mutations.Marketplace.AcceptStockProposal.mutation,
const data: IStockFinalizeInput = {
proposal_id,
order_lines,
signed_convert: signed_convert ?? null,
};
const { [Mutations.Marketplace.FinalizeStockIssuance.name]: result } = await client.Mutation(
Mutations.Marketplace.FinalizeStockIssuance.mutation,
{ variables: { data } },
);
return result as { proposal: MarketplaceStockProposalView; order_ids: string[] };
@@ -16,7 +16,9 @@ import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
import {
getChairmanSignablePayload,
openIssuance,
getStockIssuancePayloads,
createStockProposal,
type CreateStockProposalInput,
type MarketplaceOrderIssuanceView,
} from '../api';
import StockPickDialog, { type StockPickLine } from './StockPickDialog.vue';
@@ -354,15 +356,34 @@ async function confirm(): Promise<void> {
}),
);
// Докладка со склада: создаём предложение тем же действием — пайщику оно
// уйдёт в гейт вместе с заказом (принятие и подпись получения — у пайщика).
// Докладка со склада: оператор формирует бандл и СРАЗУ подписывает акты
// приёма-передачи первой подписью (signiss1) — пайщику докладка уйдёт в гейт
// готовым актом с одной кнопкой «Подписать» (получение), без отдельного
// «Принять». Заказ из остатка и сама выдача рождаются на подписи пайщика.
if (restockLines.value.length && recipientAccount.value && issueBraname.value) {
try {
await createStockProposal({
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(
payloads.map(async (p) => ({
offer_id: p.offer_id,
quantity: p.quantity,
order_hash: p.order_hash,
signiss1_act: (await docSigner.signDocument(
p.signiss1_document,
globalStore.username,
1,
)) as CreateStockProposalInput['items'][number]['signiss1_act'],
})),
);
await createStockProposal({
braname: issueBraname.value,
member_account: recipientAccount.value,
items,
});
restockLines.value = [];
} catch (e) {
FailAlert(e, 'Не удалось доложить со склада');
@@ -12,12 +12,13 @@ import {
listMyReadyToReceive,
finalizeOrdererIssuance,
listStockProposals,
acceptStockProposal,
finalizeStockIssuance,
declineStockProposal,
getStockProposalSignablePayloads,
type MarketplaceOrderIssuanceView,
type MarketplaceStockProposalView,
type IStockConvertSigned,
type IStockFinalizeOrderLine,
} from 'src/pages/Marketplace/OperatorIssuance/api';
/**
@@ -240,12 +241,13 @@ async function signOrderer(task: OrdererPickupTask): Promise<void> {
}
/**
* Пайщик принимает предложение со склада: по строкам создаются заказы (средства
* резервируются на акцепте — при нехватке паевых средств backend вернёт
* человеческую ошибку), и следом оператор открывает выдачу — акт придёт в этот
* же гейт обычной задачей заказчика.
* Пайщик ОДНОЙ подписью утверждает докладку как акт получения. Подписывает:
* при дефиците членских — единое Заявление о конвертации (paевой → членский),
* затем по каждой строке контрподписывает АПП-выдачи (signiss2) поверх подписи
* оператора. Backend создаёт заказы из остатка и проводит выдачу — имущество
* выдаётся сразу. Никакого отдельного «Принять»: принятие = подпись акта.
*/
async function acceptProposal(task: MarketplaceStockProposalView): Promise<void> {
async function signProposal(task: MarketplaceStockProposalView): Promise<void> {
const global = useGlobalStore();
const wifKey = global.wif?.toString();
if (!wifKey) {
@@ -254,23 +256,37 @@ async function acceptProposal(task: MarketplaceStockProposalView): Promise<void>
}
signingKey.value = task.id;
try {
// Одно Заявление о конвертации на весь дефицит сверх членских средств; если
// средств хватает (замена из высвобожденных — «возьмите вот это»), документа
// нет и подписывать пайщику нечего.
const payload = await getStockProposalSignablePayloads(task.id);
const signer = new Classes.Document(wifKey);
// 1. Заявление о конвертации — только при дефиците членских средств (иначе
// convert_document пустой и подписывать нечего, кроме самих актов).
let signed_convert: IStockConvertSigned | null = null;
if (payload.convert_document) {
const signer = new Classes.Document(wifKey);
signed_convert = (await signer.signDocument(
payload.convert_document,
global.username,
1,
)) as IStockConvertSigned;
}
const { order_ids } = await acceptStockProposal(task.id, payload.order_lines, signed_convert);
SuccessAlert(
`Предложение принято: оформлено позиций со склада — ${order_ids.length}. Оператор откроет выдачу.`,
// 2. Контрподпись получения (signiss2) поверх подписи оператора по каждой
// строке — документ НЕ перегенерируется, берётся агрегат rawDocument +
// document с подписью оператора (канон 2-подписи).
const order_lines: IStockFinalizeOrderLine[] = await Promise.all(
payload.order_lines.map(async (line) => ({
offer_id: line.offer_id,
signed_signiss2_act: (await signer.signDocument(
line.signiss1_aggregate.rawDocument,
global.username,
2,
[line.signiss1_aggregate.document],
)) as IStockFinalizeOrderLine['signed_signiss2_act'],
})),
);
const { order_ids } = await finalizeStockIssuance(task.id, order_lines, signed_convert);
SuccessAlert(`Имущество получено по ${order_ids.length} позиц. со склада. Акт подписан.`);
} catch (error) {
FailAlert(error);
} finally {
@@ -304,7 +320,7 @@ export function useOnsiteSignatureGate() {
refresh,
signSupplier,
signOrderer,
acceptProposal,
signProposal,
declineProposal,
};
}
@@ -30,7 +30,7 @@ const {
refresh,
signSupplier,
signOrderer,
acceptProposal,
signProposal,
declineProposal,
} = useOnsiteSignatureGate();
@@ -180,15 +180,15 @@ BaseDialog(
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='add_shopping_cart', size='28px')
q-icon(name='inventory_2', size='28px')
.onsite-gate__ident
span.onsite-gate__name Предложение со склада кооператива
span.onsite-gate__sub Оператор пункта выдачи предлагает добавить к получению
span.onsite-gate__name Получение со склада кооператива
span.onsite-gate__sub Подтвердите получение имущества ваша подпись акта
table.onsite-gate__table
thead
@@ -225,11 +225,11 @@ BaseDialog(
variant='primary',
:loading='proposalBusy(p.id)',
:disabled='anySigning && !proposalBusy(p.id)',
@click='acceptProposal(p)'
@click='signProposal(p)'
)
template(#icon-left)
q-icon(name='check', size='18px')
| Принять предложение
q-icon(name='draw', size='18px')
| Подписать
</template>
<style scoped lang="scss">