[598-30][@ant] feat: батч-акцепт сводной заявки и триггер open_subscription (SDK + desktop + aggregator) — чтобы поставщик принимал накопленный объём одной кнопкой и запускал цикл подписки
This commit is contained in:
+48
@@ -489,9 +489,57 @@ export class MarketplaceCycleAggregatorService {
|
||||
this.logger.log(
|
||||
`MarketplaceCycleAggregatorService: offer=${offer_id} cycle_type=${cycle_type} → consolidated_request=${cycle.id} status=${status}; assigned ${affected}/${orderIds.length} Order'ов`
|
||||
);
|
||||
|
||||
// open_subscription: триггер поставщика — это одновременно создание и
|
||||
// акцепт заявки. БД уже помечена ACCEPTED, но on-chain Order остаётся в
|
||||
// статусе блокировки. Без on-chain `acceptOrder` последующий `signsupp`
|
||||
// на акте приёмки падает ассертом «Заказ не в статусе акцепта». Для
|
||||
// time_based/volume_based on-chain accept делает отдельный шаг
|
||||
// marketplaceAcceptConsolidatedRequest; для triggered-пути выполняем его
|
||||
// здесь, иначе магистраль open_subscription блокируется на приёмке.
|
||||
if (triggered) {
|
||||
await this.acceptPoolOnChain(pool, supplier_account);
|
||||
}
|
||||
|
||||
return cycle;
|
||||
}
|
||||
|
||||
/**
|
||||
* open_subscription: per-Order on-chain `acceptOrder` (offerer = поставщик).
|
||||
* Best-effort, симметрично expirePoolOnChain: chain fail одного Order'а —
|
||||
* лог error + продолжаем; Order остаётся ACCEPTED в БД (assignToCycle уже
|
||||
* прошёл), рассинхрон чинится manual reconciliation / повторным запуском.
|
||||
*/
|
||||
private async acceptPoolOnChain(
|
||||
pool: MarketplaceOrderDomainEntity[],
|
||||
offerer_account: string
|
||||
): Promise<{ succeeded: number; failed: number }> {
|
||||
let succeeded = 0;
|
||||
let failed = 0;
|
||||
for (const order of pool) {
|
||||
try {
|
||||
await this.chainPort.acceptOrder({
|
||||
coopname: order.coopname,
|
||||
offerer: offerer_account,
|
||||
order_hash: order.order_hash,
|
||||
});
|
||||
succeeded++;
|
||||
} catch (chainErr: any) {
|
||||
failed++;
|
||||
this.logger.error(
|
||||
`MarketplaceCycleAggregatorService.acceptPoolOnChain: chain.acceptOrder упал для Order ${order.id} (order_hash=${order.order_hash}, offerer=${offerer_account}): ${chainErr.message}. Order остаётся ACCEPTED в БД, on-chain accept не прошёл — повтор через manual reconciliation.`,
|
||||
chainErr.stack
|
||||
);
|
||||
}
|
||||
}
|
||||
if (failed > 0) {
|
||||
this.logger.warn(
|
||||
`MarketplaceCycleAggregatorService.acceptPoolOnChain: завершено для пула из ${pool.length} Order'ов (succeeded=${succeeded}, failed=${failed})`
|
||||
);
|
||||
}
|
||||
return { succeeded, failed };
|
||||
}
|
||||
|
||||
private computeTotalAmount(price_per_unit: string, quantity: number): string {
|
||||
const price = Number.parseFloat(price_per_unit);
|
||||
if (!Number.isFinite(price)) return '0.0000';
|
||||
|
||||
@@ -57,3 +57,19 @@ export async function declineIndividualOrder(order_id: string, reason: string):
|
||||
variables: { input: { order_id, reason } },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Групповой (volume_based/open_subscription/time_based) акцепт: поставщик
|
||||
* принимает всю сводную заявку партии разом. request_id = cycle_id заказа.
|
||||
*/
|
||||
export async function acceptConsolidatedRequest(request_id: string): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.AcceptConsolidatedRequest.mutation, {
|
||||
variables: { input: { request_id } },
|
||||
});
|
||||
}
|
||||
|
||||
export async function declineConsolidatedRequest(request_id: string, reason: string): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.DeclineConsolidatedRequest.mutation, {
|
||||
variables: { input: { request_id, reason } },
|
||||
});
|
||||
}
|
||||
|
||||
+33
-5
@@ -6,7 +6,13 @@ import {
|
||||
type Order as OrderCardModel,
|
||||
type OrderStatus as OrderCardStatus,
|
||||
} from 'src/widgets/Marketplace/OrderCard';
|
||||
import { acceptIndividualOrder, declineIndividualOrder, fetchSupplierOrders } from '../api';
|
||||
import {
|
||||
acceptConsolidatedRequest,
|
||||
acceptIndividualOrder,
|
||||
declineConsolidatedRequest,
|
||||
declineIndividualOrder,
|
||||
fetchSupplierOrders,
|
||||
} from '../api';
|
||||
import type {
|
||||
MarketplaceOrderStatusView,
|
||||
MarketplaceOrderView,
|
||||
@@ -111,8 +117,21 @@ function loadMore(): void {
|
||||
async function onAccept(orderId: string): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
await acceptIndividualOrder(orderId);
|
||||
Notify.create({ type: 'positive', message: 'Заказ принят', timeout: 4000 });
|
||||
// Индивидуальный заказ акцептуется поштучно (order_id); групповой
|
||||
// (cycle_type volume_based/open_subscription/time_based) — целиком сводной
|
||||
// заявкой по cycle_id. Иначе acceptIndividualOrder на групповом заказе
|
||||
// падает: заказ привязан к сводной заявке, а не к индивидуальному циклу.
|
||||
const order = items.value.find((o) => o.id === orderId);
|
||||
if (order && order.status === 'ACCEPTED_PENDING_SUPPLIER') {
|
||||
if (!order.cycle_id) {
|
||||
throw new Error('У группового заказа отсутствует идентификатор сводной заявки (cycle_id).');
|
||||
}
|
||||
await acceptConsolidatedRequest(order.cycle_id);
|
||||
Notify.create({ type: 'positive', message: 'Сводная заявка партии принята', timeout: 4000 });
|
||||
} else {
|
||||
await acceptIndividualOrder(orderId);
|
||||
Notify.create({ type: 'positive', message: 'Заказ принят', timeout: 4000 });
|
||||
}
|
||||
await load(1, false);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
@@ -133,8 +152,17 @@ function onDecline(orderId: string): void {
|
||||
}).onOk(async (reason: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await declineIndividualOrder(orderId, reason.trim());
|
||||
Notify.create({ type: 'info', message: 'Заказ отклонён', timeout: 4000 });
|
||||
const order = items.value.find((o) => o.id === orderId);
|
||||
if (order && order.status === 'ACCEPTED_PENDING_SUPPLIER') {
|
||||
if (!order.cycle_id) {
|
||||
throw new Error('У группового заказа отсутствует идентификатор сводной заявки (cycle_id).');
|
||||
}
|
||||
await declineConsolidatedRequest(order.cycle_id, reason.trim());
|
||||
Notify.create({ type: 'info', message: 'Сводная заявка партии отклонена', timeout: 4000 });
|
||||
} else {
|
||||
await declineIndividualOrder(orderId, reason.trim());
|
||||
Notify.create({ type: 'info', message: 'Заказ отклонён', timeout: 4000 });
|
||||
}
|
||||
await load(1, false);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { marketplaceConsolidatedRequestActionResultSelector } from '../../selectors/marketplace/consolidatedRequestSelector'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'marketplaceAcceptConsolidatedRequest'
|
||||
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [
|
||||
{ input: $('input', 'MarketplaceAcceptConsolidatedRequestInput!') },
|
||||
marketplaceConsolidatedRequestActionResultSelector,
|
||||
],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
input: ModelTypes['MarketplaceAcceptConsolidatedRequestInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { marketplaceConsolidatedRequestActionResultSelector } from '../../selectors/marketplace/consolidatedRequestSelector'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'marketplaceDeclineConsolidatedRequest'
|
||||
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [
|
||||
{ input: $('input', 'MarketplaceDeclineConsolidatedRequestInput!') },
|
||||
marketplaceConsolidatedRequestActionResultSelector,
|
||||
],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
input: ModelTypes['MarketplaceDeclineConsolidatedRequestInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -12,6 +12,12 @@ export * as CancelOrder from './cancelOrder'
|
||||
export * as AcceptIndividualOrder from './acceptIndividualOrder'
|
||||
/** Эпик 4: поставщик отклоняет индивидуальный заказ с указанием причины */
|
||||
export * as DeclineIndividualOrder from './declineIndividualOrder'
|
||||
/** Эпик 4: поставщик принимает сводную заявку партии (групповой cycle_type) */
|
||||
export * as AcceptConsolidatedRequest from './acceptConsolidatedRequest'
|
||||
/** Эпик 4: поставщик отклоняет сводную заявку партии — заказы отменяются, средства разблокируются */
|
||||
export * as DeclineConsolidatedRequest from './declineConsolidatedRequest'
|
||||
/** Эпик 4: поставщик вручную запускает поставку по предложению с открытой подпиской (cycle_type=open_subscription) */
|
||||
export * as TriggerOpenSubscription from './triggerOpenSubscription'
|
||||
/** Опубликовать новое предложение поставщика на модерацию */
|
||||
export * as CreateOffer from './createOffer'
|
||||
/** Эпик 5: сформировать партии поставки из акцептованной заявки */
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { marketplaceConsolidatedRequestSelector } from '../../selectors/marketplace/consolidatedRequestSelector'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'marketplaceTriggerOpenSubscription'
|
||||
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [
|
||||
{ input: $('input', 'MarketplaceTriggerOpenSubscriptionInput!') },
|
||||
marketplaceConsolidatedRequestSelector,
|
||||
],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
input: ModelTypes['MarketplaceTriggerOpenSubscriptionInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
|
||||
const rawConsolidatedRequestSelector = {
|
||||
id: true,
|
||||
coopname: true,
|
||||
offer_id: true,
|
||||
supplier_account: true,
|
||||
cycle_type: true,
|
||||
status: true,
|
||||
total_quantity: true,
|
||||
total_amount: true,
|
||||
cycle_started_at: true,
|
||||
cycle_ended_at: true,
|
||||
expires_at: true,
|
||||
accepted_at: true,
|
||||
declined_at: true,
|
||||
decline_reason: true,
|
||||
triggered_by_supplier_at: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
}
|
||||
|
||||
const _validateConsolidatedRequest: MakeAllFieldsRequired<
|
||||
ValueTypes['MarketplaceConsolidatedRequest']
|
||||
> = rawConsolidatedRequestSelector
|
||||
|
||||
export const marketplaceConsolidatedRequestSelector = Selector('MarketplaceConsolidatedRequest')(
|
||||
rawConsolidatedRequestSelector,
|
||||
)
|
||||
|
||||
export const marketplaceConsolidatedRequestActionResultSelector = Selector(
|
||||
'MarketplaceConsolidatedRequestActionResult',
|
||||
)({
|
||||
request: rawConsolidatedRequestSelector,
|
||||
affected_orders: true,
|
||||
on_chain_succeeded: true,
|
||||
on_chain_failed: true,
|
||||
})
|
||||
Reference in New Issue
Block a user