fix(marketplace): выдача — финальная подпись в кабинете заказчика, факт фиксируется оператором при открытии (B1)
Кардинальный дефект: финальную подпись заказчика держал стол оператора (2-й скан штрих-кода + ввод приватного ключа ЗАКАЗЧИКА на устройстве оператора). Перенесено корректно, без изменения контракта: - signiss1 не берёт actual_quantity (только акт), signiss2 берёт → relocation чисто backend+frontend. - openIssuance принимает actual_quantity (правка оператора): зашивается в акт председателя и сохраняется снапшотом issuance_fact на открытии. - finalizeIssuance больше не принимает actual_quantity/delivery_signer — backend берёт их из заказа (issuance_fact, chairman_account); заказчик факт не редактирует. - Полный codegen: schema.gql + zeus (controller+sdk). - Frontend оператора: IssueActOpenDialog получил таблицу сверки кол-ва + «Показать акт»; IssueActFinalizeDialog удалён, «Завершить выдачу»/2-й скан убраны, READY_TO_RECEIVE на столе оператора = «Ждём подпись заказчика». - Frontend заказчика: новый OrdererFinalizeIssuanceDialog (read-only сверка + «Показать акт» + «Подписать и получить» своим ключом) на столе «Готово к получению». Правка ЦЕНЫ (не только кол-ва) — отдельным шагом B2 (затрагивает контракт). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7806,19 +7806,11 @@ type MarketplaceExpressPickupCandidate {
|
||||
}
|
||||
|
||||
input MarketplaceFinalizeIssuanceInput {
|
||||
"""Фактически выдаваемое количество единиц (равно/меньше/больше заказа)."""
|
||||
actual_quantity: Int!
|
||||
|
||||
"""
|
||||
Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком.
|
||||
"""
|
||||
delivery_signer: String!
|
||||
|
||||
"""Идентификатор заказа, который выдаём пайщику."""
|
||||
"""Идентификатор заказа, который получаем."""
|
||||
order_id: ID!
|
||||
|
||||
"""
|
||||
Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями.
|
||||
Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями.
|
||||
"""
|
||||
signed_document: MarketplaceIssueActSignedDocumentInput!
|
||||
}
|
||||
@@ -8361,6 +8353,9 @@ type MarketplaceOnboardingState {
|
||||
}
|
||||
|
||||
input MarketplaceOpenIssuanceInput {
|
||||
"""Фактически выдаваемое количество единиц (равно/меньше/больше заказа)."""
|
||||
actual_quantity: Int!
|
||||
|
||||
"""Идентификатор заказа, выдачу которого открываем."""
|
||||
order_id: ID!
|
||||
|
||||
|
||||
+20
-22
@@ -6,6 +6,12 @@ import { MarketplaceOrderDTO } from './marketplace-order.dto';
|
||||
|
||||
/**
|
||||
* Story 6.1: вход на открытие выдачи председателем КУ (первая подпись АПП).
|
||||
*
|
||||
* `actual_quantity` — фактически выдаваемое количество единиц. Оператор КУ
|
||||
* сверяет привезённое имущество с заказом и фиксирует факт именно в момент
|
||||
* открытия выдачи (взвешивание/пересчёт на стойке). Это количество зашивается
|
||||
* в подписываемый председателем акт и сохраняется на заказе; финальная подпись
|
||||
* заказчика факт уже не редактирует.
|
||||
*/
|
||||
@InputType('MarketplaceOpenIssuanceInput')
|
||||
export class MarketplaceOpenIssuanceInputDTO {
|
||||
@@ -14,6 +20,13 @@ export class MarketplaceOpenIssuanceInputDTO {
|
||||
@IsNotEmpty()
|
||||
public readonly order_id!: string;
|
||||
|
||||
@Field(() => Int, {
|
||||
description: 'Фактически выдаваемое количество единиц (равно/меньше/больше заказа).',
|
||||
})
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
public readonly actual_quantity!: number;
|
||||
|
||||
@Field(() => MarketplaceIssueActSignedDocumentInputDTO, {
|
||||
description:
|
||||
'Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись.',
|
||||
@@ -26,37 +39,22 @@ export class MarketplaceOpenIssuanceInputDTO {
|
||||
/**
|
||||
* Story 6.3: вход на финальную подпись заказчика (получение имущества).
|
||||
*
|
||||
* `actual_quantity` указывается оператором перед сканированием подписи и
|
||||
* влияет на корректирующие операции в композитной транзакции `signiss2`
|
||||
* (см. FR23-25 и контракт `signiss2.cpp`).
|
||||
*
|
||||
* `delivery_signer` — учётная запись стороны кооператива (председатель КУ
|
||||
* или доверенное им лицо), подписывающая акт вместе с заказчиком.
|
||||
* Заказчик ставит финальную подпись в своём кабинете на своём устройстве своим
|
||||
* ключом — он только подтверждает уже сформированный акт, факт не редактирует.
|
||||
* Поэтому фактическое количество и сторона кооператива, открывшая выдачу,
|
||||
* берутся backend'ом из заказа (зафиксированы оператором при открытии), а не
|
||||
* передаются заказчиком.
|
||||
*/
|
||||
@InputType('MarketplaceFinalizeIssuanceInput')
|
||||
export class MarketplaceFinalizeIssuanceInputDTO {
|
||||
@Field(() => ID, { description: 'Идентификатор заказа, который выдаём пайщику.' })
|
||||
@Field(() => ID, { description: 'Идентификатор заказа, который получаем.' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public readonly order_id!: string;
|
||||
|
||||
@Field(() => Int, {
|
||||
description: 'Фактически выдаваемое количество единиц (равно/меньше/больше заказа).',
|
||||
})
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
public readonly actual_quantity!: number;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком.',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public readonly delivery_signer!: string;
|
||||
|
||||
@Field(() => MarketplaceIssueActSignedDocumentInputDTO, {
|
||||
description:
|
||||
'Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями.',
|
||||
'Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями.',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => MarketplaceIssueActSignedDocumentInputDTO)
|
||||
|
||||
+3
-3
@@ -76,6 +76,7 @@ export class MarketplaceIssuanceResolver {
|
||||
coopname: config.coopname,
|
||||
chairman_account: member.username,
|
||||
order_id: data.order_id,
|
||||
actual_quantity: data.actual_quantity,
|
||||
signed_document: data.signed_document,
|
||||
});
|
||||
return new MarketplaceIssuanceResultDTO({
|
||||
@@ -99,8 +100,6 @@ export class MarketplaceIssuanceResolver {
|
||||
coopname: config.coopname,
|
||||
orderer_account: member.username,
|
||||
order_id: data.order_id,
|
||||
actual_quantity: data.actual_quantity,
|
||||
delivery_signer: data.delivery_signer,
|
||||
signed_document: data.signed_document,
|
||||
});
|
||||
return new MarketplaceIssuanceResultDTO({
|
||||
@@ -147,7 +146,8 @@ export class MarketplaceIssuanceResolver {
|
||||
const doc = await this.service.getOpenIssuanceSignablePayload(
|
||||
coopname,
|
||||
data.order_id,
|
||||
member.username
|
||||
member.username,
|
||||
data.actual_quantity
|
||||
);
|
||||
return toGeneratedDocumentDTO(doc);
|
||||
}
|
||||
|
||||
+58
-30
@@ -37,6 +37,7 @@ export interface MarketplaceOpenIssuanceInput {
|
||||
coopname: string;
|
||||
chairman_account: string;
|
||||
order_id: string;
|
||||
actual_quantity: number;
|
||||
signed_document: MarketplaceIssueActSignedDocumentInputDTO;
|
||||
}
|
||||
|
||||
@@ -44,8 +45,6 @@ export interface MarketplaceFinalizeIssuanceInput {
|
||||
coopname: string;
|
||||
orderer_account: string;
|
||||
order_id: string;
|
||||
actual_quantity: number;
|
||||
delivery_signer: string;
|
||||
signed_document: MarketplaceIssueActSignedDocumentInputDTO;
|
||||
}
|
||||
|
||||
@@ -61,21 +60,25 @@ export interface MarketplaceIssuanceResult {
|
||||
*
|
||||
* Flow:
|
||||
*
|
||||
* 1. `openIssuance(order_id, chairman_signed_document)` — председатель
|
||||
* КУ выдачи открывает выдачу первой подписью. Backend верифицирует
|
||||
* подпись, отправляет on-chain `signiss1`, переводит Order
|
||||
* 1. `openIssuance(order_id, actual_quantity, chairman_signed_document)` —
|
||||
* оператор КУ сверяет привезённое имущество с заказом и фиксирует
|
||||
* фактическое количество (`actual_quantity`) прямо при открытии выдачи;
|
||||
* председатель КУ подписывает акт с этим количеством. Backend верифицирует
|
||||
* подпись, сохраняет снапшот фактической выдачи (`issuance_fact`),
|
||||
* отправляет on-chain `signiss1`, переводит Order
|
||||
* ACCEPTED_TO_COOP → READY_TO_RECEIVE и эмитит push заказчику
|
||||
* `marketplace-order-ready` (FR22).
|
||||
*
|
||||
* 2. `finalizeIssuance(order_id, actual_quantity, delivery_signer,
|
||||
* signed_document)` — заказчик закрывает выдачу финальной подписью.
|
||||
* Backend верифицирует обе подписи в `signed_document.signatures`,
|
||||
* отправляет on-chain `signiss2` с указанным `actual_quantity` — C++
|
||||
* контракт сам исполняет корректирующие операции
|
||||
* (`o.mkt.unlock` если actual<ordered / `o.mkt.lock` если actual>ordered,
|
||||
* FR23) и `o.mkt.consum` (BURN w.mkt.order, Дт 86 / Кт 10, FR24).
|
||||
* Backend сохраняет снапшот фактической выдачи (`issuance_fact`) и
|
||||
* переводит Order READY_TO_RECEIVE → RECEIVED.
|
||||
* 2. `finalizeIssuance(order_id, signed_document)` — заказчик закрывает
|
||||
* выдачу финальной подписью в своём кабинете на своём устройстве: он
|
||||
* лишь подтверждает уже сформированный акт, факт не редактирует.
|
||||
* `actual_quantity` и `delivery_signer` backend берёт из заказа
|
||||
* (зафиксированы оператором при открытии). Backend верифицирует обе
|
||||
* подписи в `signed_document.signatures`, отправляет on-chain `signiss2`
|
||||
* с этим `actual_quantity` — C++ контракт сам исполняет корректирующие
|
||||
* операции (`o.mkt.unlock` если actual<ordered / `o.mkt.lock` если
|
||||
* actual>ordered, FR23) и `o.mkt.consum` (BURN w.mkt.order, Дт 86 /
|
||||
* Кт 10, FR24). Order переводится READY_TO_RECEIVE → RECEIVED.
|
||||
*
|
||||
* При расхождении фактического количества с заказом и нехватке средств
|
||||
* пайщика на доплату (`actual > заказ`, L6 guard) транзакция `signiss2`
|
||||
@@ -105,7 +108,8 @@ export class MarketplaceIssuanceService {
|
||||
async getOpenIssuanceSignablePayload(
|
||||
coopname: string,
|
||||
order_id: string,
|
||||
chairman_account: string
|
||||
chairman_account: string,
|
||||
actual_quantity?: number
|
||||
): Promise<DocumentDomainEntity> {
|
||||
const order = await this.loadOrder(coopname, order_id);
|
||||
if (order.status !== 'ACCEPTED_TO_COOP') {
|
||||
@@ -113,10 +117,14 @@ export class MarketplaceIssuanceService {
|
||||
`Заказ в статусе «${order.status}», открытие выдачи недопустимо.`
|
||||
);
|
||||
}
|
||||
// Оператор сверяет факт при открытии: акт формируется на фактически
|
||||
// выдаваемое количество. Если оператор ещё не скорректировал — по заказу.
|
||||
const fact_quantity =
|
||||
actual_quantity && actual_quantity > 0 ? actual_quantity : order.quantity;
|
||||
return this.generateIssueActDocument({
|
||||
order,
|
||||
transmitter: chairman_account,
|
||||
actual_quantity: order.quantity,
|
||||
actual_quantity: fact_quantity,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,9 +171,16 @@ export class MarketplaceIssuanceService {
|
||||
if (order.chairman_signed_at !== null) {
|
||||
throw new ConflictException('Первая подпись выдачи уже зафиксирована для этого заказа.');
|
||||
}
|
||||
if (input.actual_quantity <= 0) {
|
||||
throw new BadRequestException('Фактическое количество должно быть больше нуля.');
|
||||
}
|
||||
|
||||
this.verifyDocumentSignature(input.signed_document);
|
||||
|
||||
// Факт фиксируется оператором при открытии выдачи и сохраняется на заказе;
|
||||
// финальная подпись заказчика берёт его из снапшота, а не редактирует.
|
||||
const factSnapshot = this.buildIssuanceFactSnapshot(order, input.actual_quantity);
|
||||
|
||||
const act = new SignedDigitalDocumentInputDTO(input.signed_document).toDocument() as MarketContract.Actions.SignIss1.ISignIss1['act'];
|
||||
|
||||
let tx;
|
||||
@@ -199,6 +214,9 @@ export class MarketplaceIssuanceService {
|
||||
chairman_account: input.chairman_account,
|
||||
signiss1_tx_hash: txHash,
|
||||
current_warehouse_braname: order.delivery_braname,
|
||||
// факт зафиксирован оператором при открытии — сохраняем снапшот, чтобы
|
||||
// финальная подпись заказчика взяла actual_quantity отсюда.
|
||||
issuance_fact: factSnapshot,
|
||||
// канон 2-подписи: сохраняем подписанный председателем документ, чтобы
|
||||
// заказчик получил его как DocumentAggregate и наложил вторую подпись.
|
||||
issue_act_signiss1_document:
|
||||
@@ -226,12 +244,12 @@ export class MarketplaceIssuanceService {
|
||||
input: MarketplaceFinalizeIssuanceInput
|
||||
): Promise<MarketplaceIssuanceResult> {
|
||||
const order = await this.loadOrder(input.coopname, input.order_id);
|
||||
// Канон выдачи (UX-DR4): финальную подпись заказчик ставит на устройстве
|
||||
// оператора КУ (operator-assisted POS) либо со своего стола. Авторизуем
|
||||
// по самой подписи, а не по JWT submitter'а: закрывающую подпись акта
|
||||
// обязан нести ключ заказчика-владельца заказа. Криптовалидность всех
|
||||
// подписей проверяется ниже в verifyDocumentSignature; submitter уже
|
||||
// ограничен RoleGuard'ом 'Issuance','sign:final'.
|
||||
// Канон выдачи: финальную подпись заказчик ставит сам в своём кабинете на
|
||||
// своём устройстве своим ключом. Авторизуем по самой подписи, а не по JWT
|
||||
// submitter'а: закрывающую подпись акта обязан нести ключ заказчика-владельца
|
||||
// заказа. Криптовалидность всех подписей проверяется ниже в
|
||||
// verifyDocumentSignature; submitter уже ограничен RoleGuard'ом
|
||||
// 'Issuance','sign:final'.
|
||||
const ordererSigned = (input.signed_document.signatures ?? []).some(
|
||||
(sig) => sig.signer === order.orderer_account
|
||||
);
|
||||
@@ -250,9 +268,19 @@ export class MarketplaceIssuanceService {
|
||||
'Финальная подпись выдачи уже зафиксирована для этого заказа.'
|
||||
);
|
||||
}
|
||||
if (input.actual_quantity <= 0) {
|
||||
throw new BadRequestException(
|
||||
'Фактическое количество должно быть больше нуля.'
|
||||
// Факт зафиксирован оператором при открытии выдачи (issuance_fact); сторона
|
||||
// кооператива — председатель, открывший выдачу (chairman_account). Заказчик
|
||||
// ничего из этого не передаёт и не редактирует.
|
||||
const actual_quantity = order.issuance_fact?.actual_quantity;
|
||||
if (!actual_quantity || actual_quantity <= 0) {
|
||||
throw new ConflictException(
|
||||
`Заказ ${order.id}: фактическое количество не зафиксировано при открытии выдачи — финализация недоступна.`
|
||||
);
|
||||
}
|
||||
const delivery_signer = order.chairman_account;
|
||||
if (!delivery_signer) {
|
||||
throw new ConflictException(
|
||||
`Заказ ${order.id}: не найден председатель, открывший выдачу — финализация недоступна.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -266,8 +294,8 @@ export class MarketplaceIssuanceService {
|
||||
coopname: order.coopname,
|
||||
orderer: order.orderer_account,
|
||||
order_hash: order.order_hash,
|
||||
actual_quantity: input.actual_quantity,
|
||||
delivery_signer: input.delivery_signer,
|
||||
actual_quantity,
|
||||
delivery_signer,
|
||||
act,
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -286,21 +314,21 @@ export class MarketplaceIssuanceService {
|
||||
`Не получен tx_hash от блокчейна для finalize issuance order ${order.id}. Повторите подписание.`
|
||||
);
|
||||
}
|
||||
const factSnapshot = this.buildIssuanceFactSnapshot(order, input.actual_quantity);
|
||||
const factSnapshot = order.issuance_fact ?? this.buildIssuanceFactSnapshot(order, actual_quantity);
|
||||
const warrantyUntil =
|
||||
order.warranty_period_secs > 0
|
||||
? new Date(Date.now() + order.warranty_period_secs * 1000)
|
||||
: null;
|
||||
|
||||
const updated = await this.orderRepo.applyIssuanceFinalized(order.id, {
|
||||
delivery_signer_account: input.delivery_signer,
|
||||
delivery_signer_account: delivery_signer,
|
||||
signiss2_tx_hash: txHash,
|
||||
issuance_fact: factSnapshot,
|
||||
warranty_until: warrantyUntil,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Выдача order ${order.id} завершена: actual_quantity=${input.actual_quantity}, diff=${factSnapshot.diff_state}, fact_cost=${factSnapshot.fact_cost} (tx=${txHash}).`
|
||||
`Выдача order ${order.id} завершена: actual_quantity=${actual_quantity}, diff=${factSnapshot.diff_state}, fact_cost=${factSnapshot.fact_cost} (tx=${txHash}).`
|
||||
);
|
||||
|
||||
return { order: updated, tx_hash: txHash };
|
||||
|
||||
+3
-1
@@ -124,7 +124,8 @@ export interface MarketplaceOrderDomainRepository
|
||||
* Story 6.1: применяет первую подпись АПП-выдачи (председатель КУ открыл
|
||||
* выдачу). Переводит Order ACCEPTED_TO_COOP → READY_TO_RECEIVE и
|
||||
* заполняет `current_warehouse_braname` (= delivery_braname),
|
||||
* `chairman_signed_at`, `chairman_account`, `signiss1_tx_hash`.
|
||||
* `chairman_signed_at`, `chairman_account`, `signiss1_tx_hash`,
|
||||
* `issuance_fact` (факт зафиксирован оператором при открытии).
|
||||
*/
|
||||
applyIssuanceOpened(
|
||||
id: string,
|
||||
@@ -132,6 +133,7 @@ export interface MarketplaceOrderDomainRepository
|
||||
chairman_account: string;
|
||||
signiss1_tx_hash: string;
|
||||
current_warehouse_braname: string;
|
||||
issuance_fact: MarketplaceOrderIssuanceFactSnapshot;
|
||||
issue_act_signiss1_document: ISignedDocumentDomainInterface;
|
||||
}
|
||||
): Promise<MarketplaceOrderDomainEntity>;
|
||||
|
||||
+2
@@ -277,6 +277,7 @@ export class MarketplaceOrderRepositoryAdapter implements MarketplaceOrderDomain
|
||||
chairman_account: string;
|
||||
signiss1_tx_hash: string;
|
||||
current_warehouse_braname: string;
|
||||
issuance_fact: MarketplaceOrderIssuanceFactSnapshot;
|
||||
issue_act_signiss1_document: ISignedDocumentDomainInterface;
|
||||
}
|
||||
): Promise<MarketplaceOrderDomainEntity> {
|
||||
@@ -288,6 +289,7 @@ export class MarketplaceOrderRepositoryAdapter implements MarketplaceOrderDomain
|
||||
chairman_account: patch.chairman_account,
|
||||
signiss1_tx_hash: patch.signiss1_tx_hash,
|
||||
current_warehouse_braname: patch.current_warehouse_braname,
|
||||
issuance_fact: patch.issuance_fact,
|
||||
issue_act_signiss1_document: patch.issue_act_signiss1_document,
|
||||
} as Record<string, unknown>
|
||||
);
|
||||
|
||||
@@ -6679,13 +6679,9 @@ export type ValueTypes = {
|
||||
['...on MarketplaceExpressPickupCandidate']?: Omit<ValueTypes["MarketplaceExpressPickupCandidate"], "...on MarketplaceExpressPickupCandidate">
|
||||
}>;
|
||||
["MarketplaceFinalizeIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number | Variable<any, string>,
|
||||
/** Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком. */
|
||||
delivery_signer: string | Variable<any, string>,
|
||||
/** Идентификатор заказа, который выдаём пайщику. */
|
||||
/** Идентификатор заказа, который получаем. */
|
||||
order_id: ValueTypes["ID"] | Variable<any, string>,
|
||||
/** Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
/** Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
signed_document: ValueTypes["MarketplaceIssueActSignedDocumentInput"] | Variable<any, string>
|
||||
};
|
||||
/** Параметры запроса одного заказа. */
|
||||
@@ -7082,6 +7078,8 @@ export type ValueTypes = {
|
||||
['...on MarketplaceOnboardingState']?: Omit<ValueTypes["MarketplaceOnboardingState"], "...on MarketplaceOnboardingState">
|
||||
}>;
|
||||
["MarketplaceOpenIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number | Variable<any, string>,
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
order_id: ValueTypes["ID"] | Variable<any, string>,
|
||||
/** Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись. */
|
||||
@@ -16761,13 +16759,9 @@ export type ResolverInputTypes = {
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["MarketplaceFinalizeIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком. */
|
||||
delivery_signer: string,
|
||||
/** Идентификатор заказа, который выдаём пайщику. */
|
||||
/** Идентификатор заказа, который получаем. */
|
||||
order_id: ResolverInputTypes["ID"],
|
||||
/** Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
/** Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
signed_document: ResolverInputTypes["MarketplaceIssueActSignedDocumentInput"]
|
||||
};
|
||||
/** Параметры запроса одного заказа. */
|
||||
@@ -17153,6 +17147,8 @@ export type ResolverInputTypes = {
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["MarketplaceOpenIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
order_id: ResolverInputTypes["ID"],
|
||||
/** Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись. */
|
||||
@@ -26517,13 +26513,9 @@ export type ModelTypes = {
|
||||
total_units: number
|
||||
};
|
||||
["MarketplaceFinalizeIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком. */
|
||||
delivery_signer: string,
|
||||
/** Идентификатор заказа, который выдаём пайщику. */
|
||||
/** Идентификатор заказа, который получаем. */
|
||||
order_id: ModelTypes["ID"],
|
||||
/** Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
/** Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
signed_document: ModelTypes["MarketplaceIssueActSignedDocumentInput"]
|
||||
};
|
||||
/** Параметры запроса одного заказа. */
|
||||
@@ -26897,6 +26889,8 @@ export type ModelTypes = {
|
||||
template_registry_id: number
|
||||
};
|
||||
["MarketplaceOpenIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
order_id: ModelTypes["ID"],
|
||||
/** Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись. */
|
||||
@@ -37189,13 +37183,9 @@ export type GraphQLTypes = {
|
||||
['...on MarketplaceExpressPickupCandidate']: Omit<GraphQLTypes["MarketplaceExpressPickupCandidate"], "...on MarketplaceExpressPickupCandidate">
|
||||
};
|
||||
["MarketplaceFinalizeIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком. */
|
||||
delivery_signer: string,
|
||||
/** Идентификатор заказа, который выдаём пайщику. */
|
||||
/** Идентификатор заказа, который получаем. */
|
||||
order_id: GraphQLTypes["ID"],
|
||||
/** Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
/** Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
signed_document: GraphQLTypes["MarketplaceIssueActSignedDocumentInput"]
|
||||
};
|
||||
/** Параметры запроса одного заказа. */
|
||||
@@ -37592,7 +37582,9 @@ export type GraphQLTypes = {
|
||||
['...on MarketplaceOnboardingState']: Omit<GraphQLTypes["MarketplaceOnboardingState"], "...on MarketplaceOnboardingState">
|
||||
};
|
||||
["MarketplaceOpenIssuanceInput"]: {
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
order_id: GraphQLTypes["ID"],
|
||||
/** Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись. */
|
||||
signed_document: GraphQLTypes["MarketplaceIssueActSignedDocumentInput"]
|
||||
|
||||
@@ -65,10 +65,11 @@ export async function listMyReadyToReceive(): Promise<MarketplaceOrderIssuanceVi
|
||||
|
||||
export async function getChairmanSignablePayload(
|
||||
order_id: string,
|
||||
actual_quantity?: number,
|
||||
): Promise<MarketplaceGeneratedDocumentView> {
|
||||
const { [Queries.Marketplace.IssueActChairmanSignablePayload.name]: result } =
|
||||
await client.Query(Queries.Marketplace.IssueActChairmanSignablePayload.query, {
|
||||
variables: { data: { order_id } },
|
||||
variables: { data: { order_id, actual_quantity } },
|
||||
});
|
||||
return result as MarketplaceGeneratedDocumentView;
|
||||
}
|
||||
@@ -86,28 +87,29 @@ export async function getOrdererSignablePayload(
|
||||
|
||||
export async function openIssuance(
|
||||
order_id: string,
|
||||
actual_quantity: number,
|
||||
signed_document: SignedDocumentInput,
|
||||
): Promise<MarketplaceIssuanceResultView> {
|
||||
const { [Mutations.Marketplace.OpenIssuance.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.OpenIssuance.mutation,
|
||||
{ variables: { data: { order_id, signed_document } } },
|
||||
{ variables: { data: { order_id, actual_quantity, signed_document } } },
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Финальную подпись заказчик ставит сам в своём кабинете своим ключом — он
|
||||
* лишь подтверждает уже сформированный акт. Фактическое количество и сторона
|
||||
* кооператива берутся backend'ом из заказа (зафиксированы оператором при
|
||||
* открытии), поэтому здесь передаём только подписанный документ.
|
||||
*/
|
||||
export async function finalizeIssuance(
|
||||
order_id: string,
|
||||
actual_quantity: number,
|
||||
delivery_signer: string,
|
||||
signed_document: SignedDocumentInput,
|
||||
): Promise<MarketplaceIssuanceResultView> {
|
||||
const { [Mutations.Marketplace.FinalizeIssuance.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.FinalizeIssuance.mutation,
|
||||
{
|
||||
variables: {
|
||||
data: { order_id, actual_quantity, delivery_signer, signed_document },
|
||||
},
|
||||
},
|
||||
{ variables: { data: { order_id, signed_document } } },
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
-277
@@ -1,277 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Classes } from '@coopenomics/sdk';
|
||||
import { PrivateKey } from '@wharfkit/session';
|
||||
import { useGlobalStore } from 'src/shared/store';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api';
|
||||
import { TakeoverDialog } from 'src/widgets/Marketplace/TakeoverDialog';
|
||||
import { BarcodeScanner } from 'src/widgets/Marketplace/BarcodeScanner';
|
||||
import { CorrectionTable, type CorrectionRow } from 'src/widgets/Marketplace/CorrectionTable';
|
||||
import {
|
||||
finalizeIssuance,
|
||||
getOrdererSignablePayload,
|
||||
type MarketplaceOrderIssuanceView,
|
||||
} from '../api';
|
||||
|
||||
/**
|
||||
* Story 6.3 / FR23-FR25: full-screen takeover для финальной подписи
|
||||
* АПП-выдачи на ПВЗ.
|
||||
*
|
||||
* Поток:
|
||||
* 1. Оператор сканирует штрих-код заказа (BarcodeScanner) — verification,
|
||||
* что выдаётся правильный заказ.
|
||||
* 2. CorrectionTable показывает «план vs факт» — оператор корректирует
|
||||
* `actual_quantity` (равно / меньше / больше); UI считает
|
||||
* `fact_cost = actual_quantity × unit_price` и `diff_state`.
|
||||
* 3. Backend отдаёт акт, уже подписанный председателем при открытии
|
||||
* выдачи (signatureId=1, on-chain signiss1) — в виде агрегата
|
||||
* (rawDocument + document с подписью председателя).
|
||||
* 4. Заказчик вводит свой приватный ключ и накладывает финальную
|
||||
* подпись (signatureId=2) поверх подписи председателя; документ не
|
||||
* перегенерируется. `delivery_signer` = председатель, открывший
|
||||
* выдачу (order.chairman_account).
|
||||
* 5. Подписанный документ уходит mutation `marketplaceFinalizeIssuance`
|
||||
* с указанием `actual_quantity` и `delivery_signer` — C++ контракт
|
||||
* исполняет три ветки сверки в одной композитной транзакции
|
||||
* `signiss2`: при `actual < ordered` срабатывает `o.mkt.unblk`,
|
||||
* при `actual > ordered` — серия `o.wal.conv + o.mkt.assign +
|
||||
* o.mkt.block` на разницу (FR23). Если средств не хватает на
|
||||
* разницу — транзакция фейлится с человеческим сообщением (FR25 /
|
||||
* L6 guard), backend пробрасывает его клиенту.
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
order: MarketplaceOrderIssuanceView | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void;
|
||||
(e: 'finalized'): void;
|
||||
}>();
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const STEP_SCAN = 'scan' as const;
|
||||
const STEP_FACT = 'fact' as const;
|
||||
const STEP_SIGN = 'sign' as const;
|
||||
type Step = typeof STEP_SCAN | typeof STEP_FACT | typeof STEP_SIGN;
|
||||
|
||||
const step = ref<Step>(STEP_SCAN);
|
||||
const scannedCode = ref<string>('');
|
||||
const actualQuantity = ref<number>(0);
|
||||
const ordererWif = ref<string>('');
|
||||
const signing = ref(false);
|
||||
|
||||
const factCost = computed<string>(() => {
|
||||
if (!props.order) return '0';
|
||||
const unit = Number.parseFloat(props.order.price_per_unit);
|
||||
return (actualQuantity.value * unit).toFixed(4);
|
||||
});
|
||||
|
||||
const diffState = computed<'equal' | 'less' | 'more'>(() => {
|
||||
if (!props.order) return 'equal';
|
||||
if (actualQuantity.value === props.order.quantity) return 'equal';
|
||||
if (actualQuantity.value < props.order.quantity) return 'less';
|
||||
return 'more';
|
||||
});
|
||||
|
||||
const diffHint = computed<string>(() => {
|
||||
if (!props.order) return '';
|
||||
if (diffState.value === 'equal') return 'Факт совпадает с заказом — корректирующие операции не нужны.';
|
||||
if (diffState.value === 'less')
|
||||
return `Выдаётся меньше заказа — разница ${props.order.quantity - actualQuantity.value} ед. вернётся на доступный паевой пайщика операцией o.mkt.unblk.`;
|
||||
return `Выдаётся больше заказа — разница ${actualQuantity.value - props.order.quantity} ед. потребует доплаты с паевого (o.wal.conv + o.mkt.assign + o.mkt.block).`;
|
||||
});
|
||||
|
||||
const correctionRows = computed<CorrectionRow[]>(() => {
|
||||
if (!props.order) return [];
|
||||
return [
|
||||
{
|
||||
sku: props.order.id.slice(0, 8),
|
||||
title: `Заказ ${props.order.id.slice(0, 8)}`,
|
||||
unit: 'ед.',
|
||||
expected: props.order.quantity,
|
||||
fact: actualQuantity.value,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.order?.id],
|
||||
([visible]) => {
|
||||
if (visible && props.order) {
|
||||
step.value = STEP_SCAN;
|
||||
scannedCode.value = '';
|
||||
actualQuantity.value = props.order.quantity;
|
||||
ordererWif.value = '';
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
function onScanned(code: string): void {
|
||||
scannedCode.value = code;
|
||||
step.value = STEP_FACT;
|
||||
}
|
||||
|
||||
function onCorrectionChange(payload: { sku: string; fact: number }): void {
|
||||
actualQuantity.value = Math.max(0, payload.fact);
|
||||
}
|
||||
|
||||
function goToSign(): void {
|
||||
if (actualQuantity.value <= 0) {
|
||||
FailAlert(new Error('Фактическое количество должно быть больше нуля.'));
|
||||
return;
|
||||
}
|
||||
step.value = STEP_SIGN;
|
||||
}
|
||||
|
||||
function backToFact(): void {
|
||||
step.value = STEP_FACT;
|
||||
}
|
||||
|
||||
async function confirm(): Promise<void> {
|
||||
if (!props.order) return;
|
||||
if (step.value !== STEP_SIGN) {
|
||||
if (step.value === STEP_SCAN) {
|
||||
FailAlert(new Error('Сначала отсканируйте штрих-код заказа.'));
|
||||
} else {
|
||||
goToSign();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const operatorWif = globalStore.wif?.toString();
|
||||
if (!operatorWif) {
|
||||
FailAlert(new Error('Приватный ключ оператора не найден. Войдите в кооператив.'));
|
||||
return;
|
||||
}
|
||||
if (!ordererWif.value.trim()) {
|
||||
FailAlert(new Error('Заказчик должен предоставить свой приватный ключ для подписи акта.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const deliverySigner = props.order.chairman_account;
|
||||
if (!deliverySigner) {
|
||||
FailAlert(new Error('Не найден председатель, открывший выдачу. Сначала откройте выдачу первой подписью.'));
|
||||
return;
|
||||
}
|
||||
|
||||
signing.value = true;
|
||||
try {
|
||||
// Валидация приватного ключа заказчика — ранний фейл до запроса акта,
|
||||
// если введён мусор.
|
||||
try {
|
||||
PrivateKey.from(ordererWif.value.trim());
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Некорректный приватный ключ заказчика: ${message}`);
|
||||
}
|
||||
|
||||
// Backend отдаёт акт, уже подписанный председателем при открытии выдачи
|
||||
// (signatureId=1, on-chain signiss1). Заказчик накладывает финальную
|
||||
// подпись (signatureId=2) поверх — документ не перегенерируется.
|
||||
const aggregate = await getOrdererSignablePayload(props.order.id);
|
||||
|
||||
const ordererSigner = new Classes.Document(ordererWif.value.trim());
|
||||
const fullSigned = await ordererSigner.signDocument(
|
||||
aggregate.rawDocument,
|
||||
props.order.orderer_account,
|
||||
2,
|
||||
[aggregate.document],
|
||||
);
|
||||
|
||||
await finalizeIssuance(
|
||||
props.order.id,
|
||||
actualQuantity.value,
|
||||
deliverySigner,
|
||||
fullSigned,
|
||||
);
|
||||
SuccessAlert('Заказ выдан. Статус заказа — RECEIVED.');
|
||||
emit('finalized');
|
||||
emit('update:modelValue', false);
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось завершить выдачу');
|
||||
} finally {
|
||||
signing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancel(): void {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
|
||||
const confirmLabel = computed(() => {
|
||||
if (step.value === STEP_SCAN) return 'Сначала отсканируйте штрих-код';
|
||||
if (step.value === STEP_FACT) return 'Перейти к подписи';
|
||||
return 'Подписать и выдать';
|
||||
});
|
||||
|
||||
const confirmDisabled = computed(
|
||||
() => signing.value || (step.value === STEP_SIGN && (!ordererWif.value.trim() || actualQuantity.value <= 0)),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
TakeoverDialog(
|
||||
:model-value="modelValue"
|
||||
title="Финальная подпись акта выдачи"
|
||||
:lead-text="order ? `Заказ ${order.id.slice(0, 8)} · заказчик ${order.orderer_account}` : ''"
|
||||
:kind="diffState === 'more' ? 'warning' : 'success'"
|
||||
:confirm-label="confirmLabel"
|
||||
cancel-label="Закрыть"
|
||||
:loading="signing"
|
||||
:disable-confirm="confirmDisabled"
|
||||
@update:model-value="(v: boolean) => emit('update:modelValue', v)"
|
||||
@confirm="confirm"
|
||||
@cancel="cancel"
|
||||
)
|
||||
template(#default)
|
||||
.mp-issue-finalize
|
||||
q-stepper(
|
||||
:model-value="step"
|
||||
flat
|
||||
bordered
|
||||
animated
|
||||
active-color="primary"
|
||||
done-color="positive"
|
||||
)
|
||||
q-step(:name="STEP_SCAN" title="Штрих-код" icon="qr_code_scanner" :done="step !== STEP_SCAN")
|
||||
BarcodeScanner(start-label="Сканировать штрих-код заказа" @scanned="onScanned")
|
||||
.text-caption.text-grey.q-mt-sm
|
||||
| Отсканируйте штрих-код заказа камерой или USB-сканером, чтобы сверить его с выдаваемым имуществом.
|
||||
|
||||
q-step(:name="STEP_FACT" title="Сверка факта" icon="balance" :done="step === STEP_SIGN")
|
||||
.q-mb-sm Заказ {{ order?.id.slice(0, 8) }} · план {{ order?.quantity }} ед. · подтверждённый штрих-код «{{ scannedCode }}»
|
||||
CorrectionTable(:rows="correctionRows" @change="onCorrectionChange")
|
||||
q-banner.q-mt-md(rounded :class="diffState === 'more' ? 'bg-warning text-dark' : (diffState === 'less' ? 'bg-info text-white' : 'bg-positive text-white')")
|
||||
| {{ diffHint }}
|
||||
.q-mt-sm.text-grey.text-caption
|
||||
| Фактическая стоимость: {{ factCost }} ₽ (вычислено как {{ actualQuantity }} × {{ order?.price_per_unit ?? '-' }}).
|
||||
.row.justify-end.q-mt-md
|
||||
q-btn(unelevated no-caps color="primary" label="Перейти к подписи" @click="goToSign")
|
||||
|
||||
q-step(:name="STEP_SIGN" title="Финальная подпись" icon="draw")
|
||||
q-banner.q-mb-md(rounded class="bg-primary text-white")
|
||||
| Председатель уже подписал акт при открытии выдачи. Заказчик ставит финальную подпись своим приватным ключом поверх подписи председателя — имущество переходит к нему.
|
||||
q-input(
|
||||
v-model="ordererWif"
|
||||
outlined
|
||||
dense
|
||||
type="password"
|
||||
label="Приватный ключ заказчика (WIF)"
|
||||
hint="Заказчик вводит свой ключ на устройстве оператора либо сканирует QR. После подписи UI не сохраняет ключ."
|
||||
)
|
||||
.row.q-mt-md.q-gutter-md
|
||||
q-btn(flat no-caps label="Назад к сверке" @click="backToFact")
|
||||
q-space
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mp-issue-finalize {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
}
|
||||
</style>
|
||||
+113
-38
@@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Classes } from '@coopenomics/sdk';
|
||||
import { useGlobalStore } from 'src/shared/store';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api';
|
||||
import { TakeoverDialog } from 'src/widgets/Marketplace/TakeoverDialog';
|
||||
import { CorrectionTable, type CorrectionRow } from 'src/widgets/Marketplace/CorrectionTable';
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
|
||||
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
|
||||
import {
|
||||
getChairmanSignablePayload,
|
||||
openIssuance,
|
||||
@@ -12,19 +14,21 @@ import {
|
||||
} from '../api';
|
||||
|
||||
/**
|
||||
* Story 6.1 / FR21: full-screen takeover для первой подписи АПП-выдачи —
|
||||
* председатель кооперативного участка открывает выдачу заказа пайщику.
|
||||
* Story 6.1 / FR21: full-screen takeover для открытия выдачи на ПВЗ.
|
||||
*
|
||||
* Открытие выдачи делает оператор КУ. Именно здесь фиксируется факт: оператор
|
||||
* сверяет привезённое имущество с заказом, взвешивает/пересчитывает и
|
||||
* корректирует фактически выдаваемое количество. Это количество зашивается в
|
||||
* подписываемый председателем акт и сохраняется на заказе — финальная подпись
|
||||
* заказчика факт уже не редактирует.
|
||||
*
|
||||
* Поток:
|
||||
* 1. Бэкенд возвращает preview документа (registry_id=1102) — HTML
|
||||
* отображается в окне; председатель убеждается, что данные акта
|
||||
* совпадают с заказом.
|
||||
* 2. Председатель нажимает «Подписать и открыть выдачу» — UI забирает
|
||||
* приватный ключ из useGlobalStore и подписывает hash акта на месте
|
||||
* (signatureId=1, signer = текущий пайщик).
|
||||
* 3. Подписанный документ уходит mutation `marketplaceOpenIssuance` —
|
||||
* backend верифицирует подпись и отправляет on-chain `signiss1`.
|
||||
* Заказ получает статус READY_TO_RECEIVE, заказчик — push.
|
||||
* 1. Оператор корректирует количество в таблице сверки (предзаполнено заказом).
|
||||
* 2. По кнопке «Показать акт» можно посмотреть сформированный документ.
|
||||
* 3. «Открыть выдачу» — UI берёт акт на фактическое количество, подписывает
|
||||
* его ключом председателя текущей сессии (signatureId=1) и отправляет в
|
||||
* `openIssuance`. Заказ переходит в «Готово к получению», заказчику —
|
||||
* уведомление; финальную подпись он поставит сам в своём кабинете.
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -39,33 +43,88 @@ const emit = defineEmits<{
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const actualQuantity = ref<number>(0);
|
||||
const previewHtml = ref<string>('');
|
||||
const previewLoading = ref(false);
|
||||
const signing = ref(false);
|
||||
|
||||
const unitShort = computed(() =>
|
||||
props.order ? marketplaceUnitShort(props.order.unit_of_measure) : 'ед.',
|
||||
);
|
||||
|
||||
const factCost = computed<string>(() => {
|
||||
if (!props.order) return '0';
|
||||
const unit = Number.parseFloat(props.order.price_per_unit);
|
||||
return (actualQuantity.value * unit).toFixed(4);
|
||||
});
|
||||
|
||||
const diffState = computed<'equal' | 'less' | 'more'>(() => {
|
||||
if (!props.order) return 'equal';
|
||||
if (actualQuantity.value === props.order.quantity) return 'equal';
|
||||
if (actualQuantity.value < props.order.quantity) return 'less';
|
||||
return 'more';
|
||||
});
|
||||
|
||||
const diffHint = computed<string>(() => {
|
||||
if (!props.order) return '';
|
||||
if (diffState.value === 'equal') return 'Факт совпадает с заказом.';
|
||||
if (diffState.value === 'less')
|
||||
return `Выдаётся меньше заказа на ${props.order.quantity - actualQuantity.value} ${unitShort.value} — разница вернётся заказчику.`;
|
||||
return `Выдаётся больше заказа на ${actualQuantity.value - props.order.quantity} ${unitShort.value} — потребуется доплата с паевого заказчика.`;
|
||||
});
|
||||
|
||||
const correctionRows = computed<CorrectionRow[]>(() => {
|
||||
if (!props.order) return [];
|
||||
return [
|
||||
{
|
||||
sku: props.order.id.slice(0, 8),
|
||||
title: props.order.product_name || 'Товар по предложению',
|
||||
unit: unitShort.value,
|
||||
expected: props.order.quantity,
|
||||
fact: actualQuantity.value,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.order?.id],
|
||||
async ([visible]) => {
|
||||
if (!visible || !props.order) {
|
||||
([visible]) => {
|
||||
if (visible && props.order) {
|
||||
actualQuantity.value = props.order.quantity;
|
||||
previewHtml.value = '';
|
||||
return;
|
||||
}
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const doc = await getChairmanSignablePayload(props.order.id);
|
||||
previewHtml.value = doc.html;
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить предварительный акт выдачи');
|
||||
emit('update:modelValue', false);
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
function onCorrectionChange(payload: { sku: string; fact: number }): void {
|
||||
actualQuantity.value = Math.max(0, payload.fact);
|
||||
// Документ зависит от факта — сбрасываем устаревший превью.
|
||||
previewHtml.value = '';
|
||||
}
|
||||
|
||||
async function loadPreview(): Promise<void> {
|
||||
if (!props.order || actualQuantity.value <= 0) {
|
||||
FailAlert(new Error('Сначала укажите фактическое количество больше нуля.'));
|
||||
return;
|
||||
}
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const doc = await getChairmanSignablePayload(props.order.id, actualQuantity.value);
|
||||
previewHtml.value = doc.html;
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось сформировать акт выдачи');
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm(): Promise<void> {
|
||||
if (!props.order) return;
|
||||
if (actualQuantity.value <= 0) {
|
||||
FailAlert(new Error('Фактическое количество должно быть больше нуля.'));
|
||||
return;
|
||||
}
|
||||
const wifKey = globalStore.wif?.toString();
|
||||
if (!wifKey) {
|
||||
FailAlert(new Error('Приватный ключ не найден. Войдите в кооператив.'));
|
||||
@@ -73,11 +132,11 @@ async function confirm(): Promise<void> {
|
||||
}
|
||||
signing.value = true;
|
||||
try {
|
||||
const generated = await getChairmanSignablePayload(props.order.id);
|
||||
const generated = await getChairmanSignablePayload(props.order.id, actualQuantity.value);
|
||||
const docSigner = new Classes.Document(wifKey);
|
||||
const signed = await docSigner.signDocument(generated, globalStore.username, 1);
|
||||
await openIssuance(props.order.id, signed);
|
||||
SuccessAlert('Выдача открыта. Заказчику отправлено уведомление.');
|
||||
await openIssuance(props.order.id, actualQuantity.value, signed);
|
||||
SuccessAlert('Выдача открыта. Ждём подпись заказчика — он подтвердит получение в своём кабинете.');
|
||||
emit('opened');
|
||||
emit('update:modelValue', false);
|
||||
} catch (e) {
|
||||
@@ -96,28 +155,35 @@ function cancel(): void {
|
||||
TakeoverDialog(
|
||||
:model-value="modelValue"
|
||||
title="Открытие выдачи заказа"
|
||||
:lead-text="order ? `Заказ ${order.id.slice(0, 8)} · ${order.quantity} ед. на сумму ${formatAsset2Digits(order.total_cost)} ₽` : ''"
|
||||
kind="info"
|
||||
:lead-text="order ? `Заказ ${order.id.slice(0, 8)} · к выдаче ${formatAsset2Digits(factCost)} ₽` : ''"
|
||||
:kind="diffState === 'more' ? 'warning' : 'info'"
|
||||
confirm-label="Подписать и открыть выдачу"
|
||||
cancel-label="Закрыть"
|
||||
:loading="signing"
|
||||
:disable-confirm="!previewHtml || signing"
|
||||
:disable-confirm="actualQuantity <= 0 || signing"
|
||||
@update:model-value="(v: boolean) => emit('update:modelValue', v)"
|
||||
@confirm="confirm"
|
||||
@cancel="cancel"
|
||||
)
|
||||
template(#default)
|
||||
.mp-issue-open-dialog
|
||||
q-card(v-if="previewLoading" flat bordered).q-pa-md
|
||||
q-spinner(color="primary" size="32px")
|
||||
.q-ml-md Готовлю акт выдачи…
|
||||
.mp-issue-open-dialog__intro.text-body2.text-grey
|
||||
| Сверьте привезённое имущество с заказом и укажите фактически выдаваемое количество — оно войдёт в акт.
|
||||
|
||||
q-card(v-else-if="previewHtml" flat bordered).mp-issue-open-dialog__preview
|
||||
CorrectionTable(:rows="correctionRows" @change="onCorrectionChange")
|
||||
|
||||
q-banner.mp-issue-open-dialog__hint(rounded :class="diffState === 'more' ? 'bg-warning text-dark' : (diffState === 'less' ? 'bg-info text-white' : 'bg-positive text-white')")
|
||||
| {{ diffHint }}
|
||||
|
||||
.mp-issue-open-dialog__sum.text-body2
|
||||
| К выдаче: {{ formatAsset2Digits(factCost) }} ₽ ({{ actualQuantity }} {{ unitShort }})
|
||||
|
||||
.mp-issue-open-dialog__preview-actions
|
||||
q-btn(flat no-caps icon="description" label="Показать акт" :loading="previewLoading" @click="loadPreview")
|
||||
|
||||
q-card(v-if="previewHtml" flat bordered).mp-issue-open-dialog__preview
|
||||
q-card-section.q-pa-md
|
||||
div(v-html="previewHtml")
|
||||
|
||||
q-card(v-else flat bordered).q-pa-md
|
||||
.text-negative Предварительный документ не сформирован.
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -126,6 +192,15 @@ TakeoverDialog(
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__sum {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__preview-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
|
||||
+29
-50
@@ -16,22 +16,20 @@ import {
|
||||
type MarketplaceOrderIssuanceView,
|
||||
} from '../api';
|
||||
import IssueActOpenDialog from './IssueActOpenDialog.vue';
|
||||
import IssueActFinalizeDialog from './IssueActFinalizeDialog.vue';
|
||||
|
||||
/**
|
||||
* Story 6.1 / 6.3 / 6.6: operator-стол выдачи имущества пайщику.
|
||||
*
|
||||
* Показывает Order'ы на КУ выдачи в статусах ACCEPTED_TO_COOP (ожидают
|
||||
* открытия первой подписью председателя — `signiss1`) и READY_TO_RECEIVE
|
||||
* (ожидают финальной подписи заказчика на ПВЗ — `signiss2`).
|
||||
* открытия выдачи оператором) и READY_TO_RECEIVE (выдача открыта — ждём,
|
||||
* когда заказчик подтвердит получение в своём кабинете).
|
||||
*
|
||||
* - «Открыть выдачу» вызывает IssueActOpenDialog — full-screen takeover
|
||||
* с превью акта (registry_id=1102) и подписью председателя.
|
||||
* - «Завершить выдачу» вызывает IssueActFinalizeDialog — BarcodeScanner
|
||||
* для проверки штрих-кода заказа + CorrectionTable со сверкой
|
||||
* факт vs заказ + двойная подпись (заказчик + delivery_signer) и
|
||||
* композитная транзакция `signiss2` с корректирующими операциями
|
||||
* по FR23-FR25.
|
||||
* Роль оператора заканчивается на открытии: «Открыть выдачу» вызывает
|
||||
* IssueActOpenDialog — full-screen takeover, где оператор сверяет факт
|
||||
* (корректирует количество) и подписывает акт ключом председателя. Дальше
|
||||
* заказ ждёт финальную подпись заказчика — оператор свободен и может
|
||||
* обслуживать следующего. Финальную подпись заказчик ставит сам в своём
|
||||
* кабинете на своём устройстве (стол «Готово к получению»).
|
||||
*/
|
||||
|
||||
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
|
||||
@@ -43,7 +41,6 @@ const items = ref<MarketplaceOrderIssuanceView[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const openDialog = ref(false);
|
||||
const finalizeDialog = ref(false);
|
||||
const selectedOrder = ref<MarketplaceOrderIssuanceView | null>(null);
|
||||
|
||||
const columns: QTableProps['columns'] = [
|
||||
@@ -90,11 +87,6 @@ function startOpen(item: MarketplaceOrderIssuanceView): void {
|
||||
openDialog.value = true;
|
||||
}
|
||||
|
||||
function startFinalize(item: MarketplaceOrderIssuanceView): void {
|
||||
selectedOrder.value = item;
|
||||
finalizeDialog.value = true;
|
||||
}
|
||||
|
||||
// QR-код получения (Story 14.4): оператор сканирует account-bound код заказчика
|
||||
// → резолвим аккаунт против ленты своего КУ и показываем РАЗОМ все его заказы
|
||||
// на выдачу.
|
||||
@@ -112,16 +104,15 @@ const pickupOrders = computed(() =>
|
||||
),
|
||||
);
|
||||
|
||||
// Открыть/завершить выдачу одного заказа. Панель агрегированных заказов
|
||||
// заказчика остаётся открытой под полноэкранным шагом выдачи — по `load()`
|
||||
// список пересчитывается, выданный заказ уходит, оператор продолжает следующий.
|
||||
// Открыть выдачу одного заказа. Панель агрегированных заказов заказчика
|
||||
// остаётся открытой под полноэкранным шагом открытия — по `load()` список
|
||||
// пересчитывается, открытый заказ уходит в «ждём заказчика», оператор
|
||||
// продолжает следующий.
|
||||
function startIssuanceStep(order: MarketplaceOrderIssuanceView): void {
|
||||
if (order.status === 'ACCEPTED_TO_COOP') {
|
||||
startOpen(order);
|
||||
} else if (order.status === 'READY_TO_RECEIVE') {
|
||||
startFinalize(order);
|
||||
} else {
|
||||
FailAlert(new Error('Заказ не в статусе выдачи.'));
|
||||
FailAlert(new Error('Выдача уже открыта — ждём подтверждение заказчика.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +142,6 @@ function onOpened(): void {
|
||||
void load();
|
||||
}
|
||||
|
||||
function onFinalized(): void {
|
||||
void load();
|
||||
}
|
||||
|
||||
watch(braname, () => void load());
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -177,7 +164,7 @@ q-page.issuance(role='region', aria-label='Выдача заказов')
|
||||
|
||||
template(v-else)
|
||||
PageHint(storage-key='mp:operator-issuance:banner-dismissed')
|
||||
| Заказы, принятые кооперативом на ваш пункт выдачи. Откройте выдачу подписью председателя, затем завершите её на стойке с заказчиком.
|
||||
| Заказы, принятые кооперативом на ваш пункт выдачи. Сверьте имущество и откройте выдачу подписью председателя — дальше заказчик подтвердит получение сам в своём кабинете.
|
||||
|
||||
.issuance__toolbar
|
||||
BaseButton(variant='secondary', @click='scanDialogOpen = true')
|
||||
@@ -208,15 +195,9 @@ q-page.issuance(role='region', aria-label='Выдача заказов')
|
||||
template(#icon-left)
|
||||
q-icon(name='draw', size='16px')
|
||||
| Открыть выдачу
|
||||
BaseButton(
|
||||
v-else-if='props.row.status === "READY_TO_RECEIVE"',
|
||||
variant='primary',
|
||||
size='sm',
|
||||
@click='startFinalize(props.row)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='inventory', size='16px')
|
||||
| Завершить выдачу
|
||||
.issuance__await(v-else-if='props.row.status === "READY_TO_RECEIVE"')
|
||||
q-icon(name='hourglass_empty', size='16px')
|
||||
| Ждём подпись заказчика
|
||||
|
||||
template(#no-data)
|
||||
EmptyState(
|
||||
@@ -231,11 +212,6 @@ q-page.issuance(role='region', aria-label='Выдача заказов')
|
||||
:order='selectedOrder',
|
||||
@opened='onOpened'
|
||||
)
|
||||
IssueActFinalizeDialog(
|
||||
v-model='finalizeDialog',
|
||||
:order='selectedOrder',
|
||||
@finalized='onFinalized'
|
||||
)
|
||||
|
||||
BaseDialog(v-model='scanDialogOpen', title='Сканирование QR заказа', size='sm')
|
||||
QrScanner(@scanned='onQrScanned')
|
||||
@@ -261,15 +237,9 @@ q-page.issuance(role='region', aria-label='Выдача заказов')
|
||||
template(#icon-left)
|
||||
q-icon(name='draw', size='16px')
|
||||
| Открыть
|
||||
BaseButton(
|
||||
v-else-if='o.status === "READY_TO_RECEIVE"',
|
||||
variant='primary',
|
||||
size='sm',
|
||||
@click='startIssuanceStep(o)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='inventory', size='16px')
|
||||
| Завершить
|
||||
.issuance__await(v-else-if='o.status === "READY_TO_RECEIVE"')
|
||||
q-icon(name='hourglass_empty', size='16px')
|
||||
| Ждём заказчика
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -327,6 +297,15 @@ q-page.issuance(role='region', aria-label='Выдача заказов')
|
||||
color: var(--p-ink-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__await {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--p-1, 4px);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -4,9 +4,16 @@ import { client } from 'src/shared/api/client';
|
||||
export type {
|
||||
MarketplaceOrderIssuanceView,
|
||||
MarketplaceOrderIssuanceFactView,
|
||||
MarketplaceIssuanceAggregateView,
|
||||
MarketplaceIssuanceResultView,
|
||||
SignedDocumentInput,
|
||||
} from '../../OperatorIssuance/api';
|
||||
import type { MarketplaceOrderIssuanceView } from '../../OperatorIssuance/api';
|
||||
|
||||
// Финальную подпись заказчик ставит сам в своём кабинете: переиспользуем
|
||||
// запрос акта (агрегат с подписью председателя) и mutation финализации.
|
||||
export { getOrdererSignablePayload, finalizeIssuance } from '../../OperatorIssuance/api';
|
||||
|
||||
export async function listMyReadyToReceive(): Promise<MarketplaceOrderIssuanceView[]> {
|
||||
const { [Queries.Marketplace.ListMyReadyToReceive.name]: result } = await client.Query(
|
||||
Queries.Marketplace.ListMyReadyToReceive.query,
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Classes } from '@coopenomics/sdk';
|
||||
import { useGlobalStore } from 'src/shared/store';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api';
|
||||
import { BaseButton, BaseDialog } from 'src/shared/ui/base';
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
|
||||
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
|
||||
import {
|
||||
getOrdererSignablePayload,
|
||||
finalizeIssuance,
|
||||
type MarketplaceOrderIssuanceView,
|
||||
} from '../api';
|
||||
|
||||
/**
|
||||
* Story 6.3: финальная подпись заказчика — подтверждение получения имущества.
|
||||
*
|
||||
* Заказчик подтверждает получение сам в своём кабинете на своём устройстве
|
||||
* своим ключом. Он не редактирует факт: количество и стоимость уже
|
||||
* зафиксированы оператором при открытии выдачи и показаны в таблице только
|
||||
* для ознакомления. Заказчик может посмотреть акт и поставить финальную
|
||||
* подпись (signatureId=2) поверх подписи председателя — имущество переходит
|
||||
* к нему, backend исполняет корректирующие операции (`signiss2`).
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
order: MarketplaceOrderIssuanceView | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void;
|
||||
(e: 'finalized'): void;
|
||||
}>();
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const signing = ref(false);
|
||||
const previewHtml = ref<string>('');
|
||||
const previewLoading = ref(false);
|
||||
|
||||
const unitShort = computed(() =>
|
||||
props.order ? marketplaceUnitShort(props.order.unit_of_measure) : 'ед.',
|
||||
);
|
||||
|
||||
// Факт зафиксирован оператором при открытии выдачи — берём из заказа.
|
||||
const factQuantity = computed<number>(() => props.order?.issuance_fact?.actual_quantity ?? props.order?.quantity ?? 0);
|
||||
const factCost = computed<string>(() => props.order?.issuance_fact?.fact_cost ?? props.order?.total_cost ?? '0');
|
||||
const diffState = computed<string>(() => props.order?.issuance_fact?.diff_state ?? 'equal');
|
||||
|
||||
const diffHint = computed<string>(() => {
|
||||
if (!props.order) return '';
|
||||
if (diffState.value === 'equal') return 'Выдаётся ровно по заказу.';
|
||||
if (diffState.value === 'less') return 'Выдаётся меньше заказа — разница вернётся на ваш паевой остаток.';
|
||||
return 'Выдаётся больше заказа — разница спишется доплатой с вашего паевого остатка.';
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.order?.id],
|
||||
([visible]) => {
|
||||
if (visible) previewHtml.value = '';
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
async function loadPreview(): Promise<void> {
|
||||
if (!props.order) return;
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const aggregate = await getOrdererSignablePayload(props.order.id);
|
||||
previewHtml.value = aggregate.rawDocument.html;
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить акт выдачи');
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm(): Promise<void> {
|
||||
if (!props.order) return;
|
||||
const wif = globalStore.wif?.toString();
|
||||
if (!wif) {
|
||||
FailAlert(new Error('Приватный ключ не найден. Войдите в кооператив.'));
|
||||
return;
|
||||
}
|
||||
signing.value = true;
|
||||
try {
|
||||
// Backend отдаёт акт, уже подписанный председателем при открытии выдачи
|
||||
// (signatureId=1). Заказчик накладывает финальную подпись (signatureId=2)
|
||||
// поверх — документ не перегенерируется.
|
||||
const aggregate = await getOrdererSignablePayload(props.order.id);
|
||||
const signer = new Classes.Document(wif);
|
||||
const fullSigned = await signer.signDocument(
|
||||
aggregate.rawDocument,
|
||||
globalStore.username,
|
||||
2,
|
||||
[aggregate.document],
|
||||
);
|
||||
await finalizeIssuance(props.order.id, fullSigned);
|
||||
SuccessAlert('Имущество получено. Заказ закрыт.');
|
||||
emit('finalized');
|
||||
emit('update:modelValue', false);
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось подтвердить получение');
|
||||
} finally {
|
||||
signing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancel(): void {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
BaseDialog(
|
||||
:model-value="modelValue"
|
||||
title="Подтверждение получения"
|
||||
maximized
|
||||
@update:model-value="(v: boolean) => emit('update:modelValue', v)"
|
||||
)
|
||||
.mp-orderer-finalize(v-if="order")
|
||||
.text-caption.text-grey
|
||||
| Пункт выдачи: {{ [order.delivery_point_name, order.delivery_point_address].filter(Boolean).join(' · ') || order.delivery_braname }}
|
||||
|
||||
.mp-orderer-finalize__title.text-subtitle1
|
||||
| {{ order.product_name || 'Товар по предложению' }}
|
||||
|
||||
table.mp-orderer-finalize__table
|
||||
thead
|
||||
tr
|
||||
th
|
||||
th.text-right Заказ
|
||||
th.text-right К получению
|
||||
tbody
|
||||
tr
|
||||
td Количество
|
||||
td.text-right {{ order.quantity }} {{ unitShort }}
|
||||
td.text-right {{ factQuantity }} {{ unitShort }}
|
||||
tr
|
||||
td Сумма
|
||||
td.text-right {{ formatAsset2Digits(order.total_cost) }} ₽
|
||||
td.text-right {{ formatAsset2Digits(factCost) }} ₽
|
||||
|
||||
q-banner.mp-orderer-finalize__hint(rounded :class="diffState === 'more' ? 'bg-warning text-dark' : (diffState === 'less' ? 'bg-info text-white' : 'bg-positive text-white')")
|
||||
| {{ diffHint }}
|
||||
|
||||
.mp-orderer-finalize__preview-actions
|
||||
q-btn(flat no-caps icon="description" label="Показать акт" :loading="previewLoading" @click="loadPreview")
|
||||
|
||||
q-card(v-if="previewHtml" flat bordered).mp-orderer-finalize__preview
|
||||
q-card-section.q-pa-md
|
||||
div(v-html="previewHtml")
|
||||
|
||||
template(#footer)
|
||||
BaseButton(variant="ghost", :disabled="signing", @click="cancel") Отмена
|
||||
BaseButton(variant="primary", :loading="signing", @click="confirm")
|
||||
template(#icon-left)
|
||||
q-icon(name="draw", size="16px")
|
||||
| Подписать и получить
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mp-orderer-finalize {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3, 12px);
|
||||
|
||||
&__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: var(--p-2, 8px);
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-3);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+29
-1
@@ -12,6 +12,7 @@ import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
|
||||
import { encodeHandoffToken, HandoffTokenKind } from 'src/shared/lib/marketplace';
|
||||
import { listMyReadyToReceive, type MarketplaceOrderIssuanceView } from '../api';
|
||||
import OrdererFinalizeIssuanceDialog from './OrdererFinalizeIssuanceDialog.vue';
|
||||
|
||||
/**
|
||||
* Story 6.3 / FR22: orderer-стол «Готово к получению».
|
||||
@@ -33,6 +34,19 @@ const coopname = computed(() => String(route.params.coopname ?? ''));
|
||||
const items = ref<MarketplaceOrderIssuanceView[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// Финальная подпись заказчика — в своём кабинете на своём устройстве.
|
||||
const finalizeDialogOpen = ref(false);
|
||||
const selectedOrder = ref<MarketplaceOrderIssuanceView | null>(null);
|
||||
|
||||
function startFinalize(order: MarketplaceOrderIssuanceView): void {
|
||||
selectedOrder.value = order;
|
||||
finalizeDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function onFinalized(): void {
|
||||
void load();
|
||||
}
|
||||
|
||||
// Story 14.4: один account-bound код получения. Заказчик показывает его
|
||||
// оператору выдачи — тот резолвит аккаунт против ленты своего КУ и видит разом
|
||||
// все готовые к выдаче заказы этого заказчика. Код привязан к личности, не к
|
||||
@@ -90,6 +104,7 @@ const columns: QTableProps['columns'] = [
|
||||
align: 'left',
|
||||
format: (v: unknown) => formatDate(v),
|
||||
},
|
||||
{ name: 'actions', label: '', field: 'id', align: 'right' },
|
||||
];
|
||||
|
||||
async function load(): Promise<void> {
|
||||
@@ -117,7 +132,7 @@ q-page.ready(role="region", aria-label="Готово к получению")
|
||||
RefreshButton(:loading="loading", @refresh="load")
|
||||
|
||||
PageHint(storage-key="mp:ready-to-receive:banner-dismissed")
|
||||
| Оператор открыл выдачу этих заказов на пункте. Приходите на участок для сверки имущества и финальной подписи.
|
||||
| Оператор открыл выдачу этих заказов на пункте. Получите имущество на участке и подтвердите получение подписью здесь — на своём устройстве, своим ключом.
|
||||
|
||||
q-table(
|
||||
:rows="items",
|
||||
@@ -127,6 +142,13 @@ q-page.ready(role="region", aria-label="Готово к получению")
|
||||
bordered,
|
||||
:loading="loading"
|
||||
)
|
||||
template(#body-cell-actions="props")
|
||||
q-td(:props="props")
|
||||
BaseButton(variant="primary", size="sm", @click="startFinalize(props.row)")
|
||||
template(#icon-left)
|
||||
q-icon(name="draw", size="16px")
|
||||
| Подписать и получить
|
||||
|
||||
template(#no-data)
|
||||
EmptyState(
|
||||
title="Нет заказов, готовых к получению",
|
||||
@@ -135,6 +157,12 @@ q-page.ready(role="region", aria-label="Готово к получению")
|
||||
template(#icon)
|
||||
q-icon(name="inventory_2", size="48px")
|
||||
|
||||
OrdererFinalizeIssuanceDialog(
|
||||
v-model="finalizeDialogOpen",
|
||||
:order="selectedOrder",
|
||||
@finalized="onFinalized"
|
||||
)
|
||||
|
||||
BaseDialog(v-model="myCodeDialogOpen", title="Мой код получения", size="sm")
|
||||
.ready__qr(v-if="myReceiveCode")
|
||||
HandoffQr(
|
||||
|
||||
@@ -6679,13 +6679,9 @@ export type ValueTypes = {
|
||||
['...on MarketplaceExpressPickupCandidate']?: Omit<ValueTypes["MarketplaceExpressPickupCandidate"], "...on MarketplaceExpressPickupCandidate">
|
||||
}>;
|
||||
["MarketplaceFinalizeIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number | Variable<any, string>,
|
||||
/** Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком. */
|
||||
delivery_signer: string | Variable<any, string>,
|
||||
/** Идентификатор заказа, который выдаём пайщику. */
|
||||
/** Идентификатор заказа, который получаем. */
|
||||
order_id: ValueTypes["ID"] | Variable<any, string>,
|
||||
/** Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
/** Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
signed_document: ValueTypes["MarketplaceIssueActSignedDocumentInput"] | Variable<any, string>
|
||||
};
|
||||
/** Параметры запроса одного заказа. */
|
||||
@@ -7082,6 +7078,8 @@ export type ValueTypes = {
|
||||
['...on MarketplaceOnboardingState']?: Omit<ValueTypes["MarketplaceOnboardingState"], "...on MarketplaceOnboardingState">
|
||||
}>;
|
||||
["MarketplaceOpenIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number | Variable<any, string>,
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
order_id: ValueTypes["ID"] | Variable<any, string>,
|
||||
/** Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись. */
|
||||
@@ -16761,13 +16759,9 @@ export type ResolverInputTypes = {
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["MarketplaceFinalizeIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком. */
|
||||
delivery_signer: string,
|
||||
/** Идентификатор заказа, который выдаём пайщику. */
|
||||
/** Идентификатор заказа, который получаем. */
|
||||
order_id: ResolverInputTypes["ID"],
|
||||
/** Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
/** Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
signed_document: ResolverInputTypes["MarketplaceIssueActSignedDocumentInput"]
|
||||
};
|
||||
/** Параметры запроса одного заказа. */
|
||||
@@ -17153,6 +17147,8 @@ export type ResolverInputTypes = {
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["MarketplaceOpenIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
order_id: ResolverInputTypes["ID"],
|
||||
/** Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись. */
|
||||
@@ -26517,13 +26513,9 @@ export type ModelTypes = {
|
||||
total_units: number
|
||||
};
|
||||
["MarketplaceFinalizeIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком. */
|
||||
delivery_signer: string,
|
||||
/** Идентификатор заказа, который выдаём пайщику. */
|
||||
/** Идентификатор заказа, который получаем. */
|
||||
order_id: ModelTypes["ID"],
|
||||
/** Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
/** Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
signed_document: ModelTypes["MarketplaceIssueActSignedDocumentInput"]
|
||||
};
|
||||
/** Параметры запроса одного заказа. */
|
||||
@@ -26897,6 +26889,8 @@ export type ModelTypes = {
|
||||
template_registry_id: number
|
||||
};
|
||||
["MarketplaceOpenIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
order_id: ModelTypes["ID"],
|
||||
/** Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись. */
|
||||
@@ -37189,13 +37183,9 @@ export type GraphQLTypes = {
|
||||
['...on MarketplaceExpressPickupCandidate']: Omit<GraphQLTypes["MarketplaceExpressPickupCandidate"], "...on MarketplaceExpressPickupCandidate">
|
||||
};
|
||||
["MarketplaceFinalizeIssuanceInput"]: {
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Учётная запись стороны кооператива, подписывающей акт вместе с заказчиком. */
|
||||
delivery_signer: string,
|
||||
/** Идентификатор заказа, который выдаём пайщику. */
|
||||
/** Идентификатор заказа, который получаем. */
|
||||
order_id: GraphQLTypes["ID"],
|
||||
/** Подписанный заказчиком и стороной кооператива акт выдачи. Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
/** Подписанный заказчиком акт выдачи (поверх подписи председателя). Backend верифицирует подписи и отправляет on-chain финальную подпись со всеми корректирующими операциями. */
|
||||
signed_document: GraphQLTypes["MarketplaceIssueActSignedDocumentInput"]
|
||||
};
|
||||
/** Параметры запроса одного заказа. */
|
||||
@@ -37592,7 +37582,9 @@ export type GraphQLTypes = {
|
||||
['...on MarketplaceOnboardingState']: Omit<GraphQLTypes["MarketplaceOnboardingState"], "...on MarketplaceOnboardingState">
|
||||
};
|
||||
["MarketplaceOpenIssuanceInput"]: {
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
/** Фактически выдаваемое количество единиц (равно/меньше/больше заказа). */
|
||||
actual_quantity: number,
|
||||
/** Идентификатор заказа, выдачу которого открываем. */
|
||||
order_id: GraphQLTypes["ID"],
|
||||
/** Подписанный председателем кооперативного участка акт выдачи. Backend верифицирует подпись и отправляет on-chain первую подпись. */
|
||||
signed_document: GraphQLTypes["MarketplaceIssueActSignedDocumentInput"]
|
||||
|
||||
Reference in New Issue
Block a user