returnByMoneyDecision Document

This commit is contained in:
Alex Ant
2025-06-25 20:34:12 +05:00
parent 3a21af48d1
commit 2a263d749b
92 changed files with 4212 additions and 281 deletions
+1 -1
View File
@@ -1,9 +1,9 @@
{
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"editor.tabCompletion": "onlySnippets",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.addMissingImports.ts": "explicit",
"source.fixAll.ts": "explicit",
BIN
View File
Binary file not shown.
+3
View File
@@ -114,6 +114,9 @@ static const std::set<eosio::name> soviet_actions = {
"capwthdrprog"_n, //произвести возврат накопленных членских взносов по программе на капиталиста
"capwthdrproj"_n, //произвести возврат накопленных членских взносов по проекту на актора
"capwthdrres"_n, //произвести возврат из задания
//WALLET
"createwthd"_n, //создать заявление на возврат паевого взноса
};
static const std::set<eosio::name> gateway_income_actions = {
@@ -14,7 +14,7 @@ void wallet::authwthd(eosio::name coopname, checksum256 withdraw_hash) {
});
// создаём объект исходящего платежа в gateway с коллбэком после обработки
action(permission_level{ _capital, "active"_n}, _gateway, "createoutpay"_n,
action(permission_level{ _wallet, "active"_n}, _gateway, "createoutpay"_n,
std::make_tuple(
coopname,
withdraw -> username,
@@ -28,22 +28,20 @@ void wallet::createwthd(eosio::name coopname, eosio::name username, checksum256
d.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
//TODO: перевести approve на hash-идентификатор???
action(
permission_level{_capital, "active"_n}, // кто вызывает
Action::send<createagenda_interface>(
_soviet,
"createapprv"_n,
std::make_tuple(
coopname,
username,
statement,
withdraw_hash,
_wallet, // callback_contract (текущий контракт)
"approvewthd"_n, // callback_action_approve
"declinewthd"_n, // callback_action_decline
std::string("")
)
).send();
"createagenda"_n,
_wallet,
coopname,
username,
get_valid_soviet_action("createwthd"_n),
withdraw_hash,
_wallet, // callback_contract (текущий контракт)
"approvewthd"_n, // callback_action_approve
"declinewthd"_n, // callback_action_decline
statement,
std::string("")
);
std::string memo_in = "Возврат части целевого паевого взноса по ЦПП 'Цифровой Кошелёк'";
Wallet::block_funds(_wallet, coopname, username, quantity, _wallet_program, memo_in);
+302
View File
@@ -1640,6 +1640,34 @@ input CreateProjectFreeDecisionInput {
question: String!
}
input CreateWithdrawInput {
"""Имя аккаунта кооператива"""
coopname: String!
"""Дополнительная информация"""
memo: String
"""ID метода платежа"""
method_id: String!
"""Количество средств"""
quantity: String!
"""Подписанное заявление на возврат средств"""
statement: SignedDigitalDocumentInput!
"""Символ валюты"""
symbol: String!
"""Имя пользователя"""
username: String!
}
type CreateWithdrawResponse {
"""Хеш созданной заявки на вывод"""
withdraw_hash: String!
}
type CreatedProjectFreeDecision {
"""Проект решения, которое предлагается принять"""
decision: String!
@@ -2070,6 +2098,68 @@ input FreeDecisionGenerateDocumentInput {
version: String
}
type GatewayPayment {
"""Данные из блокчейна"""
blockchain_data: JSON
"""Может ли статус быть изменен"""
can_change_status: Boolean!
"""Название кооператива"""
coopname: String!
"""Дата создания"""
created_at: DateTime!
"""Форматированная сумма"""
formatted_amount: String!
"""Универсальный хеш платежа"""
hash: String!
"""Уникальный идентификатор платежа"""
id: ID
"""Хеш входящего платежа"""
income_hash: String
"""Дополнительная информация"""
memo: String
"""ID платежного метода"""
method_id: String!
"""Хеш исходящего платежа"""
outcome_hash: String
"""Детали платежа"""
payment_details: String
"""Количество"""
quantity: String!
"""Статус платежа"""
status: gatewayPaymentStatus!
"""Человекочитаемый статус"""
status_label: String!
"""Символ валюты"""
symbol: String!
"""Тип платежа: входящий или исходящий"""
type: gatewayPaymentType!
"""Человекочитаемый тип платежа"""
type_label: String!
"""Дата последнего обновления"""
updated_at: DateTime
"""Имя пользователя"""
username: String!
}
input GenerateDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int
@@ -2182,6 +2272,12 @@ input GetMeetsInput {
coopname: String!
}
input GetOutgoingPaymentsInput {
coopname: String
status: String
username: String
}
input GetPaymentMethodsInput {
"""Количество элементов на странице"""
limit: Int! = 10
@@ -2681,6 +2777,9 @@ type Mutation {
"""
createProjectOfFreeDecision(data: CreateProjectFreeDecisionInput!): CreatedProjectFreeDecision!
"""Создать заявку на вывод средств"""
createWithdraw(input: CreateWithdrawInput!): CreateWithdrawResponse!
"""Отклонить заявку"""
declineRequest(data: DeclineRequestInput!): Transaction!
@@ -2749,6 +2848,12 @@ type Mutation {
"""Сгенерировать документ заявления о возврате имущества."""
generateReturnByAssetStatement(data: ReturnByAssetStatementGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать документ решения совета о возврате паевого взноса"""
generateReturnByMoneyDecisionDocument(data: ReturnByMoneyDecisionGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать документ заявления на возврат паевого взноса"""
generateReturnByMoneyStatementDocument(data: ReturnByMoneyGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать документ, подтверждающий выбор кооперативного участка"""
generateSelectBranchDocument(data: SelectBranchGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
@@ -2872,6 +2977,9 @@ type Mutation {
"""Обновить расширение"""
updateExtension(data: ExtensionInput!): Extension!
"""Обновить статус платежа."""
updatePaymentStatus(input: UpdatePaymentStatusInput!): OutgoingPayment!
"""Обновить заявку"""
updateRequest(data: UpdateRequestInput!): Transaction!
@@ -2975,6 +3083,73 @@ enum OrganizationType {
ZAO
}
type OutgoingPayment {
"""Данные из блокчейна"""
blockchain_data: JSON
"""Может ли статус быть изменен"""
can_change_status: Boolean!
"""Название кооператива"""
coopname: String!
"""Дата создания"""
created_at: DateTime!
"""Форматированная сумма"""
formatted_amount: String!
"""Хеш исходящего платежа"""
hash: String!
"""Уникальный идентификатор платежа"""
id: ID
"""Дополнительная информация"""
memo: String
"""ID платежного метода"""
method_id: String!
"""Детали платежа"""
payment_details: String
"""Количество"""
quantity: String!
"""Статус платежа"""
status: gatewayPaymentStatus!
"""Человекочитаемый статус"""
status_label: String!
"""Символ валюты"""
symbol: String!
"""Тип платежа (всегда outgoing)"""
type: gatewayPaymentType!
"""Дата последнего обновления"""
updated_at: DateTime
"""Имя пользователя"""
username: String!
}
type PaginatedGatewayPaymentsPaginationResult {
"""Текущая страница"""
currentPage: Int!
"""Элементы текущей страницы"""
items: [GatewayPayment!]!
"""Общее количество элементов"""
totalCount: Int!
"""Общее количество страниц"""
totalPages: Int!
}
input PaginationInput {
"""Количество элементов на странице"""
limit: Int! = 10
@@ -3538,6 +3713,11 @@ type Query {
"""Получить список расширений"""
getExtensions(data: GetExtensionsInput): [Extension!]!
"""
Получить список всех платежей (универсальный метод, поддержка входящих платежей в будущем).
"""
getGatewayPayments(filters: GetOutgoingPaymentsInput!, options: PaginationInput!): PaginatedGatewayPaymentsPaginationResult!
"""Получить данные собрания по хешу"""
getMeet(data: GetMeetInput!): MeetAggregate!
@@ -3749,6 +3929,17 @@ input RepresentedByInput {
position: String!
}
input RequestInput {
"""Сумма к возврату"""
amount: String!
"""Валюта"""
currency: String!
"""ID платежного метода"""
method_id: String!
}
input ResetKeyInput {
"""Публичный ключ для замены"""
public_key: String!
@@ -4051,6 +4242,85 @@ input ReturnByAssetStatementSignedMetaDocumentInput {
version: String!
}
input ReturnByMoneyDecisionGenerateDocumentInput {
"""Сумма к возврату"""
amount: String!
"""Номер блока, на котором был создан документ"""
block_num: Int
"""Название кооператива, связанное с документом"""
coopname: String!
"""Дата и время создания документа"""
created_at: String
"""Валюта"""
currency: String!
"""ID решения совета"""
decision_id: Float!
"""Имя генератора, использованного для создания документа"""
generator: String
"""Язык документа"""
lang: String
"""Ссылки, связанные с документом"""
links: [String!]
"""Хэш платежа"""
payment_hash: String!
"""Часовой пояс, в котором был создан документ"""
timezone: String
"""Название документа"""
title: String
"""Имя пользователя, создавшего документ"""
username: String!
"""Версия генератора, использованного для создания документа"""
version: String
}
input ReturnByMoneyGenerateDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int
"""Название кооператива, связанное с документом"""
coopname: String!
"""Дата и время создания документа"""
created_at: String
"""Имя генератора, использованного для создания документа"""
generator: String
"""Язык документа"""
lang: String
"""Ссылки, связанные с документом"""
links: [String!]
"""Данные запроса на возврат денежных средств"""
request: RequestInput!
"""Часовой пояс, в котором был создан документ"""
timezone: String
"""Название документа"""
title: String
"""Имя пользователя, создавшего документ"""
username: String!
"""Версия генератора, использованного для создания документа"""
version: String
}
type SbpAccount {
"""Мобильный телефон получателя"""
phone: String!
@@ -4572,6 +4842,23 @@ input UpdateOrganizationDataInput {
username: String!
}
input UpdatePaymentStatusInput {
"""Название кооператива"""
coopname: String!
"""Универсальный хеш платежа"""
hash: String!
"""Причина изменения статуса (для статуса failed)"""
reason: String
"""Новый статус платежа"""
status: String!
"""Тип платежа"""
type: String!
}
input UpdateRequestInput {
"""Имя аккаунта кооператива"""
coopname: String!
@@ -4734,4 +5021,19 @@ type WaitWeight {
"""Вес"""
weight: Int!
}
"""Статус платежа в системе Gateway"""
enum gatewayPaymentStatus {
CANCELLED
COMPLETED
FAILED
PENDING
PROCESSING
}
"""Тип платежа в системе Gateway"""
enum gatewayPaymentType {
INCOMING
OUTGOING
}
+1 -2
View File
@@ -1,6 +1,6 @@
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerDefinition from '../src/docs/swaggerDef';
import fs from 'fs'
import fs from 'fs';
import path from 'path';
const specs = swaggerJsdoc({
@@ -8,5 +8,4 @@ const specs = swaggerJsdoc({
apis: ['src/docs/*.yml', 'src/**/*.ts'],
});
fs.writeFileSync(path.join(__dirname, '../src/docs/swagger.json'), JSON.stringify(specs, null, 2));
@@ -68,6 +68,18 @@ const envVarsSchema = z.object({
REDIS_PASSWORD: z.string(),
BLOCKCHAIN_RPC: z.string().min(1, { message: 'Не должно быть пустым' }),
CHAIN_ID: z.string().min(1, { message: 'Не должно быть пустым' }),
// Параметры блокчейна
ROOT_SYMBOL: z.string().default('AXON'),
ROOT_GOVERN_SYMBOL: z.string().default('RUB'),
ROOT_PRECISION: z
.string()
.default('4')
.transform((val) => parseInt(val, 10)),
ROOT_GOVERN_PRECISION: z
.string()
.default('4')
.transform((val) => parseInt(val, 10)),
});
// Валидация переменных окружения
@@ -95,6 +107,10 @@ export default {
blockchain: {
url: envVars.data.BLOCKCHAIN_RPC,
id: envVars.data.CHAIN_ID,
root_symbol: envVars.data.ROOT_SYMBOL,
root_govern_symbol: envVars.data.ROOT_GOVERN_SYMBOL,
root_precision: envVars.data.ROOT_PRECISION,
root_govern_precision: envVars.data.ROOT_GOVERN_PRECISION,
},
mongoose: {
url: envVars.data.MONGODB_URL + (envVars.data.NODE_ENV === 'test' ? '-test' : ''),
@@ -18,6 +18,8 @@ import { DesktopDomainModule } from './desktop/desktop-domain.module';
import { MeetDomainModule } from './meet/meet-domain.module';
import { ControllerWsMeetModule } from './meet/controllers-ws-meet.module';
import { UserCertificateDomainModule } from './user-certificate/user-certificate.module';
import { GatewayDomainModule } from './gateway/gateway-domain.module';
import { WalletDomainModule } from './wallet/wallet-domain.module';
@Module({
imports: [
@@ -39,6 +41,8 @@ import { UserCertificateDomainModule } from './user-certificate/user-certificate
MeetDomainModule,
ControllerWsMeetModule,
UserCertificateDomainModule,
GatewayDomainModule,
WalletDomainModule,
],
exports: [
AuthDomainModule,
@@ -58,6 +62,8 @@ import { UserCertificateDomainModule } from './user-certificate/user-certificate
CooplaceDomainModule,
MeetDomainModule,
UserCertificateDomainModule,
GatewayDomainModule,
WalletDomainModule,
],
})
export class DomainModule {}
@@ -0,0 +1,93 @@
import type { OutgoingPaymentDomainInterface } from '../interfaces/outgoing-payment-domain.interface';
import { GatewayPaymentStatusEnum } from '../enums/gateway-payment-status.enum';
import { GatewayPaymentTypeEnum } from '../enums/gateway-payment-type.enum';
/**
* Доменная сущность исходящего платежа
*/
export class OutgoingPaymentDomainEntity implements OutgoingPaymentDomainInterface {
id?: string;
coopname: string;
username: string;
hash: string;
quantity: string;
symbol: string;
method_id: string;
status: GatewayPaymentStatusEnum;
type: GatewayPaymentTypeEnum.OUTGOING = GatewayPaymentTypeEnum.OUTGOING;
created_at: Date;
updated_at?: Date;
memo?: string;
payment_details?: string;
blockchain_data?: any;
constructor(data: OutgoingPaymentDomainInterface) {
this.id = data.id;
this.coopname = data.coopname;
this.username = data.username;
this.hash = data.hash;
this.quantity = data.quantity;
this.symbol = data.symbol;
this.method_id = data.method_id;
this.status = data.status as GatewayPaymentStatusEnum;
this.created_at = data.created_at;
this.updated_at = data.updated_at;
this.memo = data.memo;
this.payment_details = data.payment_details;
this.blockchain_data = data.blockchain_data;
this.type = GatewayPaymentTypeEnum.OUTGOING;
}
/**
* Получить количество и символ в формате строки
*/
getFormattedAmount(): string {
return `${this.quantity} ${this.symbol}`;
}
/**
* Проверить, может ли статус быть изменен
*/
canChangeStatus(): boolean {
return this.status !== GatewayPaymentStatusEnum.COMPLETED && this.status !== GatewayPaymentStatusEnum.CANCELLED;
}
/**
* Получить человекочитаемый статус
*/
getStatusLabel(): string {
const statusLabels: Record<GatewayPaymentStatusEnum, string> = {
[GatewayPaymentStatusEnum.PENDING]: 'Ожидает обработки',
[GatewayPaymentStatusEnum.PROCESSING]: 'В процессе',
[GatewayPaymentStatusEnum.COMPLETED]: 'Завершен',
[GatewayPaymentStatusEnum.FAILED]: 'Ошибка',
[GatewayPaymentStatusEnum.CANCELLED]: 'Отменен',
};
return statusLabels[this.status] || this.status;
}
/**
* Преобразовать в DTO для GraphQL
*/
toDTO(): any {
return {
id: this.id,
coopname: this.coopname,
username: this.username,
hash: this.hash,
quantity: this.quantity,
symbol: this.symbol,
method_id: this.method_id,
status: this.status,
type: this.type,
created_at: this.created_at,
updated_at: this.updated_at,
memo: this.memo,
payment_details: this.payment_details,
blockchain_data: this.blockchain_data,
formatted_amount: this.getFormattedAmount(),
status_label: this.getStatusLabel(),
can_change_status: this.canChangeStatus(),
};
}
}
@@ -0,0 +1,116 @@
import type { PaymentDomainInterface } from '../interfaces/payment-domain.interface';
/**
* Универсальная доменная сущность платежа
* Работает как для входящих, так и для исходящих платежей
*/
export class PaymentDomainEntity implements PaymentDomainInterface {
id?: string;
coopname: string;
username: string;
hash: string;
quantity: string;
symbol: string;
method_id: string;
status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
type: 'incoming' | 'outgoing';
created_at: Date;
updated_at?: Date;
memo?: string;
payment_details?: string;
blockchain_data?: any;
constructor(data: PaymentDomainInterface) {
this.id = data.id;
this.coopname = data.coopname;
this.username = data.username;
this.hash = data.hash;
this.quantity = data.quantity;
this.symbol = data.symbol;
this.method_id = data.method_id;
this.status = data.status;
this.type = data.type;
this.created_at = data.created_at;
this.updated_at = data.updated_at;
this.memo = data.memo;
this.payment_details = data.payment_details;
this.blockchain_data = data.blockchain_data;
}
/**
* Получить количество и символ в формате строки
*/
getFormattedAmount(): string {
return `${this.quantity} ${this.symbol}`;
}
/**
* Проверить, может ли статус быть изменен
*/
canChangeStatus(): boolean {
return this.status !== 'completed' && this.status !== 'cancelled';
}
/**
* Получить человекочитаемый статус
*/
getStatusLabel(): string {
const statusLabels = {
pending: 'Ожидает обработки',
processing: 'В процессе',
completed: 'Завершен',
failed: 'Ошибка',
cancelled: 'Отменен',
};
return statusLabels[this.status] || this.status;
}
/**
* Получить человекочитаемый тип платежа
*/
getTypeLabel(): string {
const typeLabels = {
incoming: 'Входящий платеж',
outgoing: 'Исходящий платеж',
};
return typeLabels[this.type] || this.type;
}
/**
* Получить соответствующий хеш для типа платежа
*/
getSpecificHash(): { [key: string]: string } {
if (this.type === 'outgoing') {
return { outcome_hash: this.hash };
} else {
return { income_hash: this.hash };
}
}
/**
* Преобразовать в DTO для GraphQL
*/
toDTO(): any {
return {
id: this.id,
coopname: this.coopname,
username: this.username,
hash: this.hash,
...this.getSpecificHash(), // Добавляем конкретный хеш
quantity: this.quantity,
symbol: this.symbol,
method_id: this.method_id,
status: this.status,
type: this.type,
created_at: this.created_at,
updated_at: this.updated_at,
memo: this.memo,
payment_details: this.payment_details,
blockchain_data: this.blockchain_data,
formatted_amount: this.getFormattedAmount(),
status_label: this.getStatusLabel(),
type_label: this.getTypeLabel(),
can_change_status: this.canChangeStatus(),
};
}
}
@@ -0,0 +1,10 @@
/**
* Статус платежа в системе Gateway
*/
export enum GatewayPaymentStatusEnum {
PENDING = 'pending',
PROCESSING = 'processing',
COMPLETED = 'completed',
FAILED = 'failed',
CANCELLED = 'cancelled',
}
@@ -0,0 +1,7 @@
/**
* Тип платежа в системе Gateway
*/
export enum GatewayPaymentTypeEnum {
INCOMING = 'incoming',
OUTGOING = 'outgoing',
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { GatewayInteractor } from './interactors/gateway.interactor';
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
@Module({
imports: [InfrastructureModule],
providers: [GatewayInteractor],
exports: [GatewayInteractor],
})
export class GatewayDomainModule {}
@@ -0,0 +1,101 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import type { OutgoingPaymentDomainInterface } from '../interfaces/outgoing-payment-domain.interface';
import type { UpdatePaymentStatusInputDomainInterface } from '../interfaces/update-payment-status-input-domain.interface';
import type { GetOutgoingPaymentsInputDomainInterface } from '../interfaces/get-outgoing-payments-input-domain.interface';
import type {
PaginationInputDomainInterface,
PaginationResultDomainInterface,
} from '~/domain/common/interfaces/pagination.interface';
import { OutgoingPaymentDomainEntity } from '../entities/outgoing-payment-domain.entity';
import { GatewayBlockchainPort, GATEWAY_BLOCKCHAIN_PORT } from '../ports/gateway-blockchain.port';
import { PaymentRepository, PAYMENT_REPOSITORY } from '../repositories/payment.repository';
/**
* Интерактор домена gateway для управления исходящими платежами
*/
@Injectable()
export class GatewayInteractor {
private readonly logger = new Logger(GatewayInteractor.name);
constructor(
@Inject(GATEWAY_BLOCKCHAIN_PORT)
private readonly gatewayBlockchainPort: GatewayBlockchainPort,
@Inject(PAYMENT_REPOSITORY)
private readonly paymentRepository: PaymentRepository
) {}
/**
* Получить список исходящих платежей
*/
async getOutgoingPayments(
filters: GetOutgoingPaymentsInputDomainInterface,
options: PaginationInputDomainInterface
): Promise<PaginationResultDomainInterface<OutgoingPaymentDomainEntity>> {
// Получаем данные из локальной БД через репозиторий
const result = await this.paymentRepository.getOutgoingPayments(filters, options);
// Обогащаем каждый платеж данными из блокчейна
const enrichedItems = await Promise.all(
result.items.map(async (payment) => {
try {
// Получаем данные из блокчейна
const blockchainData = await this.gatewayBlockchainPort.getOutcome(payment.coopname, payment.hash);
// Создаем доменную сущность с обогащенными данными
const enrichedPayment = new OutgoingPaymentDomainEntity({
...payment,
blockchain_data: blockchainData,
});
return enrichedPayment;
} catch (error: any) {
this.logger.warn(`Не удалось получить данные из блокчейна для платежа ${payment.hash}: ${error.message}`);
// Возвращаем платеж без данных блокчейна
return new OutgoingPaymentDomainEntity(payment);
}
})
);
return {
items: enrichedItems,
totalCount: result.totalCount,
totalPages: result.totalPages,
currentPage: result.currentPage,
};
}
/**
* Обновить статус платежа
*/
async updatePaymentStatus(data: UpdatePaymentStatusInputDomainInterface): Promise<OutgoingPaymentDomainEntity> {
try {
if (data.status === 'completed') {
// Завершаем платеж в gateway
await this.gatewayBlockchainPort.completeOutcome({
coopname: data.coopname,
outcome_hash: data.hash,
});
this.logger.log(`Платеж ${data.hash} завершен в блокчейне`);
} else if (data.status === 'failed') {
// Отклоняем платеж в gateway
await this.gatewayBlockchainPort.declineOutcome({
coopname: data.coopname,
outcome_hash: data.hash,
reason: data.reason || 'Платеж отклонен',
});
this.logger.log(`Платеж ${data.hash} отклонен в блокчейне`);
}
// Обновляем статус в локальной БД через репозиторий
const updatedPayment = await this.paymentRepository.updatePaymentStatus(data.hash, data.status, data.reason);
if (!updatedPayment) {
throw new Error(`Не удалось найти платеж с хешем ${data.hash}`);
}
return new OutgoingPaymentDomainEntity(updatedPayment as OutgoingPaymentDomainInterface);
} catch (error: any) {
this.logger.error(`Ошибка при обновлении статуса платежа ${data.hash}: ${error.message}`, error);
throw error;
}
}
}
@@ -0,0 +1,14 @@
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
/**
* Доменный интерфейс для создания исходящего платежа
*/
export interface CreateOutgoingPaymentInputDomainInterface {
coopname: string;
username: string;
quantity: string;
symbol: string;
method_id: string;
memo?: string;
statement: ISignedDocumentDomainInterface;
}
@@ -0,0 +1,8 @@
/**
* Доменный интерфейс для получения исходящих платежей
*/
export interface GetOutgoingPaymentsInputDomainInterface {
coopname?: string;
username?: string;
status?: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
}
@@ -0,0 +1,10 @@
import type { PaymentDomainInterface } from './payment-domain.interface';
import { GatewayPaymentTypeEnum } from '../enums/gateway-payment-type.enum';
/**
* Доменный интерфейс исходящего платежа
* Расширяет универсальный интерфейс платежа
*/
export interface OutgoingPaymentDomainInterface extends Omit<PaymentDomainInterface, 'type'> {
type: GatewayPaymentTypeEnum.OUTGOING;
}
@@ -0,0 +1,25 @@
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
import { GatewayPaymentStatusEnum } from '../enums/gateway-payment-status.enum';
import { GatewayPaymentTypeEnum } from '../enums/gateway-payment-type.enum';
/**
* Универсальный доменный интерфейс платежа
* Работает как для входящих, так и для исходящих платежей
*/
export interface PaymentDomainInterface {
id?: string;
coopname: string;
username: string;
hash: string; // Универсальный хеш (income_hash или outcome_hash)
quantity: string;
symbol: string;
method_id: string;
status: GatewayPaymentStatusEnum;
type: GatewayPaymentTypeEnum;
created_at: Date;
updated_at?: Date;
memo?: string;
payment_details?: string;
blockchain_data?: any;
statement?: ISignedDocumentDomainInterface;
}
@@ -0,0 +1,13 @@
import { GatewayPaymentStatusEnum } from '../enums/gateway-payment-status.enum';
import { GatewayPaymentTypeEnum } from '../enums/gateway-payment-type.enum';
/**
* Доменный интерфейс для обновления статуса платежа
*/
export interface UpdatePaymentStatusInputDomainInterface {
hash: string; // Универсальный хеш (income_hash или outcome_hash)
coopname: string;
type?: GatewayPaymentTypeEnum; // Тип платежа
status: GatewayPaymentStatusEnum; // Любой статус из enum
reason?: string; // для статуса failed
}
@@ -0,0 +1,36 @@
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
import type { GatewayContract } from 'cooptypes';
/**
* Доменные интерфейсы для управления gateway
*/
export interface CompleteOutcomeDomainInterface {
coopname: string;
outcome_hash: string;
}
export interface DeclineOutcomeDomainInterface {
coopname: string;
outcome_hash: string;
reason: string;
}
/**
* Порт для взаимодействия с gateway блокчейном
* Только управление уже созданными outcomes - создание происходит автоматически другими контрактами
*/
export interface GatewayBlockchainPort {
// Завершение исходящего платежа
completeOutcome(data: CompleteOutcomeDomainInterface): Promise<TransactionResult>;
// Отклонение исходящего платежа
declineOutcome(data: DeclineOutcomeDomainInterface): Promise<TransactionResult>;
// Получение исходящих платежей из блокчейна
getOutcomes(coopname: string): Promise<GatewayContract.Tables.Outcomes.IOutcome[]>;
// Получение конкретного исходящего платежа
getOutcome(coopname: string, outcome_hash: string): Promise<GatewayContract.Tables.Outcomes.IOutcome | null>;
}
export const GATEWAY_BLOCKCHAIN_PORT = Symbol('GATEWAY_BLOCKCHAIN_PORT');
@@ -0,0 +1,20 @@
import type { PaymentDomainInterface } from '../interfaces/payment-domain.interface';
import type { OutgoingPaymentDomainInterface } from '../interfaces/outgoing-payment-domain.interface';
import type {
PaginationInputDomainInterface,
PaginationResultDomainInterface,
} from '~/domain/common/interfaces/pagination.interface';
import type { GetOutgoingPaymentsInputDomainInterface } from '../interfaces/get-outgoing-payments-input-domain.interface';
export interface PaymentRepository {
findByHash(hash: string): Promise<PaymentDomainInterface | null>;
create(data: PaymentDomainInterface): Promise<PaymentDomainInterface>;
updatePaymentStatus(hash: string, status: string, reason?: string): Promise<PaymentDomainInterface | null>;
updatePaymentWithBlockchainData(hash: string, blockchain_data: any): Promise<PaymentDomainInterface | null>;
getOutgoingPayments(
filters: GetOutgoingPaymentsInputDomainInterface,
options: PaginationInputDomainInterface
): Promise<PaginationResultDomainInterface<OutgoingPaymentDomainInterface>>;
}
export const PAYMENT_REPOSITORY = Symbol('PaymentRepository');
@@ -0,0 +1,72 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { Cooperative } from 'cooptypes';
import { DocumentDomainService } from '~/domain/document/services/document-domain.service';
import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
import { WalletBlockchainPort, WALLET_BLOCKCHAIN_PORT } from '../ports/wallet-blockchain.port';
import { generateUniqueHash } from '~/utils/generate-hash.util';
import type { CreateWithdrawInputDomainInterface } from '../interfaces/create-withdraw-input-domain.interface';
/**
* Интерактор домена wallet для управления выводом средств и генерацией документов
*/
@Injectable()
export class WalletDomainInteractor {
private readonly logger = new Logger(WalletDomainInteractor.name);
constructor(
private readonly documentDomainService: DocumentDomainService,
@Inject(WALLET_BLOCKCHAIN_PORT)
private readonly walletBlockchainPort: WalletBlockchainPort
) {}
/**
* Генерация документа заявления на возврат паевого взноса (900)
*/
async generateReturnByMoneyStatementDocument(
data: Cooperative.Registry.ReturnByMoney.Action,
options: Cooperative.Document.IGenerationOptions
): Promise<DocumentDomainEntity> {
// Устанавливаем registry_id для документа заявления
data.registry_id = Cooperative.Registry.ReturnByMoney.registry_id;
return await this.documentDomainService.generateDocument({ data, options });
}
/**
* Генерация документа решения совета о возврате паевого взноса (901)
*/
async generateReturnByMoneyDecisionDocument(
data: Cooperative.Registry.ReturnByMoneyDecision.Action,
options: Cooperative.Document.IGenerationOptions
): Promise<DocumentDomainEntity> {
// Устанавливаем registry_id для документа решения
data.registry_id = Cooperative.Registry.ReturnByMoneyDecision.registry_id;
return await this.documentDomainService.generateDocument({ data, options });
}
/**
* Создание заявки на вывод средств (createWithdraw)
* Данный метод создает withdraw в blockchain и автоматически создается outcome в gateway
*/
async createWithdraw(data: CreateWithdrawInputDomainInterface): Promise<{ withdraw_hash: string }> {
const withdraw_hash = generateUniqueHash();
try {
// Создаем withdraw в wallet контракте
// wallet контракт автоматически создаст outcome в gateway контракте
await this.walletBlockchainPort.createWithdraw({
coopname: data.coopname,
username: data.username,
withdraw_hash,
quantity: `${data.quantity} ${data.symbol}`,
statement: data.statement,
});
this.logger.log(`Создан withdraw в wallet: ${withdraw_hash}, outcome будет создан автоматически в gateway`);
return { withdraw_hash };
} catch (error: any) {
this.logger.error(`Ошибка при создании withdraw: ${error.message}`, error);
throw error;
}
}
}
@@ -0,0 +1,73 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { Cooperative } from 'cooptypes';
import { DocumentDomainService } from '~/domain/document/services/document-domain.service';
import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
import { WalletBlockchainPort, WALLET_BLOCKCHAIN_PORT } from '../ports/wallet-blockchain.port';
import { generateUniqueHash } from '~/utils/generate-hash.util';
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
import type { CreateWithdrawInputDomainInterface } from '../interfaces/create-withdraw-input-domain.interface';
/**
* Интерактор домена wallet для управления выводом средств и генерацией документов
*/
@Injectable()
export class WalletDomainInteractor {
private readonly logger = new Logger(WalletDomainInteractor.name);
constructor(
private readonly documentDomainService: DocumentDomainService,
@Inject(WALLET_BLOCKCHAIN_PORT)
private readonly walletBlockchainPort: WalletBlockchainPort
) {}
/**
* Генерация документа заявления на возврат паевого взноса (900)
*/
async generateReturnByMoneyStatementDocument(
data: Cooperative.Registry.ReturnByMoney.Action,
options: Cooperative.Document.IGenerationOptions
): Promise<DocumentDomainEntity> {
// Устанавливаем registry_id для документа заявления
data.registry_id = Cooperative.Registry.ReturnByMoney.registry_id;
return await this.documentDomainService.generateDocument({ data, options });
}
/**
* Генерация документа решения совета о возврате паевого взноса (901)
*/
async generateReturnByMoneyDecisionDocument(
data: Cooperative.Registry.ReturnByMoneyDecision.Action,
options: Cooperative.Document.IGenerationOptions
): Promise<DocumentDomainEntity> {
// Устанавливаем registry_id для документа решения
data.registry_id = Cooperative.Registry.ReturnByMoneyDecision.registry_id;
return await this.documentDomainService.generateDocument({ data, options });
}
/**
* Создание заявки на вывод средств (createWithdraw)
* Данный метод создает withdraw в blockchain и автоматически создается outcome в gateway
*/
async createWithdraw(data: CreateWithdrawInputDomainInterface): Promise<{ withdraw_hash: string }> {
const withdraw_hash = generateUniqueHash();
try {
// Создаем withdraw в wallet контракте
// wallet контракт автоматически создаст outcome в gateway контракте
await this.walletBlockchainPort.createWithdraw({
coopname: data.coopname,
username: data.username,
withdraw_hash,
quantity: `${data.quantity} ${data.symbol}`,
statement: data.statement,
});
this.logger.log(`Создан withdraw в wallet: ${withdraw_hash}, outcome будет создан автоматически в gateway`);
return { withdraw_hash };
} catch (error: any) {
this.logger.error(`Ошибка при создании withdraw: ${error.message}`, error);
throw error;
}
}
}
@@ -0,0 +1,14 @@
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
/**
* Доменный интерфейс для создания вывода средств (withdraw)
*/
export interface CreateWithdrawInputDomainInterface {
coopname: string;
username: string;
quantity: string;
symbol: string;
method_id: string;
memo?: string;
statement: ISignedDocumentDomainInterface;
}
@@ -0,0 +1,42 @@
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
import type { WalletContract } from 'cooptypes';
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
/**
* Доменные интерфейсы для входных данных wallet блокчейн адаптера
*/
export interface CreateWithdrawDomainInterface {
coopname: string;
username: string;
withdraw_hash: string;
quantity: string;
statement: ISignedDocumentDomainInterface;
}
export interface GenerateReturnStatementDomainInterface {
coopname: string;
username: string;
quantity: string;
symbol: string;
method_id: string;
memo?: string;
}
/**
* Порт для взаимодействия с wallet блокчейном
*/
export interface WalletBlockchainPort {
// Создание заявки на вывод средств в wallet контракте
createWithdraw(data: CreateWithdrawDomainInterface): Promise<TransactionResult>;
// Генерация заявления на возврат паевого взноса
generateReturnStatement(data: GenerateReturnStatementDomainInterface): Promise<ISignedDocumentDomainInterface>;
// Получение данных из wallet контракта
getWithdraw(coopname: string, withdraw_hash: string): Promise<WalletContract.Tables.Withdraws.IWithdraws | null>;
// Получение всех withdrawals для кооператива
getWithdraws(coopname: string): Promise<WalletContract.Tables.Withdraws.IWithdraws[]>;
}
export const WALLET_BLOCKCHAIN_PORT = Symbol('WALLET_BLOCKCHAIN_PORT');
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { WalletDomainInteractor } from './interactors/wallet.interactor';
import { DocumentDomainModule } from '~/domain/document/document.module';
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
/**
* Доменный модуль Wallet
*/
@Module({
imports: [DocumentDomainModule, InfrastructureModule],
providers: [WalletDomainInteractor],
exports: [WalletDomainInteractor],
})
export class WalletDomainModule {}
@@ -0,0 +1,111 @@
import { Injectable, Logger } from '@nestjs/common';
import { BlockchainService } from '../blockchain.service';
import { GatewayContract } from 'cooptypes';
import { TransactResult } from '@wharfkit/session';
import Vault from '~/models/vault.model';
import httpStatus from 'http-status';
import { HttpApiError } from '~/errors/http-api-error';
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
import type {
GatewayBlockchainPort,
CompleteOutcomeDomainInterface,
DeclineOutcomeDomainInterface,
} from '~/domain/gateway/ports/gateway-blockchain.port';
/**
* Блокчейн адаптер для gateway
* Реализует только управление уже созданными outcomes
*/
@Injectable()
export class GatewayBlockchainAdapter implements GatewayBlockchainPort {
private readonly logger = new Logger(GatewayBlockchainAdapter.name);
constructor(private readonly blockchainService: BlockchainService) {}
/**
* Завершение исходящего платежа
*/
async completeOutcome(data: CompleteOutcomeDomainInterface): Promise<TransactionResult> {
const wif = await Vault.getWif(data.coopname);
if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции');
this.blockchainService.initialize(data.coopname, wif);
// Преобразуем доменные данные в формат cooptypes контракта
const blockchainData: GatewayContract.Actions.CompleteOutcome.ICompleteOutcome = {
coopname: data.coopname,
outcome_hash: data.outcome_hash,
};
const result = (await this.blockchainService.transact({
account: GatewayContract.contractName.production,
name: GatewayContract.Actions.CompleteOutcome.actionName,
authorization: [{ actor: data.coopname, permission: 'active' }],
data: blockchainData,
})) as TransactResult;
this.logger.log(`Завершен исходящий платеж: ${data.outcome_hash}`);
return result;
}
/**
* Отклонение исходящего платежа
*/
async declineOutcome(data: DeclineOutcomeDomainInterface): Promise<TransactionResult> {
const wif = await Vault.getWif(data.coopname);
if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции');
this.blockchainService.initialize(data.coopname, wif);
// Преобразуем доменные данные в формат cooptypes контракта
const blockchainData: GatewayContract.Actions.DeclineOutcome.IOutDecline = {
coopname: data.coopname,
outcome_hash: data.outcome_hash,
reason: data.reason,
};
const result = (await this.blockchainService.transact({
account: GatewayContract.contractName.production,
name: GatewayContract.Actions.DeclineOutcome.actionName,
authorization: [{ actor: data.coopname, permission: 'active' }],
data: blockchainData,
})) as TransactResult;
this.logger.log(`Отклонен исходящий платеж: ${data.outcome_hash}, причина: ${data.reason}`);
return result;
}
/**
* Получение исходящих платежей из блокчейна
*/
async getOutcomes(coopname: string): Promise<GatewayContract.Tables.Outcomes.IOutcome[]> {
return this.blockchainService.getAllRows(
GatewayContract.contractName.production,
coopname,
GatewayContract.Tables.Outcomes.tableName
);
}
/**
* Получение конкретного исходящего платежа
*/
async getOutcome(coopname: string, outcome_hash: string): Promise<GatewayContract.Tables.Outcomes.IOutcome | null> {
try {
const outcomes = await this.blockchainService.query(
GatewayContract.contractName.production,
coopname,
GatewayContract.Tables.Outcomes.tableName,
{
indexPosition: 'secondary',
from: outcome_hash,
to: outcome_hash,
}
);
return outcomes.length > 0 ? outcomes[0] : null;
} catch (error: any) {
this.logger.warn(`Не удалось получить outcome ${outcome_hash} для ${coopname}: ${error.message}`);
return null;
}
}
}
@@ -0,0 +1,104 @@
import { Injectable, Logger } from '@nestjs/common';
import { BlockchainService } from '../blockchain.service';
import { WalletContract } from 'cooptypes';
import { TransactResult } from '@wharfkit/session';
import Vault from '~/models/vault.model';
import httpStatus from 'http-status';
import { HttpApiError } from '~/errors/http-api-error';
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
import type {
WalletBlockchainPort,
CreateWithdrawDomainInterface,
GenerateReturnStatementDomainInterface,
} from '~/domain/wallet/ports/wallet-blockchain.port';
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
import { DomainToBlockchainUtils } from '../utils/domain-to-blockchain.utils';
/**
* Блокчейн адаптер для wallet
* Реализует взаимодействие с контрактом wallet
*/
@Injectable()
export class WalletBlockchainAdapter implements WalletBlockchainPort {
private readonly logger = new Logger(WalletBlockchainAdapter.name);
constructor(
private readonly blockchainService: BlockchainService,
private readonly domainToBlockchainUtils: DomainToBlockchainUtils
) {}
/**
* Создание заявки на вывод средств в контракте wallet
*/
async createWithdraw(data: CreateWithdrawDomainInterface): Promise<TransactionResult> {
const wif = await Vault.getWif(data.coopname);
if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции');
this.blockchainService.initialize(data.coopname, wif);
// Форматируем quantity к необходимой точности для блокчейна
const formattedQuantity = this.domainToBlockchainUtils.formatQuantityWithPrecision(data.quantity);
// Преобразуем доменные данные в формат cooptypes контракта
const blockchainData: WalletContract.Actions.CreateWithdraw.ICreateWithdraw = {
coopname: data.coopname,
username: data.username,
withdraw_hash: data.withdraw_hash,
quantity: formattedQuantity,
statement: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.statement),
};
const result = (await this.blockchainService.transact({
account: WalletContract.contractName.production,
name: WalletContract.Actions.CreateWithdraw.actionName,
authorization: [{ actor: data.coopname, permission: 'active' }],
data: blockchainData,
})) as TransactResult;
this.logger.log(`Создана заявка на вывод средств: ${data.withdraw_hash}`);
return result;
}
/**
* Генерация заявления на возврат паевого взноса
*/
async generateReturnStatement(_data: GenerateReturnStatementDomainInterface): Promise<ISignedDocumentDomainInterface> {
// TODO: Реализовать генерацию заявления через документный сервис
// Пока возвращаем заглушку
throw new Error('Метод generateReturnStatement еще не реализован');
}
/**
* Получение данных из wallet контракта
*/
async getWithdraw(coopname: string, withdraw_hash: string): Promise<WalletContract.Tables.Withdraws.IWithdraws | null> {
try {
const withdraws = await this.blockchainService.query(
WalletContract.contractName.production,
coopname,
WalletContract.Tables.Withdraws.tableName,
{
indexPosition: 'secondary',
from: withdraw_hash,
to: withdraw_hash,
}
);
return withdraws.length > 0 ? withdraws[0] : null;
} catch (error: any) {
this.logger.warn(`Не удалось получить withdraw ${withdraw_hash} для ${coopname}: ${error.message}`);
return null;
}
}
/**
* Получение всех withdrawals для кооператива
*/
async getWithdraws(coopname: string): Promise<WalletContract.Tables.Withdraws.IWithdraws[]> {
return this.blockchainService.getAllRows(
WalletContract.contractName.production,
coopname,
WalletContract.Tables.Withdraws.tableName
);
}
}
@@ -15,6 +15,10 @@ import { MEET_BLOCKCHAIN_PORT } from '~/domain/meet/ports/meet-blockchain.port';
import { MeetBlockchainAdapter } from './adapters/meet-blockchain.adapter';
import { DomainToBlockchainUtils } from './utils/domain-to-blockchain.utils';
import { DomainModule } from '~/domain/domain.module';
import { GatewayBlockchainAdapter } from './adapters/gateway-blockchain.adapter';
import { WALLET_BLOCKCHAIN_PORT } from '~/domain/wallet/ports/wallet-blockchain.port';
import { GATEWAY_BLOCKCHAIN_PORT } from '~/domain/gateway/ports/gateway-blockchain.port';
import { WalletBlockchainAdapter } from './adapters/wallet-blockchain.adapter';
@Global()
@Module({
@@ -49,9 +53,20 @@ import { DomainModule } from '~/domain/domain.module';
provide: MEET_BLOCKCHAIN_PORT,
useClass: MeetBlockchainAdapter,
},
// Провайдеры для gateway
{
provide: GATEWAY_BLOCKCHAIN_PORT,
useClass: GatewayBlockchainAdapter,
},
// Провайдеры для wallet
{
provide: WALLET_BLOCKCHAIN_PORT,
useClass: WalletBlockchainAdapter,
},
DomainToBlockchainUtils,
],
exports: [
BlockchainService,
BLOCKCHAIN_PORT,
BRANCH_BLOCKCHAIN_PORT,
SYSTEM_BLOCKCHAIN_PORT,
@@ -59,6 +74,8 @@ import { DomainModule } from '~/domain/domain.module';
SOVIET_BLOCKCHAIN_PORT,
COOPLACE_BLOCKCHAIN_PORT,
MEET_BLOCKCHAIN_PORT,
GATEWAY_BLOCKCHAIN_PORT,
WALLET_BLOCKCHAIN_PORT,
DomainToBlockchainUtils,
],
})
@@ -1,6 +1,8 @@
import { Injectable } from '@nestjs/common';
import { Cooperative } from 'cooptypes';
import moment from 'moment';
import config from '~/config/config';
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
/**
* Утилитарный класс для конвертации доменных объектов в инфраструктурные типы блокчейна
@@ -12,9 +14,7 @@ export class DomainToBlockchainUtils {
* @param document Доменный объект подписанного документа
* @returns Объект документа в формате блокчейна
*/
convertSignedDocumentToBlockchainFormat(
document: Cooperative.Document.ISignedDocument2
): Cooperative.Document.IChainDocument2 {
convertSignedDocumentToBlockchainFormat(document: ISignedDocumentDomainInterface): Cooperative.Document.IChainDocument2 {
return {
version: document.version,
hash: document.hash,
@@ -25,6 +25,22 @@ export class DomainToBlockchainUtils {
};
}
/**
* Преобразует документ из блокчейна в доменный интерфейс
* @param chainDoc Документ из блокчейна
* @returns Документ в доменном формате
*/
convertBlockchainDocumentToDomainFormat(chainDoc: Cooperative.Document.IChainDocument2): ISignedDocumentDomainInterface {
return {
version: chainDoc.version,
hash: chainDoc.hash,
doc_hash: chainDoc.doc_hash,
meta_hash: chainDoc.meta_hash,
meta: typeof chainDoc.meta === 'string' ? (chainDoc.meta === '' ? {} : JSON.parse(chainDoc.meta)) : chainDoc.meta,
signatures: chainDoc.signatures,
};
}
/**
* Преобразует Date в формат EOSIO time_point_sec для блокчейна
* @param date Дата или строка с датой
@@ -35,7 +51,44 @@ export class DomainToBlockchainUtils {
}
/**
* Преобразует объект IChainDocument2 в ISignedDocument2
* Форматирование quantity с проверкой символа и использованием precision из конфига
* @param quantity Строка в формате "число символ" (например, "100.0 AXON")
* @returns Отформатированная строка quantity для блокчейна
*/
formatQuantityWithPrecision(quantity: string): string {
// Извлекаем число и символ
const parts = quantity.split(' ');
if (parts.length !== 2) {
throw new Error(`Неверный формат quantity: ${quantity}. Ожидается "число символ"`);
}
const [amount, symbol] = parts;
const numericAmount = parseFloat(amount);
if (isNaN(numericAmount)) {
throw new Error(`Некорректное числовое значение в quantity: ${amount}`);
}
// Проверяем символ на соответствие с конфигом
let precision: number;
if (symbol === config.blockchain.root_symbol) {
precision = config.blockchain.root_precision;
} else if (symbol === config.blockchain.root_govern_symbol) {
precision = config.blockchain.root_govern_precision;
} else {
throw new Error(
`Неподдерживаемый символ: ${symbol}. Поддерживаются только: ${config.blockchain.root_symbol}, ${config.blockchain.root_govern_symbol}`
);
}
// Форматируем к необходимому количеству знаков после запятой
const formattedAmount = numericAmount.toFixed(precision);
return `${formattedAmount} ${symbol}`;
}
/**
* Преобразует объект IChainDocument2 в ISignedDocument2 (для совместимости)
* @param chainDoc Документ из блокчейна
* @returns Документ в формате ISignedDocument2
*/
@@ -0,0 +1,47 @@
import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
@Entity('payments')
export class PaymentEntity {
@PrimaryColumn()
hash!: string;
@Column()
coopname!: string;
@Column()
username!: string;
@Column()
quantity!: string;
@Column()
symbol!: string;
@Column()
method_id!: string;
@Column()
status!: string;
@Column()
type!: string;
@CreateDateColumn()
created_at!: Date;
@UpdateDateColumn()
updated_at!: Date;
@Column({ nullable: true })
memo?: string;
@Column('json', { nullable: true })
payment_details?: any;
@Column('json', { nullable: true })
blockchain_data?: any;
@Column('json', { nullable: true })
statement?: ISignedDocumentDomainInterface;
}
@@ -0,0 +1,136 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PaymentEntity } from '../entities/payment.entity';
import { GatewayPaymentStatusEnum } from '~/domain/gateway/enums/gateway-payment-status.enum';
import { GatewayPaymentTypeEnum } from '~/domain/gateway/enums/gateway-payment-type.enum';
import type { PaymentRepository } from '~/domain/gateway/repositories/payment.repository';
import type { PaymentDomainInterface } from '~/domain/gateway/interfaces/payment-domain.interface';
import type { OutgoingPaymentDomainInterface } from '~/domain/gateway/interfaces/outgoing-payment-domain.interface';
import type {
PaginationInputDomainInterface,
PaginationResultDomainInterface,
} from '~/domain/common/interfaces/pagination.interface';
import type { GetOutgoingPaymentsInputDomainInterface } from '~/domain/gateway/interfaces/get-outgoing-payments-input-domain.interface';
@Injectable()
export class TypeOrmPaymentRepository implements PaymentRepository {
constructor(
@InjectRepository(PaymentEntity)
private readonly paymentRepository: Repository<PaymentEntity>
) {}
async findByHash(hash: string): Promise<PaymentDomainInterface | null> {
const payment = await this.paymentRepository.findOneBy({ hash });
if (!payment) return null;
return this.mapToDomainEntity(payment);
}
async create(data: PaymentDomainInterface): Promise<PaymentDomainInterface> {
const payment = new PaymentEntity();
payment.hash = data.hash;
payment.coopname = data.coopname;
payment.username = data.username;
payment.quantity = data.quantity;
payment.symbol = data.symbol;
payment.method_id = data.method_id;
payment.status = data.status;
payment.type = data.type;
payment.created_at = data.created_at;
payment.memo = data.memo;
payment.payment_details = data.payment_details;
payment.blockchain_data = data.blockchain_data;
payment.statement = data.statement;
const createdEntity = await this.paymentRepository.save(payment);
return this.mapToDomainEntity(createdEntity);
}
async updatePaymentStatus(
hash: string,
status: GatewayPaymentStatusEnum,
reason?: string
): Promise<PaymentDomainInterface | null> {
const payment = await this.paymentRepository.findOneBy({ hash });
if (!payment) return null;
payment.status = status;
payment.updated_at = new Date();
if (reason) {
payment.memo = reason;
}
const updatedEntity = await this.paymentRepository.save(payment);
return this.mapToDomainEntity(updatedEntity);
}
async updatePaymentWithBlockchainData(hash: string, blockchain_data: any): Promise<PaymentDomainInterface | null> {
const payment = await this.paymentRepository.findOneBy({ hash });
if (!payment) return null;
payment.blockchain_data = blockchain_data;
payment.updated_at = new Date();
const updatedEntity = await this.paymentRepository.save(payment);
return this.mapToDomainEntity(updatedEntity);
}
async getOutgoingPayments(
filters: GetOutgoingPaymentsInputDomainInterface,
options: PaginationInputDomainInterface
): Promise<PaginationResultDomainInterface<OutgoingPaymentDomainInterface>> {
const queryBuilder = this.paymentRepository.createQueryBuilder('payment');
queryBuilder.where('payment.type = :type', { type: GatewayPaymentTypeEnum.OUTGOING });
if (filters.coopname) {
queryBuilder.andWhere('payment.coopname = :coopname', { coopname: filters.coopname });
}
if (filters.username) {
queryBuilder.andWhere('payment.username = :username', { username: filters.username });
}
if (filters.status) {
queryBuilder.andWhere('payment.status = :status', { status: filters.status });
}
queryBuilder.orderBy('payment.created_at', 'DESC');
const limit = options.limit || 10;
const page = options.page || 1;
const skip = (page - 1) * limit;
queryBuilder.take(limit).skip(skip);
const [payments, totalCount] = await queryBuilder.getManyAndCount();
const items = payments.map((payment) => this.mapToDomainEntity(payment) as OutgoingPaymentDomainInterface);
return {
items,
totalCount,
totalPages: Math.ceil(totalCount / limit),
currentPage: page,
};
}
private mapToDomainEntity(entity: PaymentEntity): PaymentDomainInterface {
return {
hash: entity.hash,
coopname: entity.coopname,
username: entity.username,
quantity: entity.quantity,
symbol: entity.symbol,
method_id: entity.method_id,
status: entity.status as GatewayPaymentStatusEnum,
type: entity.type as GatewayPaymentTypeEnum,
created_at: entity.created_at,
updated_at: entity.updated_at,
memo: entity.memo,
payment_details: entity.payment_details,
blockchain_data: entity.blockchain_data,
statement: entity.statement,
};
}
}
@@ -21,6 +21,9 @@ import { TypeOrmCandidateRepository } from './repositories/typeorm-candidate.rep
import { MeetProcessedEntity } from './entities/meet-processed.entity';
import { MEET_PROCESSED_REPOSITORY } from '~/domain/meet/repositories/meet-processed.repository';
import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-processed.repository';
import { PaymentEntity } from './entities/payment.entity';
import { PAYMENT_REPOSITORY } from '~/domain/gateway/repositories/payment.repository';
import { TypeOrmPaymentRepository } from './repositories/typeorm-payment.repository';
@Global()
@Module({
@@ -43,6 +46,7 @@ import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-proc
MeetProcessedEntity,
MigrationEntity,
CandidateEntity,
PaymentEntity,
]),
],
providers: [
@@ -70,6 +74,10 @@ import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-proc
provide: CANDIDATE_REPOSITORY,
useClass: TypeOrmCandidateRepository,
},
{
provide: PAYMENT_REPOSITORY,
useClass: TypeOrmPaymentRepository,
},
],
exports: [
NestTypeOrmModule,
@@ -79,6 +87,7 @@ import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-proc
MEET_PROCESSED_REPOSITORY,
MIGRATION_REPOSITORY,
CANDIDATE_REPOSITORY,
PAYMENT_REPOSITORY,
],
})
export class TypeOrmModule {}
@@ -1,4 +1,3 @@
// app.module.ts
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { GraphqlModule } from './graphql/graphql.module';
@@ -18,6 +18,8 @@ import { PaymentModule } from './payment/payment.module';
import { CooplaceModule } from './cooplace/cooplace.module';
import { DesktopModule } from './desktop/desktop.module';
import { MeetModule } from './meet/meet.module';
import { GatewayModule } from './gateway/gateway.module';
import { WalletModule } from './wallet/wallet.module';
@Module({
imports: [
@@ -39,6 +41,8 @@ import { MeetModule } from './meet/meet.module';
ParticipantModule,
CooplaceModule,
MeetModule,
GatewayModule,
WalletModule,
],
exports: [
AccountModule,
@@ -59,6 +63,8 @@ import { MeetModule } from './meet/meet.module';
ParticipantModule,
CooplaceModule,
MeetModule,
GatewayModule,
WalletModule,
],
})
export class ApplicationModule {}
@@ -0,0 +1,53 @@
import { InputType, Field, IntersectionType, OmitType } from '@nestjs/graphql';
import { IsString, IsNumber } from 'class-validator';
import { Cooperative } from 'cooptypes';
import { GenerateMetaDocumentInputDTO } from '~/modules/document/dto/generate-meta-document-input.dto';
import { MetaDocumentInputDTO } from '~/modules/document/dto/meta-document-input.dto';
import { SignedDigitalDocumentInputDTO } from '~/modules/document/dto/signed-digital-document-input.dto';
type ExcludeCommonProps<T> = Omit<T, 'coopname' | 'username' | 'registry_id'>;
type action = Cooperative.Registry.ReturnByMoneyDecision.Action;
@InputType('BaseReturnByMoneyDecisionMetaDocumentInput')
class BaseReturnByMoneyDecisionMetaDocumentInputDTO implements ExcludeCommonProps<action> {
@Field({ description: 'ID решения совета' })
@IsNumber()
decision_id!: number;
@Field({ description: 'Хэш платежа' })
@IsString()
payment_hash!: string;
@Field({ description: 'Сумма к возврату' })
@IsString()
amount!: string;
@Field({ description: 'Валюта' })
@IsString()
currency!: string;
}
@InputType('ReturnByMoneyDecisionGenerateDocumentInput')
export class ReturnByMoneyDecisionGenerateDocumentInputDTO
extends IntersectionType(
BaseReturnByMoneyDecisionMetaDocumentInputDTO,
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
)
implements action
{
registry_id!: number;
}
@InputType('ReturnByMoneyDecisionSignedMetaDocumentInput')
export class ReturnByMoneyDecisionSignedMetaDocumentInputDTO
extends IntersectionType(BaseReturnByMoneyDecisionMetaDocumentInputDTO, MetaDocumentInputDTO)
implements action {}
@InputType('ReturnByMoneyDecisionSignedDocumentInput')
export class ReturnByMoneyDecisionSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
@Field(() => ReturnByMoneyDecisionSignedMetaDocumentInputDTO, {
description: 'Метаинформация для документа решения совета о возврате паевого взноса',
})
public readonly meta!: ReturnByMoneyDecisionSignedMetaDocumentInputDTO;
}
@@ -0,0 +1,57 @@
import { InputType, Field, IntersectionType, OmitType } from '@nestjs/graphql';
import { IsString } from 'class-validator';
import { Cooperative } from 'cooptypes';
import { GenerateMetaDocumentInputDTO } from '~/modules/document/dto/generate-meta-document-input.dto';
import { MetaDocumentInputDTO } from '~/modules/document/dto/meta-document-input.dto';
import { SignedDigitalDocumentInputDTO } from '~/modules/document/dto/signed-digital-document-input.dto';
// утилита для выборки повторяющихся параметров из базовых интерфейсов
type ExcludeCommonProps<T> = Omit<T, 'coopname' | 'username' | 'registry_id'>;
// интерфейс параметров для генерации
type action = Cooperative.Registry.ReturnByMoney.Action;
@InputType('RequestInput')
class RequestInputDTO implements Cooperative.Registry.ReturnByMoney.IMoneyReturnRequest {
@Field({ description: 'ID платежного метода' })
@IsString()
method_id!: string;
@Field({ description: 'Сумма к возврату' })
@IsString()
amount!: string;
@Field({ description: 'Валюта' })
@IsString()
currency!: string;
}
@InputType(`BaseReturnByMoneyMetaDocumentInput`)
class BaseReturnByMoneyMetaDocumentInputDTO implements ExcludeCommonProps<action> {
@Field(() => RequestInputDTO, { description: 'Данные запроса на возврат денежных средств' })
request!: RequestInputDTO;
}
@InputType(`ReturnByMoneyGenerateDocumentInput`)
export class ReturnByMoneyGenerateDocumentInputDTO
extends IntersectionType(
BaseReturnByMoneyMetaDocumentInputDTO,
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
)
implements action
{
registry_id!: number;
}
@InputType(`ReturnByMoneySignedMetaDocumentInput`)
export class ReturnByMoneySignedMetaDocumentInputDTO
extends IntersectionType(BaseReturnByMoneyMetaDocumentInputDTO, MetaDocumentInputDTO)
implements action {}
@InputType(`ReturnByMoneySignedDocumentInput`)
export class ReturnByMoneySignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
@Field(() => ReturnByMoneySignedMetaDocumentInputDTO, {
description: 'Метаинформация для документа заявления на возврат паевого взноса денежными средствами',
})
public readonly meta!: ReturnByMoneySignedMetaDocumentInputDTO;
}
@@ -0,0 +1,48 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsString, IsOptional, IsNotEmpty, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import type { CreateOutgoingPaymentInputDomainInterface } from '~/domain/gateway/interfaces/create-outgoing-payment-input-domain.interface';
import { SignedDigitalDocumentInputDTO } from '~/modules/document/dto/signed-digital-document-input.dto';
/**
* DTO для создания исходящего платежа
*/
@InputType('CreateOutgoingPaymentInput')
export class CreateOutgoingPaymentInputDTO implements CreateOutgoingPaymentInputDomainInterface {
@Field(() => String)
@IsString()
@IsNotEmpty()
coopname!: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
username!: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
quantity!: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
symbol!: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
method_id!: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
memo?: string;
@Field(() => SignedDigitalDocumentInputDTO, {
description: 'Подписанный документ заявления на возврат денежными средствами',
})
@ValidateNested()
@Type(() => SignedDigitalDocumentInputDTO)
statement!: SignedDigitalDocumentInputDTO;
}
@@ -0,0 +1,83 @@
import { ObjectType, Field, ID, registerEnumType } from '@nestjs/graphql';
import { GraphQLJSON } from 'graphql-type-json';
import { GatewayPaymentStatusEnum } from '~/domain/gateway/enums/gateway-payment-status.enum';
import { GatewayPaymentTypeEnum } from '~/domain/gateway/enums/gateway-payment-type.enum';
// Регистрируем enum'ы для GraphQL
registerEnumType(GatewayPaymentStatusEnum, {
name: 'gatewayPaymentStatus',
description: 'Статус платежа в системе Gateway',
});
registerEnumType(GatewayPaymentTypeEnum, {
name: 'gatewayPaymentType',
description: 'Тип платежа в системе Gateway',
});
/**
* Универсальное DTO платежа для GraphQL
* Работает как для входящих, так и для исходящих платежей
*/
@ObjectType('GatewayPayment')
export class GatewayPaymentDTO {
@Field(() => ID, { nullable: true, description: 'Уникальный идентификатор платежа' })
id?: string;
@Field({ description: 'Название кооператива' })
coopname!: string;
@Field({ description: 'Имя пользователя' })
username!: string;
@Field({ description: 'Универсальный хеш платежа' })
hash!: string;
@Field({ description: 'Хеш исходящего платежа', nullable: true })
outcome_hash?: string;
@Field({ description: 'Хеш входящего платежа', nullable: true })
income_hash?: string;
@Field({ description: 'Количество' })
quantity!: string;
@Field({ description: 'Символ валюты' })
symbol!: string;
@Field({ description: 'ID платежного метода' })
method_id!: string;
@Field(() => GatewayPaymentStatusEnum, { description: 'Статус платежа' })
status!: GatewayPaymentStatusEnum;
@Field(() => GatewayPaymentTypeEnum, { description: 'Тип платежа: входящий или исходящий' })
type!: GatewayPaymentTypeEnum;
@Field({ description: 'Дата создания' })
created_at!: Date;
@Field({ description: 'Дата последнего обновления', nullable: true })
updated_at?: Date;
@Field({ description: 'Дополнительная информация', nullable: true })
memo?: string;
@Field({ description: 'Детали платежа', nullable: true })
payment_details?: string;
@Field(() => GraphQLJSON, { description: 'Данные из блокчейна', nullable: true })
blockchain_data?: any;
// Вычисляемые поля
@Field({ description: 'Форматированная сумма' })
formatted_amount!: string;
@Field({ description: 'Человекочитаемый статус' })
status_label!: string;
@Field({ description: 'Человекочитаемый тип платежа' })
type_label!: string;
@Field({ description: 'Может ли статус быть изменен' })
can_change_status!: boolean;
}
@@ -0,0 +1,26 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsString, IsEnum, IsOptional, IsNotEmpty } from 'class-validator';
import type { GetOutgoingPaymentsInputDomainInterface } from '~/domain/gateway/interfaces/get-outgoing-payments-input-domain.interface';
/**
* DTO для получения исходящих платежей
*/
@InputType('GetOutgoingPaymentsInput')
export class GetOutgoingPaymentsInputDTO implements GetOutgoingPaymentsInputDomainInterface {
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
@IsNotEmpty()
coopname?: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
@IsNotEmpty()
username?: string;
@Field(() => String, { nullable: true })
@IsEnum(['pending', 'processing', 'completed', 'failed', 'cancelled'])
@IsOptional()
status?: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
}
@@ -0,0 +1,62 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { GraphQLJSON } from 'graphql-type-json';
import { GatewayPaymentStatusEnum } from '~/domain/gateway/enums/gateway-payment-status.enum';
import { GatewayPaymentTypeEnum } from '~/domain/gateway/enums/gateway-payment-type.enum';
/**
* DTO исходящего платежа для GraphQL
*/
@ObjectType('OutgoingPayment')
export class OutgoingGatewayPaymentDTO {
@Field(() => ID, { nullable: true, description: 'Уникальный идентификатор платежа' })
id?: string;
@Field({ description: 'Название кооператива' })
coopname!: string;
@Field({ description: 'Имя пользователя' })
username!: string;
@Field({ description: 'Хеш исходящего платежа' })
hash!: string;
@Field({ description: 'Количество' })
quantity!: string;
@Field({ description: 'Символ валюты' })
symbol!: string;
@Field({ description: 'ID платежного метода' })
method_id!: string;
@Field(() => GatewayPaymentStatusEnum, { description: 'Статус платежа' })
status!: GatewayPaymentStatusEnum;
@Field(() => GatewayPaymentTypeEnum, { description: 'Тип платежа (всегда outgoing)' })
type!: GatewayPaymentTypeEnum;
@Field({ description: 'Дата создания' })
created_at!: Date;
@Field({ description: 'Дата последнего обновления', nullable: true })
updated_at?: Date;
@Field({ description: 'Дополнительная информация', nullable: true })
memo?: string;
@Field({ description: 'Детали платежа', nullable: true })
payment_details?: string;
@Field(() => GraphQLJSON, { description: 'Данные из блокчейна', nullable: true })
blockchain_data?: any;
// Вычисляемые поля
@Field({ description: 'Форматированная сумма' })
formatted_amount!: string;
@Field({ description: 'Человекочитаемый статус' })
status_label!: string;
@Field({ description: 'Может ли статус быть изменен' })
can_change_status!: boolean;
}
@@ -0,0 +1,34 @@
import { InputType, Field } from '@nestjs/graphql';
import { IsString, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
import { GatewayPaymentStatusEnum } from '~/domain/gateway/enums/gateway-payment-status.enum';
import { GatewayPaymentTypeEnum } from '~/domain/gateway/enums/gateway-payment-type.enum';
import type { UpdatePaymentStatusInputDomainInterface } from '~/domain/gateway/interfaces/update-payment-status-input-domain.interface';
/**
* DTO для обновления статуса платежа
*/
@InputType('UpdatePaymentStatusInput')
export class UpdatePaymentStatusInputDTO implements UpdatePaymentStatusInputDomainInterface {
@Field({ description: 'Универсальный хеш платежа' })
@IsString()
@IsNotEmpty()
hash!: string;
@Field({ description: 'Название кооператива' })
@IsString()
@IsNotEmpty()
coopname!: string;
@Field({ description: 'Тип платежа' })
@IsEnum(GatewayPaymentTypeEnum)
type!: GatewayPaymentTypeEnum;
@Field({ description: 'Новый статус платежа' })
@IsEnum(GatewayPaymentStatusEnum)
status!: GatewayPaymentStatusEnum;
@Field({ description: 'Причина изменения статуса (для статуса failed)', nullable: true })
@IsString()
@IsOptional()
reason?: string;
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { GatewayResolver } from './resolvers/gateway.resolver';
import { GatewayService } from './services/gateway.service';
import { GatewayDomainModule } from '~/domain/gateway/gateway-domain.module';
@Module({
imports: [GatewayDomainModule],
providers: [GatewayResolver, GatewayService],
exports: [GatewayService],
})
export class GatewayModule {}
@@ -0,0 +1,52 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { GqlJwtAuthGuard } from '~/modules/auth/guards/graphql-jwt-auth.guard';
import { RolesGuard } from '~/modules/auth/guards/roles.guard';
import { AuthRoles } from '~/modules/auth/decorators/auth.decorator';
import { GatewayService } from '../services/gateway.service';
import { UpdatePaymentStatusInputDTO } from '../dto/update-payment-status-input.dto';
import { GetOutgoingPaymentsInputDTO } from '../dto/get-outgoing-payments-input.dto';
import { OutgoingGatewayPaymentDTO } from '../dto/outgoing-payment.dto';
import { GatewayPaymentDTO } from '../dto/gateway-payment.dto';
import { createPaginationResult, PaginationInputDTO, PaginationResult } from '~/modules/common/dto/pagination.dto';
const paginatedPaymentsResult = createPaginationResult(GatewayPaymentDTO, 'PaginatedGatewayPayments');
/**
* GraphQL резолвер для gateway
* Обеспечивает API для управления исходящими платежами
*/
@Resolver()
export class GatewayResolver {
constructor(private readonly gatewayService: GatewayService) {}
/**
* Query: Получить список всех платежей (универсальный)
* Готовится для будущей поддержки входящих платежей
*/
@Query(() => paginatedPaymentsResult, {
name: 'getGatewayPayments',
description: 'Получить список всех платежей (универсальный метод, поддержка входящих платежей в будущем).',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async getGatewayPayments(
@Args('filters') filters: GetOutgoingPaymentsInputDTO,
@Args('options') options: PaginationInputDTO
): Promise<PaginationResult<GatewayPaymentDTO>> {
return this.gatewayService.getPayments(filters, options);
}
/**
* Mutation: Обновить статус платежа
*/
@Mutation(() => OutgoingGatewayPaymentDTO, {
name: 'updatePaymentStatus',
description: 'Обновить статус платежа.',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async updatePaymentStatus(@Args('input') input: UpdatePaymentStatusInputDTO): Promise<OutgoingGatewayPaymentDTO> {
return this.gatewayService.updatePaymentStatus(input);
}
}
@@ -0,0 +1,47 @@
import { Injectable } from '@nestjs/common';
import { GatewayInteractor } from '~/domain/gateway/interactors/gateway.interactor';
import type { UpdatePaymentStatusInputDTO } from '../dto/update-payment-status-input.dto';
import type { GetOutgoingPaymentsInputDTO } from '../dto/get-outgoing-payments-input.dto';
import type { OutgoingGatewayPaymentDTO } from '../dto/outgoing-payment.dto';
import type { GatewayPaymentDTO } from '../dto/gateway-payment.dto';
import type { PaginationInputDTO, PaginationResult } from '~/modules/common/dto/pagination.dto';
@Injectable()
export class GatewayService {
constructor(private readonly gatewayInteractor: GatewayInteractor) {}
/**
* Получить список всех платежей (универсальный метод)
*/
async getPayments(
filters: GetOutgoingPaymentsInputDTO,
options: PaginationInputDTO
): Promise<PaginationResult<GatewayPaymentDTO>> {
const result = await this.gatewayInteractor.getOutgoingPayments(filters, options);
return {
items: result.items.map((item) => {
const dto = item.toDTO();
return {
...dto,
type: 'outgoing',
} as GatewayPaymentDTO;
}),
totalCount: result.totalCount,
totalPages: result.totalPages,
currentPage: result.currentPage,
};
}
/**
* Обновить статус платежа (универсальный метод)
*/
async updatePaymentStatus(input: UpdatePaymentStatusInputDTO): Promise<OutgoingGatewayPaymentDTO> {
if (input.type !== 'outgoing') {
throw new Error('В настоящее время поддерживаются только исходящие платежи');
}
const result = await this.gatewayInteractor.updatePaymentStatus(input);
return result.toDTO() as OutgoingGatewayPaymentDTO;
}
}
@@ -0,0 +1,41 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsString, IsOptional, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { SignedDigitalDocumentInputDTO } from '~/modules/document/dto/signed-digital-document-input.dto';
import type { CreateWithdrawInputDomainInterface } from '~/domain/wallet/interfaces/create-withdraw-input-domain.interface';
/**
* DTO для создания заявки на вывод средств
*/
@InputType('CreateWithdrawInput')
export class CreateWithdrawInputDTO implements CreateWithdrawInputDomainInterface {
@Field(() => String, { description: 'Имя аккаунта кооператива' })
@IsString()
coopname!: string;
@Field(() => String, { description: 'Имя пользователя' })
@IsString()
username!: string;
@Field(() => String, { description: 'Количество средств' })
@IsString()
quantity!: string;
@Field(() => String, { description: 'Символ валюты' })
@IsString()
symbol!: string;
@Field(() => String, { description: 'ID метода платежа' })
@IsString()
method_id!: string;
@Field(() => String, { description: 'Дополнительная информация', nullable: true })
@IsString()
@IsOptional()
memo?: string;
@Field(() => SignedDigitalDocumentInputDTO, { description: 'Подписанное заявление на возврат средств' })
@ValidateNested()
@Type(() => SignedDigitalDocumentInputDTO)
statement!: SignedDigitalDocumentInputDTO;
}
@@ -0,0 +1,10 @@
import { Field, ObjectType } from '@nestjs/graphql';
/**
* DTO для ответа создания заявки на вывод средств
*/
@ObjectType('CreateWithdrawResponse')
export class CreateWithdrawResponseDTO {
@Field(() => String, { description: 'Хеш созданной заявки на вывод' })
withdraw_hash!: string;
}
@@ -0,0 +1,73 @@
import { Resolver, Mutation, Args } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { GqlJwtAuthGuard } from '~/modules/auth/guards/graphql-jwt-auth.guard';
import { RolesGuard } from '~/modules/auth/guards/roles.guard';
import { AuthRoles } from '~/modules/auth/decorators/auth.decorator';
import { WalletService } from '../services/wallet.service';
import { GenerateDocumentOptionsInputDTO } from '~/modules/document/dto/generate-document-options-input.dto';
import { GeneratedDocumentDTO } from '~/modules/document/dto/generated-document.dto';
import { ReturnByMoneyGenerateDocumentInputDTO } from '~/modules/document/documents-dto/return-by-money-statement.dto';
import { ReturnByMoneyDecisionGenerateDocumentInputDTO } from '~/modules/document/documents-dto/return-by-money-decision.dto';
import { CreateWithdrawInputDTO } from '../dto/create-withdraw-input.dto';
import { CreateWithdrawResponseDTO } from '../dto/create-withdraw-response.dto';
/**
* GraphQL резолвер для wallet
* Обеспечивает API для управления выводом средств и генерацией документов
*/
@Resolver()
export class WalletResolver {
constructor(private readonly walletService: WalletService) {}
/**
* Mutation: Генерация документа заявления на возврат паевого взноса (900)
*/
@Mutation(() => GeneratedDocumentDTO, {
name: 'generateReturnByMoneyStatementDocument',
description: 'Сгенерировать документ заявления на возврат паевого взноса',
})
@Throttle({ default: { limit: 3, ttl: 60000 } })
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async generateReturnByMoneyStatementDocument(
@Args('data', { type: () => ReturnByMoneyGenerateDocumentInputDTO })
data: ReturnByMoneyGenerateDocumentInputDTO,
@Args('options', { type: () => GenerateDocumentOptionsInputDTO, nullable: true })
options: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
return this.walletService.generateReturnByMoneyStatementDocument(data, options);
}
/**
* Mutation: Генерация документа решения совета о возврате паевого взноса (901)
*/
@Mutation(() => GeneratedDocumentDTO, {
name: 'generateReturnByMoneyDecisionDocument',
description: 'Сгенерировать документ решения совета о возврате паевого взноса',
})
@Throttle({ default: { limit: 3, ttl: 60000 } })
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async generateReturnByMoneyDecisionDocument(
@Args('data', { type: () => ReturnByMoneyDecisionGenerateDocumentInputDTO })
data: ReturnByMoneyDecisionGenerateDocumentInputDTO,
@Args('options', { type: () => GenerateDocumentOptionsInputDTO, nullable: true })
options: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
return this.walletService.generateReturnByMoneyDecisionDocument(data, options);
}
/**
* Mutation: Создание заявки на вывод средств
*/
@Mutation(() => CreateWithdrawResponseDTO, {
name: 'createWithdraw',
description: 'Создать заявку на вывод средств',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async createWithdraw(@Args('input') input: CreateWithdrawInputDTO): Promise<CreateWithdrawResponseDTO> {
return this.walletService.createWithdraw(input);
}
}
@@ -0,0 +1,59 @@
import { Injectable } from '@nestjs/common';
import { WalletDomainInteractor } from '~/domain/wallet/interactors/wallet.interactor';
import { GenerateDocumentOptionsInputDTO } from '~/modules/document/dto/generate-document-options-input.dto';
import { GeneratedDocumentDTO } from '~/modules/document/dto/generated-document.dto';
import { ReturnByMoneyGenerateDocumentInputDTO } from '~/modules/document/documents-dto/return-by-money-statement.dto';
import { ReturnByMoneyDecisionGenerateDocumentInputDTO } from '~/modules/document/documents-dto/return-by-money-decision.dto';
import { CreateWithdrawInputDTO } from '../dto/create-withdraw-input.dto';
import { CreateWithdrawResponseDTO } from '../dto/create-withdraw-response.dto';
import { Cooperative } from 'cooptypes';
/**
* Сервис для работы с wallet модулем
*/
@Injectable()
export class WalletService {
constructor(private readonly walletDomainInteractor: WalletDomainInteractor) {}
/**
* Генерация документа заявления на возврат паевого взноса (900)
*/
public async generateReturnByMoneyStatementDocument(
data: ReturnByMoneyGenerateDocumentInputDTO,
options?: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
// Устанавливаем registry_id для документа заявления
data.registry_id = Cooperative.Registry.ReturnByMoney.registry_id;
const document = await this.walletDomainInteractor.generateReturnByMoneyStatementDocument(data, options || {});
//TODO чтобы избавиться от unknown необходимо строго типизировать ответ фабрики документов
return document as unknown as GeneratedDocumentDTO;
}
/**
* Генерация документа решения совета о возврате паевого взноса (901)
*/
public async generateReturnByMoneyDecisionDocument(
data: ReturnByMoneyDecisionGenerateDocumentInputDTO,
options?: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
// Устанавливаем registry_id для документа решения
data.registry_id = Cooperative.Registry.ReturnByMoneyDecision.registry_id;
const document = await this.walletDomainInteractor.generateReturnByMoneyDecisionDocument(data, options || {});
//TODO чтобы избавиться от unknown необходимо строго типизировать ответ фабрики документов
return document as unknown as GeneratedDocumentDTO;
}
/**
* Создание заявки на вывод средств
*/
public async createWithdraw(data: CreateWithdrawInputDTO): Promise<CreateWithdrawResponseDTO> {
const result = await this.walletDomainInteractor.createWithdraw(data);
const response = new CreateWithdrawResponseDTO();
response.withdraw_hash = result.withdraw_hash;
return response;
}
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { WalletResolver } from './resolvers/wallet.resolver';
import { WalletService } from './services/wallet.service';
import { WalletDomainInteractor } from '~/domain/wallet/interactors/wallet.interactor';
/**
* Модуль wallet для управления выводом средств и генерацией документов
*/
@Module({
providers: [WalletResolver, WalletService, WalletDomainInteractor],
exports: [WalletService, WalletDomainInteractor],
})
export class WalletModule {}
@@ -3,7 +3,7 @@
* Do not modify this file manually
*/
export type IInstall = ({
export type IInstall = {
individual_data: {
birthdate: string;
email: string;
@@ -14,10 +14,10 @@ export type IInstall = ({
phone: string;
};
role: 'chairman' | 'member';
})[];
}[];
export interface RInstall {
body: ({
body: {
individual_data: {
birthdate: string;
email: string;
@@ -28,5 +28,5 @@ export interface RInstall {
phone: string;
};
role: 'chairman' | 'member';
})[];
}[];
}
@@ -116,19 +116,21 @@ export interface IRecieveIPN {
}
export interface ISavePaymentMethod {
data: {
phone: string;
} | ({
account_number: string;
bank_name: string;
card_number?: string;
currency: 'RUB' | 'Other';
details: {
bik: string;
corr: string;
kpp: string;
data:
| {
phone: string;
}
| {
account_number: string;
bank_name: string;
card_number?: string;
currency: 'RUB' | 'Other';
details: {
bik: string;
corr: string;
kpp: string;
};
};
});
method_id: number;
method_type: 'sbp' | 'bank_transfer';
username: string;
@@ -155,19 +157,21 @@ export interface RGetListPaymentMethods {
export interface RSavePaymentMethod {
body: {
data: {
phone: string;
} | ({
account_number: string;
bank_name: string;
card_number?: string;
currency: 'RUB' | 'Other';
details: {
bik: string;
corr: string;
kpp: string;
data:
| {
phone: string;
}
| {
account_number: string;
bank_name: string;
card_number?: string;
currency: 'RUB' | 'Other';
details: {
bik: string;
corr: string;
kpp: string;
};
};
});
method_id: number;
method_type: 'sbp' | 'bank_transfer';
username: string;
@@ -1,14 +1,10 @@
const getActiveTemplateForAction = (registry_id: string, drafts: any, translations: any) => {
if (drafts && translations) {
const draft: any = Object.values(drafts).find(
(el: any) => el.registry_id == registry_id
)
const translation:any = Object.values(translations).find(
(el: any) => el.id == draft.default_translation_id
)
if (drafts && translations) {
const draft: any = Object.values(drafts).find((el: any) => el.registry_id == registry_id);
const translation: any = Object.values(translations).find((el: any) => el.id == draft.default_translation_id);
return { context: draft.context, translation: JSON.parse(translation.data) }
} else return {}
}
return { context: draft.context, translation: JSON.parse(translation.data) };
} else return {};
};
export default getActiveTemplateForAction
export default getActiveTemplateForAction;
@@ -2,7 +2,6 @@ function getInternalAction(data, actionName) {
// Вспомогательная функция для рекурсивного поиска
function searchTraces(traces) {
for (const trace of traces) {
if (trace.act.name === actionName) {
return trace.act.data; // Предполагается, что вам нужны данные, а не сам act
}
@@ -4,23 +4,23 @@ const { verify, sha256 } = eosjsecc;
async function verifySignature(doc: DocumentInterface, hash) {
let isValidSignature = false;
let isValidHash = hash === doc.hash;
let reason = "";
const isValidHash = hash === doc.hash;
let reason = '';
try {
isValidSignature = await verify(doc.signature, doc.hash, doc.public_key);
} catch (e) {
reason = "wrong signature";
reason = 'wrong signature';
}
if (!isValidHash) {
reason = "hash is not equal doc hash";
reason = 'hash is not equal doc hash';
}
return {
is_valid_signature: isValidSignature,
is_valid_hash: isValidHash,
reason: reason,
is_valid: isValidSignature && isValidHash
is_valid: isValidSignature && isValidHash,
};
}
@@ -99,7 +99,7 @@ describe('Проверка данных', () => {
skip_save: false,
};
let res = await request(app)
const res = await request(app)
.post('/v1/documents/generate')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(options);
@@ -135,7 +135,7 @@ describe('Проверка данных', () => {
skip_save: false,
};
let res = await request(app)
const res = await request(app)
.post('/v1/documents/generate')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(options);
@@ -147,7 +147,7 @@ describe('Проверка данных', () => {
expect(res.body.html).toBeDefined();
expect(res.body.hash).toBeDefined();
let res_regenerated = await request(app)
const res_regenerated = await request(app)
.post('/v1/documents/generate')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(res.body.meta);
@@ -175,7 +175,7 @@ describe('Проверка данных', () => {
signature: 'any imaged signature',
skip_save: false,
};
let res = await request(app)
const res = await request(app)
.post('/v1/documents/generate')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(options);
@@ -195,7 +195,7 @@ describe('Проверка данных', () => {
statement: signedDocument,
};
let joincoop_result = await request(app)
const joincoop_result = await request(app)
.post('/v1/users/join-cooperative')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(joinCoopData);
@@ -220,7 +220,7 @@ describe('Проверка данных', () => {
signature: 'any imaged signature',
skip_save: false,
};
let res = await request(app)
const res = await request(app)
.post('/v1/documents/generate')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(options);
@@ -246,7 +246,7 @@ describe('Проверка данных', () => {
statement: signedDocument,
};
let joincoop_result = await request(app)
const joincoop_result = await request(app)
.post('/v1/users/join-cooperative')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(joinCoopData);
@@ -276,7 +276,7 @@ describe('Проверка данных', () => {
skip_save: false,
};
let res = await request(app)
const res = await request(app)
.post('/v1/documents/generate')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(options);
@@ -288,7 +288,7 @@ describe('Проверка данных', () => {
expect(res.body.html).toBeDefined();
expect(res.body.hash).toBeDefined();
let res_regenerated = await request(app)
const res_regenerated = await request(app)
.post('/v1/documents/generate')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(res.body.meta);
@@ -315,7 +315,7 @@ describe('Проверка данных', () => {
statement: signedDocument,
};
let joincoop_result = await request(app)
const joincoop_result = await request(app)
.post('/v1/users/join-cooperative')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(joinCoopData);
@@ -5,7 +5,7 @@ export const registry_id = 900
// Интерфейс запроса без чувствительных данных
export interface IMoneyReturnRequest {
method_id: string // ID платежного метода вместо банковских реквизитов
method_id: string // ID платежного метода
amount: string
currency: string
}
@@ -33,7 +33,7 @@ export interface Model {
export const title = 'Заявление на возврат паевого взноса денежными средствами'
export const description = 'Форма заявления на возврат паевого взноса денежными средствами'
export const context = '<div class="digital-document"><div style="text-align: right; margin:">\n<p style="margin: 0px !important">{% trans \'v_soviet\' %} {{ vars.full_abbr_genitive}} "{{vars.name}}"</p>\n<p style="margin: 0px !important">{% trans \'from_participant\' %}</p>\n<p style="margin: 0px !important">{{ user.full_name_or_short_name}}</p>\n</div>\n<div style="text-align: center">\n<h1 class="header">{% trans \'statement\' %}</h1>\n</div>\n<p>{% trans \'money_return_request\', coop.short_name %}</p>\n<table>\n<tbody>\n<tr>\n<th>{% trans \'asset_form\' %}</th>\n<td>{% trans \'money_assets\' %}</td>\n</tr>\n<tr>\n<th>{% trans \'name_requisites\' %}</th>\n<td>{% trans \'return_part_contribution\' %}</td>\n</tr>\n<tr>\n<th>№</th>\n<td>1</td>\n</tr>\n<tr>\n<th>{% trans \'payment_details_field\' %}</th>\n<td style="white-space: pre-line;">{{ request.payment_details }}</td>\n</tr>\n<tr>\n<th>{% trans \'article\' %}</th>\n<td>na</td>\n</tr>\n<tr>\n<th>{% trans \'unit_measurement\' %}</th>\n<td>{{ request.currency }}</td>\n</tr>\n<tr>\n<th>{% trans \'quantity\' %}</th>\n<td>{{ request.amount }}</td>\n</tr>\n<tr>\n<th>{% trans \'unit_cost\' %}</th>\n<td>na</td>\n</tr>\n<tr>\n<th>{% trans \'total_cost\' %}</th>\n<td>{{ request.amount }} {{ request.currency }}</td>\n</tr>\n</tbody>\n</table>\n<p>{% trans \'signature\' %}</p>\n<p>{{ user.full_name_or_short_name }}</p>\n<p>{{ meta.created_at }}</p>\n\n<style>\n.digital-document {\npadding: 20px;\nwhite-space: pre-wrap;\n};\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth, td {\n border: 1px solid #ccc;\n padding: 8px;\n text-align: left;\n word-wrap: break-word; \n overflow-wrap: break-word; \n}\nth {\n background-color: #f4f4f4;\n width: 30%;\n}\n</style>\n\n'
export const context = `<div class="digital-document"><div style="text-align: right; margin:"><p style="margin: 0px !important">{% trans 'v_soviet' %} {{ vars.full_abbr_genitive}} "{{vars.name}}"</p><p style="margin: 0px !important">{% trans 'from_participant' %}</p><p style="margin: 0px !important">{{ user.full_name_or_short_name}}</p></div><div style="text-align: center; padding-top: 20px; padding-bottom: 20px;"><h1 class="header">{% trans 'statement' %}</h1></div><p style="padding-bottom: 20px;">{% trans 'money_return_request', coop.short_name %}</p><table style="margin-bottom: 20px;"><tbody><tr><th>{% trans 'asset_form' %}</th><td>{% trans 'money_assets' %}</td></tr><tr><th>{% trans 'name_requisites' %}</th><td>{% trans 'return_part_contribution' %}</td></tr><tr><th>№</th><td>1</td></tr><tr><th>{% trans 'payment_details_field' %}</th><td style="white-space: pre-line;">{{ request.payment_details }}</td></tr><tr><th>{% trans 'article' %}</th><td>na</td></tr><tr><th>{% trans 'unit_measurement' %}</th><td>{{ request.currency }}</td></tr><tr><th>{% trans 'quantity' %}</th><td>{{ request.amount }}</td></tr><tr><th>{% trans 'unit_cost' %}</th><td>na</td></tr><tr><th>{% trans 'total_cost' %}</th><td>{{ request.amount }} {{ request.currency }}</td></tr></tbody></table><p style="padding-top: 20px;">{% trans 'signature' %}</p><p>{{ user.full_name_or_short_name }}</p><p>{{ meta.created_at }}</p><style>.digital-document {padding: 20px;white-space: pre-wrap;};table {width: 100%;border-collapse: collapse;}th, td {border: 1px solid #ccc;padding: 8px;text-align: left;word-wrap: break-word; overflow-wrap: break-word; }th {background-color: #f4f4f4;width: 30%;}</style>`
export const translations = {
ru: {
@@ -53,7 +53,6 @@ export const translations = {
money_assets: 'Денежные средства',
signature: 'Подписано электронной подписью.',
},
// ... другие переводы
}
export const exampleData = {
@@ -0,0 +1,101 @@
import type { IDecisionData, IGenerate, IMetaDocument } from '../../document'
import type { ICommonUser, ICooperativeData, IVars } from '../../model'
export const registry_id = 901
/**
* Интерфейс генерации решения совета по возврату паевого взноса
*/
export interface Action extends IGenerate {
decision_id: number
payment_hash: string // Хэш платежа
amount: string
currency: string
}
export type Meta = IMetaDocument & Action
// Модель данных
export interface Model {
meta: IMetaDocument
coop: ICooperativeData
decision: IDecisionData
user: ICommonUser
amount: string
currency: string
vars: IVars
}
export const title = 'Решение совета о возврате паевого взноса'
export const description = 'Форма решения совета о возврате паевого взноса денежными средствами'
export const context = `<style> h1 {margin: 0px; text-align:center;}h3{margin: 0px;padding-top: 15px;}.about {padding: 20px;}.digital-document {padding: 20px;white-space: pre-wrap;}.subheader {padding-bottom: 20px; }table {width: 100%;border-collapse: collapse;}th, td {border: 1px solid #ccc;padding: 8px;text-align: left;word-wrap: break-word; overflow-wrap: break-word; }th {background-color: #f4f4f4;width: 30%;}</style><div class="digital-document"><h1 class="header">{% trans 'protocol_number', decision.id %}</h1><p style="text-align:center" class="subheader">{% trans 'council_meeting_name' %} {{vars.full_abbr_genitive}} "{{vars.name}}"</p><p style="text-align: right; padding-bottom: 20px;"> {{ meta.created_at }}, {{ coop.city }}</p><table class="about" style="margin-bottom: 20px;"><tbody><tr><th>{% trans 'meeting_format' %}</th><td>{% trans 'meeting_format_value' %}</td></tr><tr><th>{% trans 'meeting_place' %}</th><td>{{ coop.full_address }}</td></tr><tr><th>{% trans 'meeting_date' %}</th><td>{{ decision.date }}</td></tr><tr><th>{% trans 'opening_time' %}</th><td>{{ decision.time }}</td></tr></tbody></table><h3 style="padding-top: 30px; padding-bottom: 10px;">{% trans 'council_members' %}</h3><table style="margin-bottom: 20px;"><tbody>{% for member in coop.members %}<tr><th>{% if member.is_chairman %}{% trans 'chairman_of_the_council' %}{% else %}{% trans 'member_of_the_council' %}{% endif %}</th><td>{{ member.last_name }} {{ member.first_name }} {{ member.middle_name }}</td></tr>{% endfor %}</tbody></table><h3 style="padding-top: 30px; padding-bottom: 10px;">{% trans 'meeting_legality' %}</h3><p style="padding-bottom: 20px;">{% trans 'voting_results', decision.voters_percent %} {% trans 'quorum' %} {% trans 'chairman_of_the_meeting', coop.chairman.last_name, coop.chairman.first_name, coop.chairman.middle_name %}.</p><h3 style="padding-top: 30px; padding-bottom: 10px;">{% trans 'agenda' %}</h3><p style="padding-bottom: 10px;">{% trans 'return_application_consideration' %}:</p><p style="padding-bottom: 20px;">- {{ user.full_name_or_short_name }}, {{ amount }} {{ currency }}</p><h3 style="padding-top: 30px; padding-bottom: 10px;">{% trans 'voting' %}</h3><p style="padding-bottom: 20px;">{% trans 'vote_results' %} {% trans 'votes_for_label' %} {{ decision.votes_for }}; {% trans 'votes_against_label' %} - {{ decision.votes_against }}; {% trans 'votes_abstained_label' %} - {{ decision.votes_abstained }}.</p><h3 style="padding-top: 30px; padding-bottom: 10px;">{% trans 'decision_made' %}</h3><p style="padding-bottom: 10px;">{% trans 'return_decision_text' %}:</p><p style="padding-bottom: 20px;">- {{ user.full_name_or_short_name }}, {{ amount }} {{ currency }}</p><hr><p style="padding-top: 20px;">{% trans 'closing_time', decision.time %}</p><div class="signature" style="padding-top: 20px;"><p>{% trans 'signature' %}</p><p>{% trans 'chairman' %} {{ coop.chairman.last_name }} {{ coop.chairman.first_name }} {{ coop.chairman.middle_name }}</p></div></div>`
export const translations = {
ru: {
meeting_format: 'Форма',
meeting_date: 'Дата',
meeting_place: 'Место',
opening_time: 'Время открытия',
council_members: 'ЧЛЕНЫ СОВЕТА',
voting_results: 'Количество голосов составляет {0}% от общего числа членов Совета.',
meeting_legality: 'СОБРАНИЕ ПРАВОМОЧНО',
chairman_of_the_meeting: 'Председатель собрания совета: {0} {1} {2}',
agenda: 'ПОВЕСТКА ДНЯ',
decision_made: 'РЕШИЛИ',
closing_time: 'Время закрытия собрания совета: {0}.',
protocol_number: 'ПРОТОКОЛ № {0}',
council_meeting_name: 'Собрания Совета',
chairman_of_the_council: 'Председатель совета',
signature: 'Документ подписан электронной подписью.',
chairman: 'Председатель',
quorum: 'Кворум для решения поставленных на повестку дня вопросов имеется.',
voting: 'ГОЛОСОВАНИЕ',
meeting_format_value: 'заочная',
member_of_the_council: 'Член совета',
return_application_consideration: 'Рассмотрение заявления на возврат паевого взноса пайщика',
vote_results: 'По первому вопросу повестки дня проголосовали:',
votes_for_label: '«За»',
votes_against_label: '«Против»',
votes_abstained_label: '«Воздержался»',
return_decision_text: 'Вернуть паевой взнос пайщика по реквизитам, указанным в заявлении',
},
}
export const exampleData = {
meta: {
created_at: '18.03.2025 14:30',
},
coop: {
city: 'Москва',
full_address: 'г. Москва, ул. Центральная, д. 1',
chairman: {
last_name: 'Петров',
first_name: 'Петр',
middle_name: 'Петрович',
},
},
decision: {
time: '14:30',
date: '18.03.2025',
votes_for: '5',
votes_against: '0',
votes_abstained: '1',
voters_percent: '100',
id: '5',
},
user: {
full_name_or_short_name: 'Иванов Иван Иванович',
},
vars: {
full_abbr_genitive: 'Потребительского Кооператива',
name: 'ВОСХОД',
},
member: {
is_chairman: '',
last_name: '',
first_name: '',
middle_name: '',
},
amount: '',
currency: '',
}
@@ -20,6 +20,7 @@ export * as ReturnByAssetDecision from './801.ReturnByAssetDecision'
export * as ReturnByAssetAct from './802.ReturnByAssetAct'
export * as ReturnByMoney from './900.ReturnByMoney'
export * as ReturnByMoneyDecision from './901.ReturnByMoneyDecision'
export * as InvestmentAgreement from './1000.InvestmentAgreement'
export * as InvestByResultStatement from './1001.InvestByResultStatement'
@@ -1,42 +1,42 @@
import { Cooperative, SovietContract } from 'cooptypes'
import { useGenerateFreeDecision } from 'src/features/FreeDecision/GenerateDecision'
import { useGenerateParticipantApplicationDecision } from 'src/features/Decision/ParticipantApplication'
import { useGenerateSovietDecisionOnAnnualMeet } from 'src/features/Meet/GenerateSovietDecision/model'
import { useSystemStore } from 'src/entities/System/model'
import { useSessionStore } from 'src/entities/Session'
import { useGlobalStore } from 'src/shared/store'
import { useVoteForDecision } from 'src/features/Decision/VoteForDecision'
import { useVoteAgainstDecision } from 'src/features/Decision/VoteAgainstDecision'
import { computed } from 'vue'
import { useAgendaStore } from 'src/entities/Agenda/model'
import type { IAgenda } from 'src/entities/Agenda/model'
import { DigitalDocument } from 'src/shared/lib/document'
import type { IUserCertificateUnion } from 'src/shared/lib/types/certificate'
import { getNameFromCertificate } from 'src/shared/lib/utils/getNameFromCertificate'
import { Cooperative, SovietContract } from 'cooptypes';
import { useGenerateFreeDecision } from 'src/features/FreeDecision/GenerateDecision';
import { useGenerateParticipantApplicationDecision } from 'src/features/Decision/ParticipantApplication';
import { useGenerateSovietDecisionOnAnnualMeet } from 'src/features/Meet/GenerateSovietDecision/model';
import { useSystemStore } from 'src/entities/System/model';
import { useSessionStore } from 'src/entities/Session';
import { useGlobalStore } from 'src/shared/store';
import { useVoteForDecision } from 'src/features/Decision/VoteForDecision';
import { useVoteAgainstDecision } from 'src/features/Decision/VoteAgainstDecision';
import { computed } from 'vue';
import { useAgendaStore } from 'src/entities/Agenda/model';
import type { IAgenda } from 'src/entities/Agenda/model';
import { DigitalDocument } from 'src/shared/lib/document';
import type { IUserCertificateUnion } from 'src/shared/lib/types/certificate';
import { getNameFromCertificate } from 'src/shared/lib/utils/getNameFromCertificate';
/**
* Процесс обработки решений
* Координирует работу различных фич для генерации и обработки решений различных типов
*/
export function useDecisionProcessor() {
const { info } = useSystemStore()
const session = useSessionStore()
const agendaStore = useAgendaStore()
const { info } = useSystemStore();
const session = useSessionStore();
const agendaStore = useAgendaStore();
// Данные повестки и состояние загрузки
const decisions = computed(() => agendaStore.agenda)
const loading = computed(() => agendaStore.loading)
const decisions = computed(() => agendaStore.agenda);
const loading = computed(() => agendaStore.loading);
/**
* Форматирует заголовок вопроса
*/
function formatDecisionTitle(title: string, cert: IUserCertificateUnion) {
let result = 'Вопрос на голосование'
let result = 'Вопрос на голосование';
const name = getNameFromCertificate(cert)
result = `${title} от ${name}`
const name = getNameFromCertificate(cert);
result = `${title} от ${name}`;
return result
return result;
}
/**
@@ -45,24 +45,27 @@ export function useDecisionProcessor() {
function getDocumentTitle(row: IAgenda) {
// Используем только агрегаты документов
if (row.documents?.statement?.documentAggregate?.rawDocument?.full_title) {
return row.documents.statement.documentAggregate.rawDocument.full_title
return row.documents.statement.documentAggregate.rawDocument.full_title;
}
if (row.documents?.decision?.documentAggregate?.rawDocument?.full_title) {
return row.documents.decision.documentAggregate.rawDocument.full_title
return row.documents.decision.documentAggregate.rawDocument.full_title;
}
// Поддержка исходного формата для таблицы
if (row.table?.statement?.meta && typeof row.table.statement.meta === 'string') {
if (
row.table?.statement?.meta &&
typeof row.table.statement.meta === 'string'
) {
try {
const meta = JSON.parse(row.table.statement.meta)
if (meta.title) return meta.title
const meta = JSON.parse(row.table.statement.meta);
if (meta.title) return meta.title;
} catch (e) {
// Игнорируем ошибку парсинга JSON
}
}
return 'Вопрос без заголовка'
return 'Вопрос без заголовка';
}
/**
@@ -70,10 +73,10 @@ export function useDecisionProcessor() {
*/
async function loadDecisions(coopname: string, hidden = false) {
try {
const result = await agendaStore.loadAgenda({ coopname }, hidden)
return result
const result = await agendaStore.loadAgenda({ coopname }, hidden);
return result;
} catch (error) {
throw error
throw error;
}
}
@@ -83,19 +86,19 @@ export function useDecisionProcessor() {
function getDocumentHash(row: IAgenda) {
// Используем только агрегаты документов
if (row.documents?.statement?.documentAggregate?.rawDocument?.hash) {
return row.documents.statement.documentAggregate.rawDocument.hash
return row.documents.statement.documentAggregate.rawDocument.hash;
}
if (row.documents?.decision?.documentAggregate?.rawDocument?.hash) {
return row.documents.decision.documentAggregate.rawDocument.hash
return row.documents.decision.documentAggregate.rawDocument.hash;
}
// Поддержка исходного формата для таблицы
if (row.table?.statement?.hash) {
return row.table.statement.hash
return row.table.statement.hash;
}
return null
return null;
}
/**
@@ -103,58 +106,76 @@ export function useDecisionProcessor() {
*/
async function generateDecisionDocument(row: IAgenda) {
if (!row.table?.id || !row.table?.username || !row.table?.type) {
throw new Error('Некорректные данные решения')
throw new Error('Некорректные данные решения');
}
const decision_id = Number(row.table.id)
const username = row.table.username
const type = row.table.type
const registry_id = Cooperative.Document.decisionsRegistry[type]
const decision_id = Number(row.table.id);
const username = row.table.username;
const type = row.table.type;
const registry_id = Cooperative.Document.decisionsRegistry[type];
// Генерация документа в зависимости от типа решения
let document
let document;
if (registry_id === Cooperative.Registry.FreeDecision.registry_id) {
const unparsedDocumentMeta = row.table.statement.meta == '' ? '{}' : row.table.statement.meta
const parsedDocumentMeta = JSON.parse(unparsedDocumentMeta) as Cooperative.Registry.FreeDecision.Action
const project_id = parsedDocumentMeta.project_id
const unparsedDocumentMeta =
row.table.statement.meta == '' ? '{}' : row.table.statement.meta;
const parsedDocumentMeta = JSON.parse(
unparsedDocumentMeta,
) as Cooperative.Registry.FreeDecision.Action;
const project_id = parsedDocumentMeta.project_id;
const { generateFreeDecision } = useGenerateFreeDecision()
const { generateFreeDecision } = useGenerateFreeDecision();
document = await generateFreeDecision({
username: username,
decision_id: decision_id,
project_id: project_id
})
}
else if (registry_id === Cooperative.Registry.DecisionOfParticipantApplication.registry_id) {
const { generateParticipantApplicationDecision } = useGenerateParticipantApplicationDecision()
project_id: project_id,
});
} else if (
registry_id ===
Cooperative.Registry.DecisionOfParticipantApplication.registry_id
) {
const { generateParticipantApplicationDecision } =
useGenerateParticipantApplicationDecision();
document = await generateParticipantApplicationDecision({
username: username,
decision_id: decision_id
})
}
else if (registry_id === Cooperative.Registry.AnnualGeneralMeetingSovietDecision.registry_id) {
const { generateSovietDecisionOnAnnualMeet } = useGenerateSovietDecisionOnAnnualMeet()
const unparsedDocumentMeta = row.table.statement.meta == '' ? '{}' : row.table.statement.meta
const parsedDocumentMeta = JSON.parse(unparsedDocumentMeta) as Cooperative.Registry.AnnualGeneralMeetingAgenda.Action
const is_repeated = parsedDocumentMeta.is_repeated || false
decision_id: decision_id,
});
} else if (
registry_id ===
Cooperative.Registry.AnnualGeneralMeetingSovietDecision.registry_id
) {
const { generateSovietDecisionOnAnnualMeet } =
useGenerateSovietDecisionOnAnnualMeet();
const unparsedDocumentMeta =
row.table.statement.meta == '' ? '{}' : row.table.statement.meta;
const parsedDocumentMeta = JSON.parse(
unparsedDocumentMeta,
) as Cooperative.Registry.AnnualGeneralMeetingAgenda.Action;
const is_repeated = parsedDocumentMeta.is_repeated || false;
document = await generateSovietDecisionOnAnnualMeet({
username: username,
decision_id: decision_id,
meet_hash: row.table.hash as string,
is_repeated
})
}
else {
throw new Error('Неизвестный тип решения')
is_repeated,
});
} else if (registry_id === Cooperative.Registry.ReturnByMoney.registry_id) {
// const { generateReturnByMoneyDecision } = useGenerateReturnByMoneyDecision()
// document = await generateReturnByMoneyDecision({
// username: username,
// decision_id: decision_id
// })
} else {
console.log('Неизвестный тип решения', registry_id);
throw new Error('Неизвестный тип решения');
}
if (!document) {
throw new Error('Ошибка при генерации документа решения')
throw new Error('Ошибка при генерации документа решения');
}
return document
return document;
}
/**
@@ -162,31 +183,35 @@ export function useDecisionProcessor() {
*/
async function authorizeAndExecuteDecision(row: IAgenda) {
if (!row.table?.id) {
throw new Error('Некорректные данные решения')
throw new Error('Некорректные данные решения');
}
const decision_id = Number(row.table.id)
const decision_id = Number(row.table.id);
// Генерируем документ решения
const document = await generateDecisionDocument(row)
const document = await generateDecisionDocument(row);
// Создаем экземпляр класса DigitalDocument и подписываем документ
const digitalDocument = new DigitalDocument(document)
const signedDocument = await digitalDocument.sign(session.username)
const digitalDocument = new DigitalDocument(document);
const signedDocument = await digitalDocument.sign(session.username);
// Подготавливаем данные для транзакций
const authorizeData: SovietContract.Actions.Decisions.Authorize.IAuthorize = {
coopname: info.coopname,
chairman: session.username,
decision_id,
document: {...signedDocument, meta: JSON.stringify(signedDocument.meta)},
}
const authorizeData: SovietContract.Actions.Decisions.Authorize.IAuthorize =
{
coopname: info.coopname,
chairman: session.username,
decision_id,
document: {
...signedDocument,
meta: JSON.stringify(signedDocument.meta),
},
};
const execData: SovietContract.Actions.Decisions.Exec.IExec = {
executer: session.username,
coopname: info.coopname,
decision_id: decision_id,
}
};
// Выполняем транзакции
await useGlobalStore().transact([
@@ -212,9 +237,9 @@ export function useDecisionProcessor() {
],
data: execData,
},
])
]);
return true
return true;
}
/**
@@ -249,21 +274,21 @@ export function useDecisionProcessor() {
* Проверяет, проголосовал ли текущий пользователь "за" решение
*/
function isVotedFor(decision: SovietContract.Tables.Decisions.IDecision) {
return decision.votes_for.includes(session.username)
return decision.votes_for.includes(session.username);
}
/**
* Проверяет, проголосовал ли текущий пользователь "против" решения
*/
function isVotedAgainst(decision: SovietContract.Tables.Decisions.IDecision) {
return decision.votes_against.includes(session.username)
return decision.votes_against.includes(session.username);
}
/**
* Проверяет, проголосовал ли текущий пользователь за решение каким-либо образом
*/
function isVotedAny(decision: SovietContract.Tables.Decisions.IDecision) {
return isVotedAgainst(decision) || isVotedFor(decision)
return isVotedAgainst(decision) || isVotedFor(decision);
}
return {
@@ -279,6 +304,6 @@ export function useDecisionProcessor() {
isVotedAny,
formatDecisionTitle,
getDocumentTitle,
getDocumentHash
}
getDocumentHash,
};
}
@@ -1,189 +1,230 @@
<template lang="pug">
div.scroll-area(style="height: calc(100% - $toolbar-min-height); overflow-y: auto;")
q-table(
ref="tableRef"
flat
:grid="isMobile"
:rows="decisions"
:columns="columns"
:table-colspan="9"
row-key="table.id"
:pagination="pagination"
virtual-scroll
:virtual-scroll-item-size="48"
:rows-per-page-options="[10]"
:loading="loading"
:no-data-label="'У совета нет вопросов на повестке для голосования. Вопросы на повестку добавляются автоматически при участии пайщиков в цифровых целевых потребительских программах кооператива. Также, вопрос на повестку можно добавить вручную, нажав на кнопку `ПРЕДЛОЖИТЬ ПОВЕСТКУ`.'"
:virtual-scroll-target="'.scroll-area'"
).full-width
.scroll-area(style='height: calc(100% - $toolbar-min-height); overflow-y: auto')
q-table.full-width(
ref='tableRef',
flat,
:grid='isMobile',
:rows='decisions',
:columns='columns',
:table-colspan='9',
row-key='table.id',
:pagination='pagination',
virtual-scroll,
:virtual-scroll-item-size='48',
:rows-per-page-options='[10]',
:loading='loading',
:no-data-label='"У совета нет вопросов на повестке для голосования. Вопросы на повестку добавляются автоматически при участии пайщиков в цифровых целевых потребительских программах кооператива. Также, вопрос на повестку можно добавить вручную, нажав на кнопку `ПРЕДЛОЖИТЬ ПОВЕСТКУ`."',
:virtual-scroll-target='".scroll-area"'
)
template(#top, v-if='$slots.top')
slot(name='top')
template(#top v-if="$slots.top")
slot(name="top")
template(#item="props")
template(#item='props')
QuestionCard(
:agenda="props.row"
:expanded="expanded.get(props.row.table.id)"
:is-processing="isProcessing(props.row.table.id)"
:is-voted-for="isVotedFor"
:is-voted-against="isVotedAgainst"
:is-voted-any="isVotedAny"
@toggle-expand="toggleExpand(props.row.table.id)"
@authorize="onAuthorizeDecision(props.row)"
@vote-for="onVoteFor(props.row)"
@vote-against="onVoteAgainst(props.row)"
:agenda='props.row',
:expanded='expanded.get(props.row.table.id)',
:is-processing='isProcessing(props.row.table.id)',
:is-voted-for='isVotedFor',
:is-voted-against='isVotedAgainst',
:is-voted-any='isVotedAny',
@toggle-expand='toggleExpand(props.row.table.id)',
@authorize='onAuthorizeDecision(props.row)',
@vote-for='onVoteFor(props.row)',
@vote-against='onVoteAgainst(props.row)'
)
template(#header="props")
q-tr(:props="props")
template(#header='props')
q-tr(:props='props')
q-th(auto-width)
q-th(
v-for="col in props.cols"
:key="col.name"
:props="props"
) {{ col.label }}
q-th(v-for='col in props.cols', :key='col.name', :props='props') {{ col.label }}
template(#body="props")
q-tr(:key="`m_${props.row.table.id}`" :props="props")
template(#body='props')
q-tr(:key='`m_${props.row.table.id}`', :props='props')
q-td(auto-width)
q-btn(
size="sm"
color="primary"
dense
:icon="expanded.get(props.row.table.id) ? 'expand_more' : 'chevron_right'"
round
@click="toggleExpand(props.row.table.id)"
size='sm',
color='primary',
dense,
:icon='expanded.get(props.row.table.id) ? "expand_more" : "chevron_right"',
round,
@click='toggleExpand(props.row.table.id)'
)
q-td {{ props.row.table.id }}
q-td {{ getShortNameFromCertificate(props.row.table.username_certificate) || props.row.table.username }}
q-td(style="max-width: 200px; word-wrap: break-word; white-space: normal;") {{ getDecisionTitle(props.row) }}
q-td(
style='max-width: 200px; word-wrap: break-word; white-space: normal'
) {{ getDecisionTitle(props.row) }}
q-td {{formatToFromNow(props.row.table.expired_at)}}
q-td {{ formatToFromNow(props.row.table.expired_at) }}
q-td
VotingButtons(
:decision="props.row.table"
:is-voted-for="isVotedFor"
:is-voted-against="isVotedAgainst"
:is-voted-any="isVotedAny"
@vote-for="onVoteFor(props.row)"
@vote-against="onVoteAgainst(props.row)"
:decision='props.row.table',
:is-voted-for='isVotedFor',
:is-voted-against='isVotedAgainst',
:is-voted-any='isVotedAny',
@vote-for='onVoteFor(props.row)',
@vote-against='onVoteAgainst(props.row)'
)
q-td
q-btn(
size="sm"
color="teal"
push
v-if="isChairman"
:loading="isProcessing(props.row.table.id)"
@click="onAuthorizeDecision(props.row)"
size='sm',
color='teal',
push,
v-if='isChairman',
:loading='isProcessing(props.row.table.id)',
@click='onAuthorizeDecision(props.row)'
) утвердить
q-tr(no-hover v-if="expanded.get(props.row.table.id)" :key="`e_${props.row.table.id}`" :props="props" class="q-virtual-scroll--with-prev")
q-td(colspan="100%")
ComplexDocument(:documents="props.row.documents")
q-tr.q-virtual-scroll--with-prev(
no-hover,
v-if='expanded.get(props.row.table.id)',
:key='`e_${props.row.table.id}`',
:props='props'
)
q-td(colspan='100%')
ComplexDocument(:documents='props.row.documents')
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ComplexDocument } from 'src/shared/ui/ComplexDocument'
import { Cooperative } from 'cooptypes'
import { formatToFromNow } from 'src/shared/lib/utils/dates/formatToFromNow'
import { QuestionCard } from '../QuestionCard'
import { VotingButtons } from '../VotingButtons'
import { useWindowSize } from 'src/shared/hooks'
import type { IAgenda } from 'src/entities/Agenda/model'
import { getShortNameFromCertificate } from 'src/shared/lib/utils/getNameFromCertificate'
import { ref, reactive } from 'vue';
import { ComplexDocument } from 'src/shared/ui/ComplexDocument';
import { Cooperative } from 'cooptypes';
import { formatToFromNow } from 'src/shared/lib/utils/dates/formatToFromNow';
import { QuestionCard } from '../QuestionCard';
import { VotingButtons } from '../VotingButtons';
import { useWindowSize } from 'src/shared/hooks';
import type { IAgenda } from 'src/entities/Agenda/model';
import { getShortNameFromCertificate } from 'src/shared/lib/utils/getNameFromCertificate';
const props = defineProps({
decisions: {
type: Array,
required: true
required: true,
},
loading: {
type: Boolean,
required: true
required: true,
},
isChairman: {
type: Boolean,
default: false
default: false,
},
formatDecisionTitle: {
type: Function,
required: true
required: true,
},
isVotedFor: {
type: Function,
required: true
required: true,
},
isVotedAgainst: {
type: Function,
required: true
required: true,
},
isVotedAny: {
type: Function,
required: true
required: true,
},
processingDecisions: {
type: Object,
default: () => ({})
}
})
default: () => ({}),
},
});
const emit = defineEmits(['authorize', 'vote-for', 'vote-against'])
const emit = defineEmits(['authorize', 'vote-for', 'vote-against']);
const { isMobile } = useWindowSize()
const { isMobile } = useWindowSize();
// Получение заголовка для решения с поддержкой агрегатов документов
function getDecisionTitle(row: IAgenda) {
console.log('agenda', row)
// Используем только агрегаты документа
const title = (row.documents?.statement?.documentAggregate?.rawDocument?.meta as any)?.title
const title = (
row.documents?.statement?.documentAggregate?.rawDocument?.meta as any
)?.title;
if (title) {
const actor_certificate = row.documents?.statement?.action?.actor_certificate
return props.formatDecisionTitle(title, actor_certificate)
const actor_certificate =
row.documents?.statement?.action?.actor_certificate;
return props.formatDecisionTitle(title, actor_certificate);
}
// Запасной вариант
return 'Вопрос на голосование'
return 'Вопрос на голосование';
}
// Настройка таблицы
const columns = [
{ name: 'id', align: 'left', label: '№', field: row => row.table.id, sortable: true },
{ name: 'username', align: 'left', label: 'Заявитель', field: row => row.table.username, sortable: true },
{ name: 'caption', align: 'left', label: 'Пункт', field: row => getDecisionTitle(row), sortable: true },
{ name: 'expired_at', align: 'left', label: 'Истекает', field: row => row.table.expired_at, format: val => formatToFromNow(val), sortable: false },
{ name: 'approved', align: 'left', label: 'Голосование', field: row => row.table.approved, sortable: true },
{ name: 'authorized', align: 'left', label: '', field: row => row.table.authorized, sortable: true },
] as any
{
name: 'id',
align: 'left',
label: '№',
field: (row) => row.table.id,
sortable: true,
},
{
name: 'username',
align: 'left',
label: 'Заявитель',
field: (row) => row.table.username,
sortable: true,
},
{
name: 'caption',
align: 'left',
label: 'Пункт',
field: (row) => getDecisionTitle(row),
sortable: true,
},
{
name: 'expired_at',
align: 'left',
label: 'Истекает',
field: (row) => row.table.expired_at,
format: (val) => formatToFromNow(val),
sortable: false,
},
{
name: 'approved',
align: 'left',
label: 'Голосование',
field: (row) => row.table.approved,
sortable: true,
},
{
name: 'authorized',
align: 'left',
label: '',
field: (row) => row.table.authorized,
sortable: true,
},
] as any;
// Состояние UI
const expanded = reactive(new Map()) // Map для отслеживания состояния развертывания каждой записи
const tableRef = ref(null)
const pagination = ref({ rowsPerPage: 10 })
const expanded = reactive(new Map()); // Map для отслеживания состояния развертывания каждой записи
const tableRef = ref(null);
const pagination = ref({ rowsPerPage: 10 });
// UI методы
const toggleExpand = (id: any) => {
expanded.set(id, !expanded.get(id))
}
expanded.set(id, !expanded.get(id));
};
const isProcessing = (decisionId: number) => {
// Используем только processingDecisions из props
return Boolean(props.processingDecisions[decisionId])
}
return Boolean(props.processingDecisions[decisionId]);
};
// Обработчики событий
const onAuthorizeDecision = (row: Cooperative.Document.IComplexAgenda) => {
emit('authorize', row)
}
emit('authorize', row);
};
const onVoteFor = (row: Cooperative.Document.IComplexAgenda) => {
emit('vote-for', row)
}
emit('vote-for', row);
};
const onVoteAgainst = (row: Cooperative.Document.IComplexAgenda) => {
emit('vote-against', row)
}
emit('vote-against', row);
};
</script>
@@ -0,0 +1,79 @@
import { DraftContract } from 'cooptypes'
import { DocFactory } from '../Factory'
import type {
IGeneratedDocument,
IGenerationOptions,
IMetaDocument,
ITemplate,
} from '../Interfaces'
import type { MongoDBConnector } from '../Services/Databazor'
import { ReturnByMoneyDecision } from '../Templates'
export { ReturnByMoneyDecision as Template } from '../Templates'
export class Factory extends DocFactory<ReturnByMoneyDecision.Action> {
constructor(storage: MongoDBConnector) {
super(storage)
}
async generateDocument(
data: ReturnByMoneyDecision.Action,
options?: IGenerationOptions,
): Promise<IGeneratedDocument> {
let template: ITemplate<ReturnByMoneyDecision.Model>
if (process.env.SOURCE === 'local') {
template = ReturnByMoneyDecision.Template
}
else {
template = await this.getTemplate(DraftContract.contractName.production, ReturnByMoneyDecision.registry_id, data.block_num)
}
const user = await super.getUser(data.username, data.block_num)
const coop = await super.getCooperative(data.coopname, data.block_num)
const vars = await super.getVars(data.coopname, data.block_num)
// TODO необходимо строго типизировать мета-данные документов друг под друга!
const meta: IMetaDocument = await super.getMeta({
title: template.title,
...data,
}) // Генерируем мета-данные
const decision = await super.getDecision(
coop,
data.coopname,
data.decision_id,
meta.created_at,
)
const combinedData: ReturnByMoneyDecision.Model = {
vars,
meta,
coop,
decision,
user: super.getCommonUser(user),
amount: data.amount,
currency: data.currency,
}
// валидируем скомбинированные данные
await super.validate(combinedData, template.model)
// получаем комплекс перевода
const translation = template.translations[meta.lang]
// генерируем документ
const document: IGeneratedDocument = await super.generatePDF(
user.data,
template.context,
combinedData,
translation,
meta,
options?.skip_save,
)
// возвращаем
return document
}
}
+1
View File
@@ -19,6 +19,7 @@ export * as AssetContributionAct from './702.AssetContributionAct'
export * as ReturnByAssetDecision from './801.ReturnByAssetDecision'
export * as ReturnByAssetAct from './802.ReturnByAssetAct'
export * as ReturnByMoney from './900.ReturnByMoney'
export * as ReturnByMoneyDecision from './901.ReturnByMoneyDecision'
export * as InvestmentAgreement from './1000.InvestmentAgreement'
export * as InvestByResultStatement from './1001.InvestByResultStatement'
@@ -0,0 +1,60 @@
import type { JSONSchemaType } from 'ajv'
import { Cooperative } from 'cooptypes'
import type { ITemplate } from '../Interfaces'
import { IMetaJSONSchema } from '../Schema/MetaSchema'
import { CooperativeSchema, VarsSchema } from '../Schema'
import { decisionSchema } from '../Schema/DecisionSchema'
import { CommonUserSchema } from '../Schema/CommonUserSchema'
export const registry_id = Cooperative.Registry.ReturnByMoneyDecision.registry_id
// Модель действия для генерации
export type Action = Cooperative.Registry.ReturnByMoneyDecision.Action
// Модель данных
export type Model = Cooperative.Registry.ReturnByMoneyDecision.Model
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
properties: {
meta: {
type: 'object',
properties: {
...IMetaJSONSchema.properties,
},
required: [...IMetaJSONSchema.required],
additionalProperties: true,
},
coop: {
type: 'object',
properties: {
...CooperativeSchema.properties,
},
required: [...CooperativeSchema.required],
additionalProperties: true,
},
decision: {
type: 'object',
properties: {
...decisionSchema.properties,
},
required: [...decisionSchema.required],
additionalProperties: true,
},
user: CommonUserSchema,
amount: { type: 'string' },
currency: { type: 'string' },
vars: VarsSchema,
},
required: ['meta', 'coop', 'decision', 'user', 'amount', 'currency', 'vars'],
additionalProperties: false,
}
export const Template: ITemplate<Model> = {
title: Cooperative.Registry.ReturnByMoneyDecision.title,
description: Cooperative.Registry.ReturnByMoneyDecision.description,
model: Schema,
context: Cooperative.Registry.ReturnByMoneyDecision.context,
translations: Cooperative.Registry.ReturnByMoneyDecision.translations,
}
@@ -17,6 +17,7 @@ export * as ReturnByAssetDecision from './801.returnByAssetDecision'
export * as AssetContributionAct from './702.assetContributionAct'
export * as ReturnByAssetAct from './802.returnByAssetAct'
export * as ReturnByMoney from './900.returnByMoney'
export * as ReturnByMoneyDecision from './901.ReturnByMoneyDecision'
export * as InvestmentAgreement from './1000.InvestmentAgreement'
export * as InvestByResultStatement from './1001.InvestByResultStatement'
export * as InvestByResultAct from './1002.InvestByResultAct'
@@ -16,6 +16,7 @@ import * as ReturnByAssetDecision from './801.returnByAssetDecision'
import * as AssetContributionAct from './702.assetContributionAct'
import * as ReturnByAssetAct from './802.returnByAssetAct'
import * as ReturnByMoney from './900.returnByMoney'
import * as ReturnByMoneyDecision from './901.ReturnByMoneyDecision'
import * as InvestmentAgreement from './1000.InvestmentAgreement'
import * as InvestByResultStatement from './1001.InvestByResultStatement'
import * as InvestByResultAct from './1002.InvestByResultAct'
@@ -54,6 +55,7 @@ export const Registry = {
801: ReturnByAssetDecision,
802: ReturnByAssetAct,
900: ReturnByMoney,
901: ReturnByMoneyDecision,
1000: InvestmentAgreement,
1001: InvestByResultStatement,
1002: InvestByResultAct,
@@ -1,8 +1,26 @@
import { matchMock } from '../matchMock'
import { voteforActionMock } from './votefor'
import { returnByMoneyDecisionActionMock, voteAgainstDecision9001Mock, voteForDecision9001Mock } from './returnByMoneyDecision'
voteforActionMock.match = function (url: string, params?: URLSearchParams): boolean {
return matchMock(this, url, params)
}
export const actionsMocks = [voteforActionMock as any]
returnByMoneyDecisionActionMock.match = function (url: string, params?: URLSearchParams): boolean {
return matchMock(this, url, params)
}
voteForDecision9001Mock.match = function (url: string, params?: URLSearchParams): boolean {
return matchMock(this, url, params)
}
voteAgainstDecision9001Mock.match = function (url: string, params?: URLSearchParams): boolean {
return matchMock(this, url, params)
}
export const actionsMocks = [
voteforActionMock as any,
returnByMoneyDecisionActionMock as any,
voteForDecision9001Mock as any,
voteAgainstDecision9001Mock as any,
]
@@ -0,0 +1,116 @@
const returnByMoneyDecisionAction = {
_id: '6852b0389bd56240ad71b998',
chain_id: 'f50256680336ee6daaeee93915b945c1166b5dfc98977adcb717403ae225c559',
block_id: '000CBF518A8998507069881FF00EE74E1E2E5345CD9B3D1F34E09DA732AC1D3D',
block_num: 835409,
transaction_id: 'F79ED54A8D37076C3ADC73D3A10EF2A3933089D7B2B3D87F46D8703BCB34DA5B',
global_sequence: '836089',
receipt: {
receiver: 'soviet',
act_digest: '642C266D89D4FB33ACB2A41E4294A382A451F361FCD040F5D202F8D1816FBFF2',
global_sequence: '836089',
recv_sequence: '163',
auth_sequence: [{ account: 'ant', sequence: '3' }],
code_sequence: 1,
abi_sequence: 1,
},
account: 'soviet',
name: 'returnbymoneydecision',
authorization: [{ actor: 'ant', permission: 'active' }],
data: {
version: '1.0.0',
coopname: 'voskhod',
username: 'ant',
decision_id: '9001',
payment_hash: 'abc123def456hash789',
amount: '50000',
currency: 'руб.',
signed_at: '2025-06-18T12:25:23.000',
signed_hash: '940734E565D3FA78527CC85C90D8601D7801F896EC5B40F6D1CC1DF0C6600948',
signature: 'SIG_K1_KkSjs8tZtmDmTyLvaCEETVZ2QQQpshMF3ZyEjVXadNXivsAQfjbkrN6smKRoApqXjBhc5vHhL8vbSt4itAzYRbrvHF18WF',
public_key: 'PUB_K1_52YPV66yKaju9WTXKuk8xWRSsCZ18Ewt9Z6SW24yMsRySPi2mZ',
},
action_ordinal: 1,
creator_action_ordinal: 0,
receiver: 'soviet',
context_free: false,
elapsed: '0',
console: '',
account_ram_deltas: [{ account: 'soviet', delta: '8' }],
return_value: '',
}
// Мок для голосов "за" решение
const voteForAction9001_1 = {
_id: '6852b0389bd56240ad71b990',
chain_id: 'f50256680336ee6daaeee93915b945c1166b5dfc98977adcb717403ae225c559',
block_id: '000CBF518A8998507069881FF00EE74E1E2E5345CD9B3D1F34E09DA732AC1D3D',
block_num: 835400,
transaction_id: 'F79ED54A8D37076C3ADC73D3A10EF2A3933089D7B2B3D87F46D8703BCB34DA5A',
global_sequence: '836080',
receipt: {
receiver: 'soviet',
act_digest: '642C266D89D4FB33ACB2A41E4294A382A451F361FCD040F5D202F8D1816FBFF1',
global_sequence: '836080',
recv_sequence: '160',
auth_sequence: [{ account: 'member1', sequence: '1' }],
code_sequence: 1,
abi_sequence: 1,
},
account: 'soviet',
name: 'votefor',
authorization: [{ actor: 'member1', permission: 'active' }],
data: {
version: '1.0.0',
coopname: 'voskhod',
username: 'member1',
decision_id: '9001',
signed_at: '2025-06-18T12:20:00.000',
signed_hash: '940734E565D3FA78527CC85C90D8601D7801F896EC5B40F6D1CC1DF0C6600940',
signature: 'SIG_K1_KkSjs8tZtmDmTyLvaCEETVZ2QQQpshMF3ZyEjVXadNXivsAQfjbkrN6smKRoApqXjBhc5vHhL8vbSt4itAzYRbrvHF18WA',
public_key: 'PUB_K1_52YPV66yKaju9WTXKuk8xWRSsCZ18Ewt9Z6SW24yMsRySPi2mA',
},
action_ordinal: 1,
creator_action_ordinal: 0,
receiver: 'soviet',
context_free: false,
elapsed: '0',
console: '',
account_ram_deltas: [{ account: 'soviet', delta: '8' }],
return_value: '',
}
// Мок для голосов "против" решения (пустой массив - никто не голосовал против)
const voteAgainstAction9001: any[] = []
export const returnByMoneyDecisionActionMock = {
code: 'soviet',
actionName: 'returnbymoneydecision',
value: { 'data.decision_id': '9001', 'data.coopname': 'voskhod' },
data: {
results: [returnByMoneyDecisionAction],
},
match: ((_url: string, _params?: URLSearchParams) => false) as (url: string, params?: URLSearchParams) => boolean,
}
// Мок для голосов "за"
export const voteForDecision9001Mock = {
code: 'soviet',
actionName: 'votefor',
value: { 'data.decision_id': '9001', 'data.coopname': 'voskhod' },
data: {
results: [voteForAction9001_1],
},
match: ((_url: string, _params?: URLSearchParams) => false) as (url: string, params?: URLSearchParams) => boolean,
}
// Мок для голосов "против"
export const voteAgainstDecision9001Mock = {
code: 'soviet',
actionName: 'voteagainst',
value: { 'data.decision_id': '9001', 'data.coopname': 'voskhod' },
data: {
results: voteAgainstAction9001,
},
match: ((_url: string, _params?: URLSearchParams) => false) as (url: string, params?: URLSearchParams) => boolean,
}
+1
View File
@@ -97,6 +97,7 @@ export class Generator implements IGenerator {
[Actions.ReturnByAssetDecision.Template.registry_id]: new Actions.ReturnByAssetDecision.Factory(this.storage), // 801
[Actions.ReturnByAssetAct.Template.registry_id]: new Actions.ReturnByAssetAct.Factory(this.storage), // 802
[Actions.ReturnByMoney.Template.registry_id]: new Actions.ReturnByMoney.Factory(this.storage), // 900
[Actions.ReturnByMoneyDecision.Template.registry_id]: new Actions.ReturnByMoneyDecision.Factory(this.storage), // 901
[Actions.AssetContributionDecision.Template.registry_id]: new Actions.AssetContributionDecision.Factory(this.storage), // 701
[Actions.AssetContributionAct.Template.registry_id]: new Actions.AssetContributionAct.Factory(this.storage), // 702
[Actions.InvestmentAgreement.Template.registry_id]: new Actions.InvestmentAgreement.Factory(this.storage), // 1000
+13
View File
@@ -45,4 +45,17 @@ describe('тестируем цифровой кошелёк', () => {
},
})
})
it('генерация решения о возврате паевого взноса (ReturnByMoneyDecision)', async () => {
await testDocumentGeneration({
registry_id: Cooperative.Registry.ReturnByMoneyDecision.registry_id,
coopname: 'voskhod',
username: 'ant',
lang: 'ru',
decision_id: 9001,
payment_hash: 'abc123def456hash789',
amount: '50000',
currency: 'руб.',
})
})
})
+1
View File
@@ -64,6 +64,7 @@ export const subsribedActions: IActionConfig[] = [
{ code: 'soviet', action: 'updateboard', notify: true },
{ code: 'soviet', action: 'createboard', notify: true },
{ code: 'meet', action: 'newgdecision', notify: true },
{ code: 'wallet', action: 'createwthd', notify: true },
]
// --------------------------
@@ -0,0 +1,169 @@
# Примеры использования SDK для Wallet и Gateway модулей
## Wallet Module
### Создание заявки на вывод средств
```typescript
import { Client } from '@cooptypes/sdk'
import * as Mutations from '@cooptypes/sdk/mutations'
const client = Client.getInstance()
const result = await client.Mutation(
Mutations.Wallet.CreateWithdraw.mutation,
{
variables: {
input: {
coopname: 'mycoopname',
username: 'alice',
quantity: '100.0000',
symbol: 'SHARES',
method_id: 'card_123',
memo: 'Возврат паевого взноса',
statement: {
hash: 'doc_hash_here',
signatures: [
{
signature: 'SIG_...',
public_key: 'EOS...',
// ... другие поля подписи
}
],
rawDocument: {
hash: 'raw_doc_hash',
binary: 'base64_content',
full_title: 'Заявление на возврат',
html: '<html>...</html>',
meta: {}
}
}
}
}
}
)
console.log('Withdraw hash:', result.createWithdraw.withdraw_hash)
```
### Генерация документа заявления (900)
```typescript
import * as Mutations from '@cooptypes/sdk/mutations'
const result = await client.Mutation(
Mutations.Wallet.GenerateReturnByMoneyStatementDocument.mutation,
{
variables: {
data: {
coopname: 'mycoopname',
username: 'alice',
full_name: 'Алиса Иванова',
quantity: '100.0000',
symbol: 'SHARES'
},
options: {
format: 'pdf'
}
}
}
)
console.log('Generated document:', result.generateReturnByMoneyStatementDocument)
```
### Генерация документа решения (901)
```typescript
import * as Mutations from '@cooptypes/sdk/mutations'
const result = await client.Mutation(
Mutations.Wallet.GenerateReturnByMoneyDecisionDocument.mutation,
{
variables: {
data: {
coopname: 'mycoopname',
username: 'alice',
full_name: 'Алиса Иванова',
quantity: '100.0000',
symbol: 'SHARES',
decision_date: new Date(),
decision_number: 'Р-001'
}
}
}
)
console.log('Generated decision:', result.generateReturnByMoneyDecisionDocument)
```
## Gateway Module
### Получение списка платежей
```typescript
import * as Queries from '@cooptypes/sdk/queries'
const result = await client.Query(
Queries.Gateway.GetGatewayPayments.query,
{
variables: {
filters: {
coopname: 'mycoopname',
username: 'alice',
status: 'pending'
},
options: {
limit: 20,
offset: 0
}
}
}
)
console.log('Payments:', result.getGatewayPayments.items)
console.log('Total count:', result.getGatewayPayments.totalCount)
```
### Обновление статуса платежа
```typescript
import * as Mutations from '@cooptypes/sdk/mutations'
const result = await client.Mutation(
Mutations.Gateway.UpdatePaymentStatus.mutation,
{
variables: {
input: {
hash: 'payment_hash_here',
coopname: 'mycoopname',
type: 'outgoing',
status: 'completed'
}
}
}
)
console.log('Updated payment:', result.updatePaymentStatus)
```
## Типизация
Все селекторы поставляются с типами:
```typescript
import type {
CreateWithdrawResponseType,
PaymentType,
OutgoingPaymentType,
PaginatedPaymentsType
} from '@cooptypes/sdk/selectors'
// Строгая типизация результатов
const payment: PaymentType = {
id: '123',
coopname: 'mycoopname',
username: 'alice',
hash: 'payment_hash',
quantity: '100.0000',
symbol: 'SHARES',
status: 'pending',
type: 'outgoing',
// ... остальные поля с проверкой типов
}
```
@@ -0,0 +1 @@
export * as UpdatePaymentStatus from './updatePaymentStatus'
@@ -0,0 +1,19 @@
import { $, Selector, type ModelTypes } from '../../zeus/index'
import { outgoingPaymentSelector } from '../../selectors/gateway/outgoingPaymentSelector'
export const name = 'updatePaymentStatus'
// Селектор мутации
export const mutation = Selector('Mutation')({
[name]: [{ input: $('input', 'UpdatePaymentStatusInput!') }, outgoingPaymentSelector],
})
// Интерфейс для входных данных
export interface IInput {
input: ModelTypes['UpdatePaymentStatusInput']
}
// Тип выходных данных
export type IOutput = {
[name]: ModelTypes['OutgoingPayment']
}
+2
View File
@@ -5,8 +5,10 @@ export * as Branches from './branches'
export * as Cooplace from './cooplace'
export * as Extensions from './extensions'
export * as FreeDecisions from './freeDecisions'
export * as Gateway from './gateway'
export * as Meet from './meet'
export * as Participants from './participants'
export * as PaymentMethods from './paymentMethods'
export * as Payments from './payments'
export * as System from './system'
export * as Wallet from './wallet'
@@ -0,0 +1,19 @@
import { $, Selector, type ModelTypes } from '../../zeus/index'
import { createWithdrawResponseSelector } from '../../selectors/wallet/createWithdrawResponseSelector'
export const name = 'createWithdraw'
// Селектор мутации
export const mutation = Selector('Mutation')({
[name]: [{ input: $('input', 'CreateWithdrawInput!') }, createWithdrawResponseSelector],
})
// Интерфейс для входных данных
export interface IInput {
input: ModelTypes['CreateWithdrawInput']
}
// Тип выходных данных
export type IOutput = {
[name]: ModelTypes['CreateWithdrawResponse']
}
@@ -0,0 +1,26 @@
import { $, Selector, type ModelTypes } from '../../zeus/index'
import { rawGeneratedDocumentSelector } from '../../selectors/documents/documentAggregateSelector'
export const name = 'generateReturnByMoneyDecisionDocument'
// Селектор мутации
export const mutation = Selector('Mutation')({
[name]: [
{
data: $('data', 'ReturnByMoneyDecisionGenerateDocumentInput!'),
options: $('options', 'GenerateDocumentOptionsInput'),
},
rawGeneratedDocumentSelector,
],
})
// Интерфейс для входных данных
export interface IInput {
data: ModelTypes['ReturnByMoneyDecisionGenerateDocumentInput']
options?: ModelTypes['GenerateDocumentOptionsInput']
}
// Тип выходных данных
export type IOutput = {
[name]: ModelTypes['GeneratedDocument']
}
@@ -0,0 +1,26 @@
import { $, Selector, type ModelTypes } from '../../zeus/index'
import { rawGeneratedDocumentSelector } from '../../selectors/documents/documentAggregateSelector'
export const name = 'generateReturnByMoneyStatementDocument'
// Селектор мутации
export const mutation = Selector('Mutation')({
[name]: [
{
data: $('data', 'ReturnByMoneyGenerateDocumentInput!'),
options: $('options', 'GenerateDocumentOptionsInput'),
},
rawGeneratedDocumentSelector,
],
})
// Интерфейс для входных данных
export interface IInput {
data: ModelTypes['ReturnByMoneyGenerateDocumentInput']
options?: ModelTypes['GenerateDocumentOptionsInput']
}
// Тип выходных данных
export type IOutput = {
[name]: ModelTypes['GeneratedDocument']
}
@@ -0,0 +1,3 @@
export * as CreateWithdraw from './createWithdraw'
export * as GenerateReturnByMoneyStatementDocument from './generateReturnByMoneyStatementDocument'
export * as GenerateReturnByMoneyDecisionDocument from './generateReturnByMoneyDecisionDocument'
@@ -0,0 +1,27 @@
import { rawOutgoingPaymentSelector } from '../../selectors'
import { paginationSelector } from '../../utils/paginationSelector'
import { $, Selector, type GraphQLTypes, type InputType, type ModelTypes } from '../../zeus/index'
const paginatedPaymentsSelector = { ...paginationSelector, items: rawOutgoingPaymentSelector }
export const name = 'getGatewayPayments'
// Селектор запроса
export const query = Selector('Query')({
[name]: [
{
filters: $('filters', 'GetOutgoingPaymentsInput!'),
options: $('options', 'PaginationInput!'),
},
paginatedPaymentsSelector,
],
})
// Интерфейс для входных данных
export interface IInput {
filters: ModelTypes['GetOutgoingPaymentsInput']
options: ModelTypes['PaginationInput']
}
// Тип выходных данных
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
@@ -0,0 +1 @@
export * as GetGatewayPayments from './getGatewayPayments'
+1
View File
@@ -4,6 +4,7 @@ export * as Branches from './branches'
export * as Desktop from './desktop'
export * as Documents from './documents'
export * as Extensions from './extensions'
export * as Gateway from './gateway'
export * as Meet from './meet'
export * as PaymentMethods from './paymentMethods'
export * as Payments from './payments'
@@ -0,0 +1,2 @@
export * from './paymentSelector'
export * from './outgoingPaymentSelector'
@@ -0,0 +1,33 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes, type ModelTypes } from '../../zeus/index'
// Сырой селектор для OutgoingPaymentDTO
export const rawOutgoingPaymentSelector = {
id: true,
coopname: true,
username: true,
hash: true,
quantity: true,
symbol: true,
method_id: true,
status: true,
type: true,
created_at: true,
updated_at: true,
memo: true,
payment_details: true,
blockchain_data: true,
formatted_amount: true,
status_label: true,
can_change_status: true,
}
// Валидация селектора
const _validate: MakeAllFieldsRequired<ValueTypes['OutgoingPayment']> = rawOutgoingPaymentSelector
/**
* Селектор для исходящего платежа
*/
export const outgoingPaymentSelector = Selector('OutgoingPayment')(rawOutgoingPaymentSelector)
export type OutgoingPaymentType = ModelTypes['OutgoingPayment']
@@ -0,0 +1,36 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes, type ModelTypes } from '../../zeus/index'
// Сырой селектор для PaymentDTO
export const rawPaymentSelector = {
id: true,
coopname: true,
username: true,
hash: true,
outcome_hash: true,
income_hash: true,
quantity: true,
symbol: true,
method_id: true,
status: true,
type: true,
created_at: true,
updated_at: true,
memo: true,
payment_details: true,
blockchain_data: true,
formatted_amount: true,
status_label: true,
type_label: true,
can_change_status: true,
}
// Валидация селектора
const _validate: MakeAllFieldsRequired<ValueTypes['GatewayPayment']> = rawPaymentSelector
/**
* Селектор для универсального платежа
*/
export const paymentSelector = Selector('GatewayPayment')(rawPaymentSelector)
export type PaymentType = ModelTypes['GatewayPayment']
+2
View File
@@ -4,7 +4,9 @@ export * from './common'
export * from './cooplace'
export * from './extensions'
export * from './freeDecisions'
export * from './gateway'
export * from './meet'
export * from './participants'
export * from './paymentMethods'
export * from './system'
export * from './wallet'
@@ -0,0 +1,17 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes, type ModelTypes } from '../../zeus/index'
// Сырой селектор для CreateWithdrawResponse
export const rawCreateWithdrawResponseSelector = {
withdraw_hash: true,
}
// Валидация селектора
const _validate: MakeAllFieldsRequired<ValueTypes['CreateWithdrawResponse']> = rawCreateWithdrawResponseSelector
/**
* Селектор для ответа создания заявки на вывод средств
*/
export const createWithdrawResponseSelector = Selector('CreateWithdrawResponse')(rawCreateWithdrawResponseSelector)
export type CreateWithdrawResponseType = ModelTypes['CreateWithdrawResponse']
@@ -0,0 +1 @@
export * from './createWithdrawResponseSelector'
+94 -1
View File
@@ -159,6 +159,9 @@ export const AllTypesProps: Record<string,any> = {
},
CreateProjectFreeDecisionInput:{
},
CreateWithdrawInput:{
statement:"SignedDigitalDocumentInput"
},
DateTime: `scalar.DateTime` as const,
DeclineRequestInput:{
@@ -220,6 +223,9 @@ export const AllTypesProps: Record<string,any> = {
},
GetMeetsInput:{
},
GetOutgoingPaymentsInput:{
},
GetPaymentMethodsInput:{
@@ -294,6 +300,9 @@ export const AllTypesProps: Record<string,any> = {
createProjectOfFreeDecision:{
data:"CreateProjectFreeDecisionInput"
},
createWithdraw:{
input:"CreateWithdrawInput"
},
declineRequest:{
data:"DeclineRequestInput"
},
@@ -375,6 +384,14 @@ export const AllTypesProps: Record<string,any> = {
data:"ReturnByAssetStatementGenerateDocumentInput",
options:"GenerateDocumentOptionsInput"
},
generateReturnByMoneyDecisionDocument:{
data:"ReturnByMoneyDecisionGenerateDocumentInput",
options:"GenerateDocumentOptionsInput"
},
generateReturnByMoneyStatementDocument:{
data:"ReturnByMoneyGenerateDocumentInput",
options:"GenerateDocumentOptionsInput"
},
generateSelectBranchDocument:{
data:"SelectBranchGenerateDocumentInput",
options:"GenerateDocumentOptionsInput"
@@ -479,6 +496,9 @@ export const AllTypesProps: Record<string,any> = {
updateExtension:{
data:"ExtensionInput"
},
updatePaymentStatus:{
input:"UpdatePaymentStatusInput"
},
updateRequest:{
data:"UpdateRequestInput"
},
@@ -552,6 +572,10 @@ export const AllTypesProps: Record<string,any> = {
getExtensions:{
data:"GetExtensionsInput"
},
getGatewayPayments:{
filters:"GetOutgoingPaymentsInput",
options:"PaginationInput"
},
getMeet:{
data:"GetMeetInput"
},
@@ -590,6 +614,9 @@ export const AllTypesProps: Record<string,any> = {
},
RepresentedByInput:{
},
RequestInput:{
},
ResetKeyInput:{
@@ -622,6 +649,12 @@ export const AllTypesProps: Record<string,any> = {
ReturnByAssetStatementSignedMetaDocumentInput:{
request:"CommonRequestInput"
},
ReturnByMoneyDecisionGenerateDocumentInput:{
},
ReturnByMoneyGenerateDocumentInput:{
request:"RequestInput"
},
SearchPrivateAccountsInput:{
},
@@ -695,6 +728,9 @@ export const AllTypesProps: Record<string,any> = {
UpdateOrganizationDataInput:{
details:"OrganizationDetailsInput",
represented_by:"RepresentedByInput"
},
UpdatePaymentStatusInput:{
},
UpdateRequestInput:{
@@ -714,7 +750,9 @@ export const AllTypesProps: Record<string,any> = {
VoteOnAnnualGeneralMeetInput:{
ballot:"AnnualGeneralMeetingVotingBallotSignedDocumentInput",
votes:"VoteItemInput"
}
},
gatewayPaymentStatus: "enum" as const,
gatewayPaymentType: "enum" as const
}
export const ReturnTypes: Record<string,any> = {
@@ -941,6 +979,9 @@ export const ReturnTypes: Record<string,any> = {
username:"String",
verifications:"Verification"
},
CreateWithdrawResponse:{
withdraw_hash:"String"
},
CreatedProjectFreeDecision:{
decision:"String",
id:"String",
@@ -1046,6 +1087,28 @@ export const ReturnTypes: Record<string,any> = {
title:"String",
updated_at:"DateTime"
},
GatewayPayment:{
blockchain_data:"JSON",
can_change_status:"Boolean",
coopname:"String",
created_at:"DateTime",
formatted_amount:"String",
hash:"String",
id:"ID",
income_hash:"String",
memo:"String",
method_id:"String",
outcome_hash:"String",
payment_details:"String",
quantity:"String",
status:"gatewayPaymentStatus",
status_label:"String",
symbol:"String",
type:"gatewayPaymentType",
type_label:"String",
updated_at:"DateTime",
username:"String"
},
GeneratedDocument:{
binary:"String",
full_title:"String",
@@ -1186,6 +1249,7 @@ export const ReturnTypes: Record<string,any> = {
createInitialPayment:"Payment",
createParentOffer:"Transaction",
createProjectOfFreeDecision:"CreatedProjectFreeDecision",
createWithdraw:"CreateWithdrawResponse",
declineRequest:"Transaction",
deleteBranch:"Boolean",
deletePaymentMethod:"Boolean",
@@ -1208,6 +1272,8 @@ export const ReturnTypes: Record<string,any> = {
generateReturnByAssetAct:"GeneratedDocument",
generateReturnByAssetDecision:"GeneratedDocument",
generateReturnByAssetStatement:"GeneratedDocument",
generateReturnByMoneyDecisionDocument:"GeneratedDocument",
generateReturnByMoneyStatementDocument:"GeneratedDocument",
generateSelectBranchDocument:"GeneratedDocument",
generateSignatureAgreement:"GeneratedDocument",
generateSovietDecisionOnAnnualMeetDocument:"GeneratedDocument",
@@ -1241,6 +1307,7 @@ export const ReturnTypes: Record<string,any> = {
updateAccount:"Account",
updateBankAccount:"PaymentMethod",
updateExtension:"Extension",
updatePaymentStatus:"OutgoingPayment",
updateRequest:"Transaction",
updateSystem:"SystemInfo",
voteOnAnnualGeneralMeet:"MeetAggregate"
@@ -1272,6 +1339,31 @@ export const ReturnTypes: Record<string,any> = {
kpp:"String",
ogrn:"String"
},
OutgoingPayment:{
blockchain_data:"JSON",
can_change_status:"Boolean",
coopname:"String",
created_at:"DateTime",
formatted_amount:"String",
hash:"String",
id:"ID",
memo:"String",
method_id:"String",
payment_details:"String",
quantity:"String",
status:"gatewayPaymentStatus",
status_label:"String",
symbol:"String",
type:"gatewayPaymentType",
updated_at:"DateTime",
username:"String"
},
PaginatedGatewayPaymentsPaginationResult:{
currentPage:"Int",
items:"GatewayPayment",
totalCount:"Int",
totalPages:"Int"
},
ParticipantAccount:{
braname:"String",
created_at:"DateTime",
@@ -1386,6 +1478,7 @@ export const ReturnTypes: Record<string,any> = {
getDesktop:"Desktop",
getDocuments:"DocumentsAggregatePaginationResult",
getExtensions:"Extension",
getGatewayPayments:"PaginatedGatewayPaymentsPaginationResult",
getMeet:"MeetAggregate",
getMeets:"MeetAggregate",
getPaymentMethods:"PaymentMethodPaginationResult",
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes, type ModelTypes } from '../../zeus/index'
// Сырой селектор для CreateWithdrawResponse
export const rawCreateWithdrawResponseSelector = {
withdraw_hash: true,
}
// Валидация селектора
const _validate: MakeAllFieldsRequired<ValueTypes['CreateWithdrawResponse']> = rawCreateWithdrawResponseSelector
/**
* Селектор для ответа создания заявки на вывод средств
*/
export const createWithdrawResponseSelector = Selector('CreateWithdrawResponse')(rawCreateWithdrawResponseSelector)
export type CreateWithdrawResponseType = ModelTypes['CreateWithdrawResponse']