[C28-33][@ant] feat: реквизиты получателей по payment_method со снимком в БД + СЗ 2010 по бумажному образцу — полные реквизиты в документ подставляет сервер, фронт видит сокращённые; вычищены осиротевшие authorizeExpenseReport/declineExpenseReport
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Has been cancelled

This commit is contained in:
coopops
2026-06-11 20:01:43 +00:00
parent 1f3e9d93c7
commit dfe73601e8
41 changed files with 612 additions and 1404 deletions
@@ -41,6 +41,23 @@ class ExpenseProposalItemInputDTO implements ItemAction {
@IsOptional()
@IsString()
requisites?: string;
@Field({
description:
'Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ.',
nullable: true,
})
@IsOptional()
@IsString()
payment_method_id?: string;
@Field({
description: 'Имя аккаунта получателя-пайщика (владелец реквизитов).',
nullable: true,
})
@IsOptional()
@IsString()
recipient_username?: string;
}
@InputType('ExpenseProposalHeaderInput')
@@ -8,6 +8,7 @@ import {
type InterExpenseItem,
type InterExpenseProposalRead,
type InterExpenseProposalStatus,
type InterExpenseRequisiteItemInput,
} from '@coopenomics/inter';
import { AccountDataPort, ACCOUNT_DATA_PORT } from '~/domain/account/ports/account-data.port';
import { CapitalBlockchainPort, CAPITAL_BLOCKCHAIN_PORT } from '../../domain/interfaces/capital-blockchain.port';
@@ -50,6 +51,18 @@ export class ProgramExpensesManagementService {
) {}
async createProgramExpense(data: CreateProgramExpenseInputDTO): Promise<TransactResult> {
// Реквизиты получателей: валидация ДО on-chain заявки, снимок в шасси —
// ПОСЛЕ (фиксация «куда платить» на момент создания СЗ).
const requisiteItems: InterExpenseRequisiteItemInput[] = data.items.map((it) => ({
proposalHash: data.expense_hash,
itemHash: it.item_hash,
recipient: it.recipient,
isOrganization: it.recipient_type === ExpenseRecipientType.ORG,
paymentMethodId: it.payment_method_id,
requisites: it.requisites,
}));
await this.expenseChassis.validateRequisites(data.coopname, requisiteItems);
const zeroAmount = `0.0000 ${config.blockchain.root_govern_symbol}`;
const blockchainData: CapitalContract.Actions.CreateProgramExpense.ICreateProgramExpense = {
coopname: data.coopname,
@@ -73,7 +86,11 @@ export class ProgramExpensesManagementService {
description: data.description,
statement: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.statement),
};
return this.capitalBlockchainPort.createProgramExpense(blockchainData);
const result = await this.capitalBlockchainPort.createProgramExpense(blockchainData);
await this.expenseChassis.snapshotRequisites(data.coopname, requisiteItems);
return result;
}
async topupProgramExpense(data: TopupProgramExpenseInputDTO): Promise<TransactResult> {
@@ -1,33 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { ExpenseProposalDecisionSignedDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-decision-document.dto';
/**
* Input авторизации СЗ-отчёта (председатель / совет утверждает закрытие сметы).
*
* Сабмит: `expense::authexp` через `ExpensesBlockchainPort.authExp`.
* Подпись `decision` (registry 2011) сделана на стороне UI.
*
* После цепочки на-цепи: REPORT_SUBMITTED → CLOSED → `ExpensesCapitalTriggerService`
* запускает capitalization Благороста (capital::createrid на сумму total_actual).
*/
@InputType('AuthorizeExpenseReportInput')
export class AuthorizeExpenseReportInputDTO {
@Field(() => String, { description: 'Имя кооператива.' })
@IsNotEmpty()
@IsString()
coopname!: string;
@Field(() => String, { description: 'Хеш сметы расхода.' })
@IsNotEmpty()
@IsString()
proposal_hash!: string;
@Field(() => ExpenseProposalDecisionSignedDocumentInputDTO, {
description: 'Подписанное решение совета (document2, registry 2011).',
})
@ValidateNested()
@Type(() => ExpenseProposalDecisionSignedDocumentInputDTO)
decision!: ExpenseProposalDecisionSignedDocumentInputDTO;
}
@@ -1,28 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
/**
* Input отклонения СЗ-отчёта (председатель / совет не утверждает закрытие).
*
* На-цепи: `expense::declineexp` (REPORT_SUBMITTED → DECLINED).
* Capitalization Благороста НЕ запускается. Reason — обязательный, видимый в UI
* и в audit log пайщика; до 1000 символов.
*/
@InputType('DeclineExpenseReportInput')
export class DeclineExpenseReportInputDTO {
@Field(() => String, { description: 'Имя кооператива.' })
@IsNotEmpty()
@IsString()
coopname!: string;
@Field(() => String, { description: 'Хеш сметы расхода.' })
@IsNotEmpty()
@IsString()
proposal_hash!: string;
@Field(() => String, { description: 'Причина отклонения СЗ-отчёта (до 1000 символов).' })
@IsNotEmpty()
@IsString()
@MaxLength(1000)
reason!: string;
}
@@ -1,5 +1,5 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum';
import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum';
@@ -31,4 +31,21 @@ export class ExpenseItemInputDTO {
@IsNotEmpty()
@IsString()
planned_amount!: string;
@Field(() => String, {
nullable: true,
description:
'Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу.',
})
@IsOptional()
@IsString()
payment_method_id?: string;
@Field(() => String, {
nullable: true,
description: 'Реквизиты получателя-организации (вводятся вручную).',
})
@IsOptional()
@IsString()
requisites?: string;
}
@@ -16,16 +16,14 @@ import { ReportExpenseItemInputDTO } from '../dto/report-expense-item.input';
import { ReturnExpenseItemInputDTO } from '../dto/return-expense-item.input';
import { OverspendExpenseItemInputDTO } from '../dto/overspend-expense-item.input';
import { SubmitExpenseReportInputDTO } from '../dto/submit-expense-report.input';
import { AuthorizeExpenseReportInputDTO } from '../dto/authorize-expense-report.input';
import { DeclineExpenseReportInputDTO } from '../dto/decline-expense-report.input';
/**
* GraphQL Mutation-резолвер контракта `expense`.
*
* 8 actions полного lifecycle: createExpenseProposal / authorizeExpenseReport /
* payExpenseItem / reportExpenseItem / returnExpenseItem / overspendExpenseItem /
* submitExpenseReport / declineExpenseReport. Все маршрутизируются в
* createExpenseProposal / payExpenseItem / reportExpenseItem / returnExpenseItem /
* overspendExpenseItem / submitExpenseReport. Все маршрутизируются в
* `ExpensesMutationsService`, который сабмитит через `ExpensesBlockchainPort`.
* Авторизация и отклонение СЗ — решение совета (повестка), не мутации backend'а.
*/
@Resolver()
export class ExpenseMutationsResolver {
@@ -135,27 +133,4 @@ export class ExpenseMutationsResolver {
return this.expensesMutations.submitExpenseReport(data);
}
@Mutation(() => TransactionDTO, {
name: 'authorizeExpenseReport',
description: 'Утвердить СЗ-отчёт (закрытие сметы). Триггерит капитализацию РИД в Благоросте.',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman'])
async authorizeExpenseReport(
@Args('data', { type: () => AuthorizeExpenseReportInputDTO }) data: AuthorizeExpenseReportInputDTO
): Promise<TransactionDTO> {
return this.expensesMutations.authorizeExpenseReport(data);
}
@Mutation(() => TransactionDTO, {
name: 'declineExpenseReport',
description: 'Отклонить СЗ-отчёт с указанием причины. Смета переходит в DECLINED.',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman'])
async declineExpenseReport(
@Args('data', { type: () => DeclineExpenseReportInputDTO }) data: DeclineExpenseReportInputDTO
): Promise<TransactionDTO> {
return this.expensesMutations.declineExpenseReport(data);
}
}
@@ -0,0 +1,92 @@
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import type { InterExpenseRequisiteItemInput } from '@coopenomics/inter'
import { PAYMENT_METHOD_REPOSITORY, PaymentMethodRepository } from '~/domain/common/repositories/payment-method.repository'
import { ExpenseRequisiteSnapshotTypeormEntity } from '../../infrastructure/entities/expense-requisite-snapshot.typeorm-entity'
import { formatPaymentMethodRequisites } from '../../domain/utils/format-requisites.util'
/**
* Снимки реквизитов получателей по строкам СЗ. Канон gateway prepareWithdraw →
* persistWithdraw: `validate` зовётся ДО on-chain заявки (метод существует),
* `snapshot` — ПОСЛЕ (данные метода копируются на момент создания СЗ).
* Реквизиты в чейн не пишутся — только в БД шасси и в документ 2010.
*/
@Injectable()
export class ExpenseRequisiteSnapshotsService {
private readonly logger = new Logger(ExpenseRequisiteSnapshotsService.name)
constructor(
@Inject(PAYMENT_METHOD_REPOSITORY)
private readonly paymentMethods: PaymentMethodRepository,
@InjectRepository(ExpenseRequisiteSnapshotTypeormEntity)
private readonly repository: Repository<ExpenseRequisiteSnapshotTypeormEntity>
) {}
async validate(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise<void> {
await this.resolve(coopname, items)
}
async snapshot(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise<void> {
const snapshots = await this.resolve(coopname, items)
if (snapshots.length === 0) return
try {
await this.repository.save(snapshots)
} catch (error: any) {
// On-chain заявка уже создана — без снимка бухгалтер не получит реквизиты
// для оплаты. Требуется ручная сверка.
this.logger.error(
`КРИТИЧНО: СЗ ${items[0]?.proposalHash} создана on-chain, но снимок реквизитов не сохранён: ${error.message}`,
error
)
throw error
}
}
/** Полные реквизиты платёжного метода пайщика строкой — для документов. */
async formatForOwner(username: string, methodId: string): Promise<string> {
const method = await this.paymentMethods.get({ username, method_id: methodId })
return formatPaymentMethodRequisites(method)
}
private async resolve(
coopname: string,
items: InterExpenseRequisiteItemInput[]
): Promise<ExpenseRequisiteSnapshotTypeormEntity[]> {
return Promise.all(
items.map(async (it) => {
const snapshot = this.repository.create({
coopname,
proposal_hash: it.proposalHash.toLowerCase(),
item_hash: it.itemHash.toLowerCase(),
recipient: it.recipient,
method_id: null,
method_type: null,
data: null,
requisites: it.requisites ?? '',
})
if (it.isOrganization) {
return snapshot
}
if (!it.paymentMethodId) {
throw new BadRequestException(
`Не указаны реквизиты получателя ${it.recipient} (платёжный метод) по строке расхода`
)
}
const method = await this.paymentMethods.get({
username: it.recipient,
method_id: it.paymentMethodId,
})
snapshot.method_id = method.method_id
snapshot.method_type = method.method_type
snapshot.data = method.data as unknown as Record<string, unknown>
snapshot.requisites = formatPaymentMethodRequisites(method)
return snapshot
})
)
}
}
@@ -6,39 +6,46 @@ import type { ReportExpenseItemInputDTO } from '../dto/report-expense-item.input
import type { ReturnExpenseItemInputDTO } from '../dto/return-expense-item.input'
import type { OverspendExpenseItemInputDTO } from '../dto/overspend-expense-item.input'
import type { SubmitExpenseReportInputDTO } from '../dto/submit-expense-report.input'
import type { AuthorizeExpenseReportInputDTO } from '../dto/authorize-expense-report.input'
import type { CreateExpenseProposalInputDTO } from '../dto/create-expense-proposal.input'
import type { DeclineExpenseReportInputDTO } from '../dto/decline-expense-report.input'
import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum'
import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum'
/**
* Контракт-тест: все 8 mutations пробрасывают payload в `ExpensesBlockchainPort`
* с корректным action-mapping. createExpenseProposal/authorizeExpenseReport
* раскладывают подписанный document2 через `.toDocument()`.
* Контракт-тест: backend-mutations пробрасывают payload в `ExpensesBlockchainPort`
* с корректным action-mapping. createExpenseProposal раскладывает подписанный
* document2 через `.toDocument()` и снимает реквизиты получателей-пайщиков.
* authexp/declexp — callbacks решения совета, у backend'а их нет.
*/
describe('ExpensesMutationsService', () => {
let service: ExpensesMutationsService
let chain: jest.Mocked<ExpensesBlockchainPort>
let generator: jest.Mocked<Pick<GeneratorInfrastructureService, 'generateDocument'>>
let requisiteSnapshots: { validate: jest.Mock; snapshot: jest.Mock; formatForOwner: jest.Mock }
const fakeResult = { response: { transaction_id: 'tx_abc' } } as never
beforeEach(() => {
chain = {
createExp: jest.fn().mockResolvedValue(fakeResult),
authExp: jest.fn().mockResolvedValue(fakeResult),
payExp: jest.fn().mockResolvedValue(fakeResult),
reportExp: jest.fn().mockResolvedValue(fakeResult),
returnExp: jest.fn().mockResolvedValue(fakeResult),
overspendExp: jest.fn().mockResolvedValue(fakeResult),
closeExp: jest.fn().mockResolvedValue(fakeResult),
declineExp: jest.fn().mockResolvedValue(fakeResult),
}
} as unknown as jest.Mocked<ExpensesBlockchainPort>
generator = {
generateDocument: jest.fn().mockResolvedValue({} as never),
}
service = new ExpensesMutationsService(chain, generator as unknown as GeneratorInfrastructureService)
requisiteSnapshots = {
validate: jest.fn().mockResolvedValue(undefined),
snapshot: jest.fn().mockResolvedValue(undefined),
formatForOwner: jest.fn().mockResolvedValue('Банковский перевод: счёт 40817810000000000000, Банк ВТБ (ПАО)'),
}
service = new ExpensesMutationsService(
chain,
generator as unknown as GeneratorInfrastructureService,
requisiteSnapshots as never
)
})
const makeSignedDoc = (overrides: Partial<{ hash: string; doc_hash: string; meta_hash: string }> = {}) => ({
@@ -84,6 +91,7 @@ describe('ExpensesMutationsService', () => {
recipient: 'petrov',
description: 'Закупка кормов',
planned_amount: '5000.0000 RUB',
payment_method_id: 'pm-1',
},
],
statement: makeSignedDoc() as any,
@@ -103,22 +111,42 @@ describe('ExpensesMutationsService', () => {
expect(call.items[0].recipient_type).toBe(1)
expect(call.callback).toEqual({ contract: '', action: '', data: '' })
expect(call.statement.doc_hash).toBe('0xdoc')
// Реквизиты: валидация до блокчейна, снимок — после.
expect(requisiteSnapshots.validate).toHaveBeenCalledTimes(1)
expect(requisiteSnapshots.snapshot).toHaveBeenCalledTimes(1)
const snapItems = requisiteSnapshots.snapshot.mock.calls[0][1]
expect(snapItems[0]).toMatchObject({
proposalHash: '0xabc',
itemHash: '0xitem1',
recipient: 'petrov',
isOrganization: false,
paymentMethodId: 'pm-1',
})
})
it('authorizeExpenseReport → chain.authExp с decision document2', async () => {
const input: AuthorizeExpenseReportInputDTO = {
it('createExpenseProposal: ошибка валидации реквизитов не доходит до блокчейна', async () => {
requisiteSnapshots.validate.mockRejectedValue(new Error('Не указаны реквизиты получателя'))
const input: CreateExpenseProposalInputDTO = {
coopname: 'voskhod',
username: 'ivanov',
proposal_hash: '0xabc',
decision: makeSignedDoc({ doc_hash: '0xdecdoc' }) as any,
} as AuthorizeExpenseReportInputDTO
source_wallet: 'w.cap.blago',
items: [
{
item_hash: '0xitem1',
mechanics: ExpenseMechanics.ADVANCE,
recipient_type: ExpenseRecipientType.SELF,
recipient: 'ivanov',
description: 'Канцелярия',
planned_amount: '1000.0000 RUB',
},
],
statement: makeSignedDoc() as any,
} as CreateExpenseProposalInputDTO
await service.authorizeExpenseReport(input)
expect(chain.authExp).toHaveBeenCalledTimes(1)
const call = chain.authExp.mock.calls[0][0]
expect(call.coopname).toBe('voskhod')
expect(call.proposal_hash).toBe('0xabc')
expect(call.decision.doc_hash).toBe('0xdecdoc')
await expect(service.createExpenseProposal(input)).rejects.toThrow('Не указаны реквизиты')
expect(chain.createExp).not.toHaveBeenCalled()
})
it('payExpenseItem → chain.payExp({coopname, proposal_hash, item_hash, actual_amount})', async () => {
@@ -203,20 +231,4 @@ describe('ExpensesMutationsService', () => {
expect(chain.closeExp).toHaveBeenCalledWith({ coopname: 'voskhod', proposal_hash: '0xabc' })
})
it('declineExpenseReport → chain.declineExp({coopname, proposal_hash, reason})', async () => {
const input = {
coopname: 'voskhod',
proposal_hash: '0xabc',
reason: 'не утверждаю',
} as DeclineExpenseReportInputDTO
await service.declineExpenseReport(input)
expect(chain.declineExp).toHaveBeenCalledWith({
coopname: 'voskhod',
proposal_hash: '0xabc',
reason: 'не утверждаю',
})
})
})
@@ -1,38 +1,41 @@
import { Inject, Injectable } from '@nestjs/common'
import type { TransactResult } from '@wharfkit/session'
import type { InterExpenseRequisiteItemInput } from '@coopenomics/inter'
import { Cooperative } from 'cooptypes'
import { GeneratorInfrastructureService } from '~/infrastructure/generator/generator.service'
import type { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity'
import { ExpenseProposalStatementGenerateDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-statement-document.dto'
import { ExpenseProposalDecisionGenerateDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-decision-document.dto'
import { CreateExpenseProposalInputDTO } from '../dto/create-expense-proposal.input'
import type { ExpenseItemInputDTO } from '../dto/expense-item.input'
import { PayExpenseItemInputDTO } from '../dto/pay-expense-item.input'
import { ReportExpenseItemInputDTO } from '../dto/report-expense-item.input'
import { ReturnExpenseItemInputDTO } from '../dto/return-expense-item.input'
import { OverspendExpenseItemInputDTO } from '../dto/overspend-expense-item.input'
import { SubmitExpenseReportInputDTO } from '../dto/submit-expense-report.input'
import { AuthorizeExpenseReportInputDTO } from '../dto/authorize-expense-report.input'
import { DeclineExpenseReportInputDTO } from '../dto/decline-expense-report.input'
import {
EXPENSES_BLOCKCHAIN_PORT,
ExpensesBlockchainPort,
} from '../../domain/interfaces/expenses-blockchain.port'
import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum'
import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum'
import { ExpenseRequisiteSnapshotsService } from './expense-requisite-snapshots.service'
/**
* Write-сервис расходов (8 actions полного lifecycle).
* Write-сервис расходов.
*
* Подписывает ключом кооператива (`active`), `account = expense`. Для пайщик-actions
* (`reportexp` / `returnexp`) — тот же канон, что в capital (`createCommit` пайщика
* сервисно подписан кооперативом).
* сервисно подписан кооперативом). Авторизация / отклонение СЗ идут через решение
* совета (callbacks authexp/declexp от контракта soviet) — здесь их нет.
*/
@Injectable()
export class ExpensesMutationsService {
constructor(
@Inject(EXPENSES_BLOCKCHAIN_PORT)
private readonly chain: ExpensesBlockchainPort,
private readonly generator: GeneratorInfrastructureService
private readonly generator: GeneratorInfrastructureService,
private readonly requisiteSnapshots: ExpenseRequisiteSnapshotsService
) {}
async generateExpenseProposalStatementDocument(
@@ -40,6 +43,30 @@ export class ExpensesMutationsService {
options: Cooperative.Document.IGenerationOptions
): Promise<DocumentDomainEntity> {
data.registry_id = Cooperative.Registry.ExpenseProposalStatement.registry_id
// Полные реквизиты получателей подставляет сервер: фронт оперирует только
// идентификатором платёжного метода (и видит лишь сокращённое представление).
// Служебные поля (payment_method_id / recipient_username) в meta документа
// не попадают — items пересобираются на документные поля.
data.items = await Promise.all(
data.items.map(async (item) => {
let requisites = item.requisites
if (item.payment_method_id) {
const owner = item.recipient_username || data.username
requisites = await this.requisiteSnapshots.formatForOwner(owner, item.payment_method_id)
}
return {
number: item.number,
description: item.description,
amount: item.amount,
recipient_type: item.recipient_type,
mechanics: item.mechanics,
recipient_name: item.recipient_name,
requisites,
}
})
)
return this.generator.generateDocument({ data: data as unknown as Cooperative.Registry.ExpenseProposalStatement.Action, options: options || {} })
}
@@ -52,6 +79,11 @@ export class ExpensesMutationsService {
}
async createExpenseProposal(input: CreateExpenseProposalInputDTO): Promise<TransactResult> {
// Валидация реквизитов ДО блокчейна, снимок — ПОСЛЕ (канон gateway
// prepareWithdraw → persistWithdraw). Реквизиты в чейн не пишутся.
const requisiteItems = toRequisiteItems(input.proposal_hash, input.items)
await this.requisiteSnapshots.validate(input.coopname, requisiteItems)
const items = input.items.map((it) => ({
item_hash: it.item_hash,
mechanics: it.mechanics === ExpenseMechanics.DIRECT ? 1 : 0,
@@ -70,7 +102,7 @@ export class ExpensesMutationsService {
const statement = input.statement.toDocument()
return this.chain.createExp({
const result = await this.chain.createExp({
coopname: input.coopname,
username: input.username,
proposal_hash: input.proposal_hash,
@@ -90,6 +122,10 @@ export class ExpensesMutationsService {
signatures: statement.signatures,
} as any,
})
await this.requisiteSnapshots.snapshot(input.coopname, requisiteItems)
return result
}
async payExpenseItem(input: PayExpenseItemInputDTO): Promise<TransactResult> {
@@ -133,28 +169,15 @@ export class ExpensesMutationsService {
proposal_hash: input.proposal_hash,
})
}
async authorizeExpenseReport(input: AuthorizeExpenseReportInputDTO): Promise<TransactResult> {
const decision = input.decision.toDocument()
return this.chain.authExp({
coopname: input.coopname,
proposal_hash: input.proposal_hash,
decision: {
version: decision.version,
hash: decision.hash,
doc_hash: decision.doc_hash,
meta_hash: decision.meta_hash,
meta: decision.meta,
signatures: decision.signatures,
} as any,
})
}
async declineExpenseReport(input: DeclineExpenseReportInputDTO): Promise<TransactResult> {
return this.chain.declineExp({
coopname: input.coopname,
proposal_hash: input.proposal_hash,
reason: input.reason,
})
}
}
function toRequisiteItems(proposalHash: string, items: ExpenseItemInputDTO[]): InterExpenseRequisiteItemInput[] {
return items.map((it) => ({
proposalHash,
itemHash: it.item_hash,
recipient: it.recipient,
isOrganization: it.recipient_type === ExpenseRecipientType.ORG,
paymentMethodId: it.payment_method_id,
requisites: it.requisites,
}))
}
@@ -5,25 +5,25 @@ import type { TransactResult } from '@wharfkit/session'
* Блокчейн-порт контракта `expense`. Hexagonal: domain видит интерфейс,
* implementation в `infrastructure/blockchain/adapters`.
*
* 8 actions полного lifecycle:
* - `createexp` — создание + подача СЗ (signact1 statement_doc, type=2010)
* - `authexp` — авторизация советом (signact2 decision_doc, type=2011)
* 6 actions, доступных backend'у. Авторизация (`authexp`) и отклонение
* (`declexp`) СЗ исполняются контрактом soviet как callbacks решения совета —
* backend их не вызывает.
*
* - `createexp` — создание + подача СЗ (signact1 statement_doc, type=2010);
* ставит вопрос в повестку совета
* - `payexp` — оплата item (ADVANCE/DIRECT)
* - `reportexp` — закрытие item чеком (ADVANCE)
* - `returnexp` — возврат неиспользованного аванса
* - `overspendexp` — доплата при перерасходе (ADVANCE)
* - `closeexp` — финализация СЗ-отчёта (REPORT_SUBMITTED → CLOSED)
* - `declexp` — отклонение СЗ
*/
export interface ExpensesBlockchainPort {
createExp(data: ExpenseContract.Actions.CreateExp.ICreateExp): Promise<TransactResult>
authExp(data: ExpenseContract.Actions.AuthExp.IAuthExp): Promise<TransactResult>
payExp(data: ExpenseContract.Actions.PayExp.IPayExp): Promise<TransactResult>
reportExp(data: ExpenseContract.Actions.ReportExp.IReportExp): Promise<TransactResult>
returnExp(data: ExpenseContract.Actions.ReturnExp.IReturnExp): Promise<TransactResult>
overspendExp(data: ExpenseContract.Actions.OverspendExp.IOverspendExp): Promise<TransactResult>
closeExp(data: ExpenseContract.Actions.CloseExp.ICloseExp): Promise<TransactResult>
declineExp(data: ExpenseContract.Actions.DeclineExp.IDeclineExp): Promise<TransactResult>
}
export const EXPENSES_BLOCKCHAIN_PORT = Symbol('ExpensesBlockchainPort')
@@ -0,0 +1,24 @@
import type { PaymentMethodDomainEntity } from '~/domain/payment-method/entities/method-domain.entity';
import type {
SBPDataDomainInterface,
BankTransferDataDomainInterface,
} from '~/domain/payment-method/interfaces/payment-methods-domain.interface';
/**
* Полная строка реквизитов платёжного метода — для документов (СЗ в совет,
* поручение бухгалтеру). Сокращённое представление для UI делает фронт.
*/
export function formatPaymentMethodRequisites(method: PaymentMethodDomainEntity): string {
if (method.method_type === 'sbp') {
const data = method.data as SBPDataDomainInterface;
return `СБП, телефон ${data.phone}`;
}
const data = method.data as BankTransferDataDomainInterface;
const parts = [`Банковский перевод: счёт ${data.account_number}`];
if (data.bank_name) parts.push(data.bank_name);
if (data.details?.bik) parts.push(`БИК ${data.details.bik}`);
if (data.details?.corr) parts.push(`к/с ${data.details.corr}`);
if (data.card_number) parts.push(`карта ${data.card_number}`);
return parts.join(', ');
}
@@ -16,6 +16,7 @@ import { EXPENSES_BLOCKCHAIN_PORT } from './domain/interfaces/expenses-blockchai
import { ExpenseProposalSyncService } from './application/syncers/expense-proposal-sync.service';
import { ExpensesManagementService } from './application/services/expenses-management.service';
import { ExpensesMutationsService } from './application/services/expenses-mutations.service';
import { ExpenseRequisiteSnapshotsService } from './application/services/expense-requisite-snapshots.service';
import { ExpensesCapitalTriggerService } from './application/services/expenses-capital-trigger.service';
import { ExpenseFilesService } from './application/services/expense-files.service';
import { ExpenseProposalResolver } from './application/resolvers/expense-proposal.resolver';
@@ -66,6 +67,7 @@ import { ExpensesInterExpenseChassisAdapter } from './infrastructure/inter/expen
},
ExpenseProposalSyncService,
ExpensesManagementService,
ExpenseRequisiteSnapshotsService,
ExpensesMutationsService,
ExpensesCapitalTriggerService,
ExpenseFilesService,
@@ -11,9 +11,10 @@ import { ExpensesBlockchainPort } from '../../../domain/interfaces/expenses-bloc
* Адаптер блокчейн-порта `expense`. Канон взят с `CapitalBlockchainAdapter`:
* подпись ключом кооператива (`active` permission), `account = contractName.production`.
*
* 8 actions полного lifecycle: createexp / authexp / payexp / reportexp / returnexp /
* overspendexp / closeexp / declexp. Документы document2 (statement / decision) идут
* как часть `data` action'а — wharfkit сериализует поля по C++ struct document2.
* 6 backend-actions: createexp / payexp / reportexp / returnexp / overspendexp /
* closeexp (authexp/declexp вызывает контракт soviet по решению совета).
* Документы document2 (statement) идут как часть `data` action'а — wharfkit
* сериализует поля по C++ struct document2.
*/
@Injectable()
export class ExpensesBlockchainAdapter implements ExpensesBlockchainPort {
@@ -40,16 +41,6 @@ export class ExpensesBlockchainAdapter implements ExpensesBlockchainPort {
})
}
async authExp(data: ExpenseContract.Actions.AuthExp.IAuthExp): Promise<TransactResult> {
await this.initWithCoopKey(data.coopname)
return this.blockchainService.transact({
account: ExpenseContract.contractName.production,
name: ExpenseContract.Actions.AuthExp.actionName,
authorization: [{ actor: data.coopname, permission: 'active' }],
data,
})
}
async payExp(data: ExpenseContract.Actions.PayExp.IPayExp): Promise<TransactResult> {
await this.initWithCoopKey(data.coopname)
return this.blockchainService.transact({
@@ -99,14 +90,4 @@ export class ExpensesBlockchainAdapter implements ExpensesBlockchainPort {
data,
})
}
async declineExp(data: ExpenseContract.Actions.DeclineExp.IDeclineExp): Promise<TransactResult> {
await this.initWithCoopKey(data.coopname)
return this.blockchainService.transact({
account: ExpenseContract.contractName.production,
name: ExpenseContract.Actions.DeclineExp.actionName,
authorization: [{ actor: data.coopname, permission: 'active' }],
data,
})
}
}
@@ -2,16 +2,19 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ExpenseProposalTypeormEntity } from '../entities/expense-proposal.typeorm-entity';
import { ExpenseFileTypeormEntity } from '../entities/expense-file.typeorm-entity';
import { ExpenseRequisiteSnapshotTypeormEntity } from '../entities/expense-requisite-snapshot.typeorm-entity';
import { EntityVersionTypeormEntity } from '~/shared/sync/entities/entity-version.typeorm-entity';
/**
* TypeORM-модуль расширения `expense` — зеркало SP-расходов + реестр файлов.
* TypeORM-модуль расширения `expense` — зеркало SP-расходов + реестр файлов
* + снимки реквизитов получателей.
*/
@Module({
imports: [
TypeOrmModule.forFeature([
ExpenseProposalTypeormEntity,
ExpenseFileTypeormEntity,
ExpenseRequisiteSnapshotTypeormEntity,
EntityVersionTypeormEntity,
]),
],
@@ -0,0 +1,48 @@
import { Entity, Column, Index, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
export const EntityName = 'expense_requisite_snapshots';
/**
* Снимок реквизитов получателя по строке расхода — фиксируется в момент
* создания СЗ (как payment_details в gateway-платежах): последующее изменение
* или удаление платёжного метода пайщиком не меняет то, куда платить по уже
* поданной смете. Источник реквизитов для поручения бухгалтеру (payexp).
*/
@Entity(EntityName)
@Index(`uq_${EntityName}_item`, ['coopname', 'proposal_hash', 'item_hash'], { unique: true })
@Index(`idx_${EntityName}_proposal`, ['coopname', 'proposal_hash'])
export class ExpenseRequisiteSnapshotTypeormEntity {
static getTableName(): string {
return EntityName;
}
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar' })
coopname!: string;
@Column({ type: 'varchar' })
proposal_hash!: string;
@Column({ type: 'varchar' })
item_hash!: string;
@Column({ type: 'varchar', comment: 'Получатель платежа (username пайщика или имя организации)' })
recipient!: string;
@Column({ type: 'varchar', nullable: true, comment: 'Идентификатор платёжного метода получателя (для пайщиков)' })
method_id!: string | null;
@Column({ type: 'varchar', nullable: true, comment: 'Тип метода (sbp / bank_transfer)' })
method_type!: string | null;
@Column({ type: 'jsonb', nullable: true, comment: 'Снимок данных платёжного метода на момент создания СЗ' })
data!: Record<string, unknown> | null;
@Column({ type: 'text', comment: 'Реквизиты строкой — как в документе СЗ' })
requisites!: string;
@CreateDateColumn()
created_at!: Date;
}
@@ -8,9 +8,11 @@ import type {
InterExpensePaginatedResult,
InterExpenseProposalRead,
InterExpenseProposalStatus,
InterExpenseRequisiteItemInput,
} from '@coopenomics/inter';
import { ExpenseProposalTypeormEntity } from '../entities/expense-proposal.typeorm-entity';
import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum';
import { ExpenseRequisiteSnapshotsService } from '../../application/services/expense-requisite-snapshots.service';
/**
* Реализация `InterExpenseChassisPort` для consumer-расширений (capital, marketplace, EMP).
@@ -24,8 +26,17 @@ export class ExpensesInterExpenseChassisAdapter implements InterExpenseChassisPo
constructor(
@InjectRepository(ExpenseProposalTypeormEntity)
private readonly repository: Repository<ExpenseProposalTypeormEntity>,
private readonly requisiteSnapshots: ExpenseRequisiteSnapshotsService,
) {}
async validateRequisites(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise<void> {
await this.requisiteSnapshots.validate(coopname, items);
}
async snapshotRequisites(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise<void> {
await this.requisiteSnapshots.snapshot(coopname, items);
}
async readProposalByHash(coopname: string, proposalHash: string): Promise<InterExpenseProposalRead | null> {
const entity = await this.repository.findOne({
where: { coopname, proposal_hash: proposalHash.toLowerCase() },
-23
View File
@@ -125,9 +125,6 @@ export const AllTypesProps: Record<string,any> = {
AuthorizeDecisionInput:{
document:"SignedDigitalDocumentInput"
},
AuthorizeExpenseReportInput:{
decision:"ExpenseProposalDecisionSignedDocumentInput"
},
BankAccountDetailsInput:{
},
@@ -396,9 +393,6 @@ export const AllTypesProps: Record<string,any> = {
},
DeclineDecisionInput:{
},
DeclineExpenseReportInput:{
},
DeclineRequestInput:{
@@ -472,15 +466,6 @@ export const AllTypesProps: Record<string,any> = {
},
ExpenseProposalDecisionItemInput:{
},
ExpenseProposalDecisionSignedDocumentInput:{
meta:"ExpenseProposalDecisionSignedMetaDocumentInput",
signatures:"SignatureInfoInput"
},
ExpenseProposalDecisionSignedMetaDocumentInput:{
decision:"ExpenseProposalDecisionBodyInput",
items:"ExpenseProposalDecisionItemInput",
proposal:"ExpenseProposalDecisionHeaderInput"
},
ExpenseProposalHeaderInput:{
@@ -727,9 +712,6 @@ export const AllTypesProps: Record<string,any> = {
authorizeDecision:{
data:"AuthorizeDecisionInput"
},
authorizeExpenseReport:{
data:"AuthorizeExpenseReportInput"
},
cancelRequest:{
data:"CancelRequestInput"
},
@@ -1073,9 +1055,6 @@ export const AllTypesProps: Record<string,any> = {
declineDecision:{
data:"DeclineDecisionInput"
},
declineExpenseReport:{
data:"DeclineExpenseReportInput"
},
declineRequest:{
data:"DeclineRequestInput"
},
@@ -3802,7 +3781,6 @@ export const ReturnTypes: Record<string,any> = {
addPaymentMethod:"PaymentMethod",
addTrustedAccount:"Branch",
authorizeDecision:"Transaction",
authorizeExpenseReport:"Transaction",
cancelRequest:"Transaction",
capitalAddAuthor:"CapitalProject",
capitalApproveCommit:"CapitalCommit",
@@ -3911,7 +3889,6 @@ export const ReturnTypes: Record<string,any> = {
deactivateWebPushSubscriptionById:"Boolean",
declineAgreement:"Transaction",
declineDecision:"Transaction",
declineExpenseReport:"Transaction",
declineRequest:"Transaction",
deleteAccount:"Boolean",
deleteBranch:"Boolean",
+36 -276
View File
@@ -1940,14 +1940,6 @@ export type ValueTypes = {
decision_id: number | Variable<any, string>,
/** Подписанный председателем документ утверждения решения */
document: ValueTypes["SignedDigitalDocumentInput"] | Variable<any, string>
};
["AuthorizeExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string | Variable<any, string>,
/** Подписанное решение совета (document2, registry 2011). */
decision: ValueTypes["ExpenseProposalDecisionSignedDocumentInput"] | Variable<any, string>,
/** Хеш сметы расхода. */
proposal_hash: string | Variable<any, string>
};
["AvailableReport"]: AliasType<{
deadline?:boolean | `@${string}`,
@@ -4924,14 +4916,6 @@ export type ValueTypes = {
coopname: string | Variable<any, string>,
/** Идентификатор решения */
decision_id: number | Variable<any, string>
};
["DeclineExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string | Variable<any, string>,
/** Хеш сметы расхода. */
proposal_hash: string | Variable<any, string>,
/** Причина отклонения СЗ-отчёта (до 1000 символов). */
reason: string | Variable<any, string>
};
["DeclineRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -5296,12 +5280,16 @@ export type ValueTypes = {
item_hash: string | Variable<any, string>,
/** Способ оплаты (ADVANCE / DIRECT). */
mechanics: ValueTypes["ExpenseMechanics"] | Variable<any, string>,
/** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */
payment_method_id?: string | undefined | null | Variable<any, string>,
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
planned_amount: string | Variable<any, string>,
/** Получатель (username / eosio::name организации). */
recipient: string | Variable<any, string>,
/** Тип получателя. */
recipient_type: ValueTypes["ExpenseRecipientType"] | Variable<any, string>
recipient_type: ValueTypes["ExpenseRecipientType"] | Variable<any, string>,
/** Реквизиты получателя-организации (вводятся вручную). */
requisites?: string | undefined | null | Variable<any, string>
};
/** Статус строки расхода. */
["ExpenseItemStatus"]:ExpenseItemStatus;
@@ -5404,52 +5392,6 @@ export type ValueTypes = {
recipient_name?: string | undefined | null | Variable<any, string>,
recipient_type: string | Variable<any, string>,
requisites?: string | undefined | null | Variable<any, string>
};
["ExpenseProposalDecisionSignedDocumentInput"]: {
/** Хэш содержимого документа */
doc_hash: string | Variable<any, string>,
/** Общий хэш (doc_hash + meta_hash) */
hash: string | Variable<any, string>,
/** Метаинформация решения по СЗ */
meta: ValueTypes["ExpenseProposalDecisionSignedMetaDocumentInput"] | Variable<any, string>,
/** Хэш мета-данных */
meta_hash: string | Variable<any, string>,
/** Вектор подписей */
signatures: Array<ValueTypes["SignatureInfoInput"]> | Variable<any, string>,
/** Версия стандарта документа */
version: string | Variable<any, string>
};
["ExpenseProposalDecisionSignedMetaDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num: number | Variable<any, string>,
/** Название кооператива, связанное с документом */
coopname: string | Variable<any, string>,
/** Дата и время создания документа */
created_at: string | Variable<any, string>,
/** Тело решения */
decision: ValueTypes["ExpenseProposalDecisionBodyInput"] | Variable<any, string>,
/** Имя генератора, использованного для создания документа */
generator: string | Variable<any, string>,
/** Позиции расхода */
items: Array<ValueTypes["ExpenseProposalDecisionItemInput"]> | Variable<any, string>,
/** Язык документа */
lang: string | Variable<any, string>,
/** Ссылки, связанные с документом */
links: Array<string> | Variable<any, string>,
/** Шапка СЗ */
proposal: ValueTypes["ExpenseProposalDecisionHeaderInput"] | Variable<any, string>,
/** Хеш сметы расхода */
proposal_hash: string | Variable<any, string>,
/** ID документа в реестре */
registry_id: number | Variable<any, string>,
/** Часовой пояс, в котором был создан документ */
timezone: string | Variable<any, string>,
/** Название документа */
title: string | Variable<any, string>,
/** Имя пользователя, создавшего документ */
username: string | Variable<any, string>,
/** Версия генератора, использованного для создания документа */
version: string | Variable<any, string>
};
["ExpenseProposalHeaderInput"]: {
/** Описание цели расходов */
@@ -5470,10 +5412,14 @@ export type ValueTypes = {
mechanics: string | Variable<any, string>,
/** Порядковый номер строки */
number: string | Variable<any, string>,
/** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */
payment_method_id?: string | undefined | null | Variable<any, string>,
/** Имя получателя */
recipient_name?: string | undefined | null | Variable<any, string>,
/** Тип получателя (SELF / MEMBER / ORG) */
recipient_type: string | Variable<any, string>,
/** Имя аккаунта получателя-пайщика (владелец реквизитов). */
recipient_username?: string | undefined | null | Variable<any, string>,
/** Реквизиты получателя */
requisites?: string | undefined | null | Variable<any, string>
};
@@ -6890,7 +6836,6 @@ addParticipant?: [{ data: ValueTypes["AddParticipantInput"] | Variable<any, stri
addPaymentMethod?: [{ data: ValueTypes["AddPaymentMethodInput"] | Variable<any, string>},ValueTypes["PaymentMethod"]],
addTrustedAccount?: [{ data: ValueTypes["AddTrustedAccountInput"] | Variable<any, string>},ValueTypes["Branch"]],
authorizeDecision?: [{ data: ValueTypes["AuthorizeDecisionInput"] | Variable<any, string>},ValueTypes["Transaction"]],
authorizeExpenseReport?: [{ data: ValueTypes["AuthorizeExpenseReportInput"] | Variable<any, string>},ValueTypes["Transaction"]],
cancelRequest?: [{ data: ValueTypes["CancelRequestInput"] | Variable<any, string>},ValueTypes["Transaction"]],
capitalAddAuthor?: [{ data: ValueTypes["AddAuthorInput"] | Variable<any, string>},ValueTypes["CapitalProject"]],
capitalApproveCommit?: [{ data: ValueTypes["CommitApproveInput"] | Variable<any, string>},ValueTypes["CapitalCommit"]],
@@ -7002,7 +6947,6 @@ createWithdraw?: [{ data: ValueTypes["CreateWithdrawInput"] | Variable<any, stri
deactivateWebPushSubscriptionById?: [{ data: ValueTypes["DeactivateSubscriptionInput"] | Variable<any, string>},boolean | `@${string}`],
declineAgreement?: [{ data: ValueTypes["DeclineAgreementInput"] | Variable<any, string>},ValueTypes["Transaction"]],
declineDecision?: [{ data: ValueTypes["DeclineDecisionInput"] | Variable<any, string>},ValueTypes["Transaction"]],
declineExpenseReport?: [{ data: ValueTypes["DeclineExpenseReportInput"] | Variable<any, string>},ValueTypes["Transaction"]],
declineRequest?: [{ data: ValueTypes["DeclineRequestInput"] | Variable<any, string>},ValueTypes["Transaction"]],
deleteAccount?: [{ data: ValueTypes["DeleteAccountInput"] | Variable<any, string>},boolean | `@${string}`],
deleteBranch?: [{ data: ValueTypes["DeleteBranchInput"] | Variable<any, string>},boolean | `@${string}`],
@@ -11111,14 +11055,6 @@ export type ResolverInputTypes = {
decision_id: number,
/** Подписанный председателем документ утверждения решения */
document: ResolverInputTypes["SignedDigitalDocumentInput"]
};
["AuthorizeExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Подписанное решение совета (document2, registry 2011). */
decision: ResolverInputTypes["ExpenseProposalDecisionSignedDocumentInput"],
/** Хеш сметы расхода. */
proposal_hash: string
};
["AvailableReport"]: AliasType<{
deadline?:boolean | `@${string}`,
@@ -14018,14 +13954,6 @@ export type ResolverInputTypes = {
coopname: string,
/** Идентификатор решения */
decision_id: number
};
["DeclineExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Хеш сметы расхода. */
proposal_hash: string,
/** Причина отклонения СЗ-отчёта (до 1000 символов). */
reason: string
};
["DeclineRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -14378,12 +14306,16 @@ export type ResolverInputTypes = {
item_hash: string,
/** Способ оплаты (ADVANCE / DIRECT). */
mechanics: ResolverInputTypes["ExpenseMechanics"],
/** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */
payment_method_id?: string | undefined | null,
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
planned_amount: string,
/** Получатель (username / eosio::name организации). */
recipient: string,
/** Тип получателя. */
recipient_type: ResolverInputTypes["ExpenseRecipientType"]
recipient_type: ResolverInputTypes["ExpenseRecipientType"],
/** Реквизиты получателя-организации (вводятся вручную). */
requisites?: string | undefined | null
};
/** Статус строки расхода. */
["ExpenseItemStatus"]:ExpenseItemStatus;
@@ -14485,52 +14417,6 @@ export type ResolverInputTypes = {
recipient_name?: string | undefined | null,
recipient_type: string,
requisites?: string | undefined | null
};
["ExpenseProposalDecisionSignedDocumentInput"]: {
/** Хэш содержимого документа */
doc_hash: string,
/** Общий хэш (doc_hash + meta_hash) */
hash: string,
/** Метаинформация решения по СЗ */
meta: ResolverInputTypes["ExpenseProposalDecisionSignedMetaDocumentInput"],
/** Хэш мета-данных */
meta_hash: string,
/** Вектор подписей */
signatures: Array<ResolverInputTypes["SignatureInfoInput"]>,
/** Версия стандарта документа */
version: string
};
["ExpenseProposalDecisionSignedMetaDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num: number,
/** Название кооператива, связанное с документом */
coopname: string,
/** Дата и время создания документа */
created_at: string,
/** Тело решения */
decision: ResolverInputTypes["ExpenseProposalDecisionBodyInput"],
/** Имя генератора, использованного для создания документа */
generator: string,
/** Позиции расхода */
items: Array<ResolverInputTypes["ExpenseProposalDecisionItemInput"]>,
/** Язык документа */
lang: string,
/** Ссылки, связанные с документом */
links: Array<string>,
/** Шапка СЗ */
proposal: ResolverInputTypes["ExpenseProposalDecisionHeaderInput"],
/** Хеш сметы расхода */
proposal_hash: string,
/** ID документа в реестре */
registry_id: number,
/** Часовой пояс, в котором был создан документ */
timezone: string,
/** Название документа */
title: string,
/** Имя пользователя, создавшего документ */
username: string,
/** Версия генератора, использованного для создания документа */
version: string
};
["ExpenseProposalHeaderInput"]: {
/** Описание цели расходов */
@@ -14551,10 +14437,14 @@ export type ResolverInputTypes = {
mechanics: string,
/** Порядковый номер строки */
number: string,
/** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */
payment_method_id?: string | undefined | null,
/** Имя получателя */
recipient_name?: string | undefined | null,
/** Тип получателя (SELF / MEMBER / ORG) */
recipient_type: string,
/** Имя аккаунта получателя-пайщика (владелец реквизитов). */
recipient_username?: string | undefined | null,
/** Реквизиты получателя */
requisites?: string | undefined | null
};
@@ -15932,7 +15822,6 @@ addParticipant?: [{ data: ResolverInputTypes["AddParticipantInput"]},ResolverInp
addPaymentMethod?: [{ data: ResolverInputTypes["AddPaymentMethodInput"]},ResolverInputTypes["PaymentMethod"]],
addTrustedAccount?: [{ data: ResolverInputTypes["AddTrustedAccountInput"]},ResolverInputTypes["Branch"]],
authorizeDecision?: [{ data: ResolverInputTypes["AuthorizeDecisionInput"]},ResolverInputTypes["Transaction"]],
authorizeExpenseReport?: [{ data: ResolverInputTypes["AuthorizeExpenseReportInput"]},ResolverInputTypes["Transaction"]],
cancelRequest?: [{ data: ResolverInputTypes["CancelRequestInput"]},ResolverInputTypes["Transaction"]],
capitalAddAuthor?: [{ data: ResolverInputTypes["AddAuthorInput"]},ResolverInputTypes["CapitalProject"]],
capitalApproveCommit?: [{ data: ResolverInputTypes["CommitApproveInput"]},ResolverInputTypes["CapitalCommit"]],
@@ -16044,7 +15933,6 @@ createWithdraw?: [{ data: ResolverInputTypes["CreateWithdrawInput"]},ResolverInp
deactivateWebPushSubscriptionById?: [{ data: ResolverInputTypes["DeactivateSubscriptionInput"]},boolean | `@${string}`],
declineAgreement?: [{ data: ResolverInputTypes["DeclineAgreementInput"]},ResolverInputTypes["Transaction"]],
declineDecision?: [{ data: ResolverInputTypes["DeclineDecisionInput"]},ResolverInputTypes["Transaction"]],
declineExpenseReport?: [{ data: ResolverInputTypes["DeclineExpenseReportInput"]},ResolverInputTypes["Transaction"]],
declineRequest?: [{ data: ResolverInputTypes["DeclineRequestInput"]},ResolverInputTypes["Transaction"]],
deleteAccount?: [{ data: ResolverInputTypes["DeleteAccountInput"]},boolean | `@${string}`],
deleteBranch?: [{ data: ResolverInputTypes["DeleteBranchInput"]},boolean | `@${string}`],
@@ -20033,14 +19921,6 @@ export type ModelTypes = {
decision_id: number,
/** Подписанный председателем документ утверждения решения */
document: ModelTypes["SignedDigitalDocumentInput"]
};
["AuthorizeExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Подписанное решение совета (document2, registry 2011). */
decision: ModelTypes["ExpenseProposalDecisionSignedDocumentInput"],
/** Хеш сметы расхода. */
proposal_hash: string
};
["AvailableReport"]: {
deadline: string,
@@ -22855,14 +22735,6 @@ export type ModelTypes = {
coopname: string,
/** Идентификатор решения */
decision_id: number
};
["DeclineExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Хеш сметы расхода. */
proposal_hash: string,
/** Причина отклонения СЗ-отчёта (до 1000 символов). */
reason: string
};
["DeclineRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -23201,12 +23073,16 @@ export type ModelTypes = {
item_hash: string,
/** Способ оплаты (ADVANCE / DIRECT). */
mechanics: ModelTypes["ExpenseMechanics"],
/** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */
payment_method_id?: string | undefined | null,
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
planned_amount: string,
/** Получатель (username / eosio::name организации). */
recipient: string,
/** Тип получателя. */
recipient_type: ModelTypes["ExpenseRecipientType"]
recipient_type: ModelTypes["ExpenseRecipientType"],
/** Реквизиты получателя-организации (вводятся вручную). */
requisites?: string | undefined | null
};
["ExpenseItemStatus"]:ExpenseItemStatus;
["ExpenseMechanics"]:ExpenseMechanics;
@@ -23305,52 +23181,6 @@ export type ModelTypes = {
recipient_name?: string | undefined | null,
recipient_type: string,
requisites?: string | undefined | null
};
["ExpenseProposalDecisionSignedDocumentInput"]: {
/** Хэш содержимого документа */
doc_hash: string,
/** Общий хэш (doc_hash + meta_hash) */
hash: string,
/** Метаинформация решения по СЗ */
meta: ModelTypes["ExpenseProposalDecisionSignedMetaDocumentInput"],
/** Хэш мета-данных */
meta_hash: string,
/** Вектор подписей */
signatures: Array<ModelTypes["SignatureInfoInput"]>,
/** Версия стандарта документа */
version: string
};
["ExpenseProposalDecisionSignedMetaDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num: number,
/** Название кооператива, связанное с документом */
coopname: string,
/** Дата и время создания документа */
created_at: string,
/** Тело решения */
decision: ModelTypes["ExpenseProposalDecisionBodyInput"],
/** Имя генератора, использованного для создания документа */
generator: string,
/** Позиции расхода */
items: Array<ModelTypes["ExpenseProposalDecisionItemInput"]>,
/** Язык документа */
lang: string,
/** Ссылки, связанные с документом */
links: Array<string>,
/** Шапка СЗ */
proposal: ModelTypes["ExpenseProposalDecisionHeaderInput"],
/** Хеш сметы расхода */
proposal_hash: string,
/** ID документа в реестре */
registry_id: number,
/** Часовой пояс, в котором был создан документ */
timezone: string,
/** Название документа */
title: string,
/** Имя пользователя, создавшего документ */
username: string,
/** Версия генератора, использованного для создания документа */
version: string
};
["ExpenseProposalHeaderInput"]: {
/** Описание цели расходов */
@@ -23371,10 +23201,14 @@ export type ModelTypes = {
mechanics: string,
/** Порядковый номер строки */
number: string,
/** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */
payment_method_id?: string | undefined | null,
/** Имя получателя */
recipient_name?: string | undefined | null,
/** Тип получателя (SELF / MEMBER / ORG) */
recipient_type: string,
/** Имя аккаунта получателя-пайщика (владелец реквизитов). */
recipient_username?: string | undefined | null,
/** Реквизиты получателя */
requisites?: string | undefined | null
};
@@ -24713,10 +24547,6 @@ export type ModelTypes = {
Требуемые роли: chairman. */
authorizeDecision: ModelTypes["Transaction"],
/** Утвердить СЗ-отчёт (закрытие сметы). Триггерит капитализацию РИД в Благоросте.
Требуемые роли: chairman. */
authorizeExpenseReport: ModelTypes["Transaction"],
/** Отменить заявку */
cancelRequest: ModelTypes["Transaction"],
/** Добавление автора проекта в CAPITAL контракте
@@ -25133,10 +24963,6 @@ export type ModelTypes = {
Требуемые роли: chairman. */
declineDecision: ModelTypes["Transaction"],
/** Отклонить СЗ-отчёт с указанием причины. Смета переходит в DECLINED.
Требуемые роли: chairman. */
declineExpenseReport: ModelTypes["Transaction"],
/** Отклонить заявку */
declineRequest: ModelTypes["Transaction"],
/** Удалить аккаунт пайщика из системы учёта провайдера. Доступно только для незавершённых регистрационных статусов (черновик, неоплачен/отклонён). Активный, заблокированный и любой зарегистрированный в блокчейне аккаунт удалить нельзя. Используется для очистки реестра и освобождения e-mail под перерегистрацию.
@@ -29461,14 +29287,6 @@ export type GraphQLTypes = {
decision_id: number,
/** Подписанный председателем документ утверждения решения */
document: GraphQLTypes["SignedDigitalDocumentInput"]
};
["AuthorizeExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Подписанное решение совета (document2, registry 2011). */
decision: GraphQLTypes["ExpenseProposalDecisionSignedDocumentInput"],
/** Хеш сметы расхода. */
proposal_hash: string
};
["AvailableReport"]: {
__typename: "AvailableReport",
@@ -32445,14 +32263,6 @@ export type GraphQLTypes = {
coopname: string,
/** Идентификатор решения */
decision_id: number
};
["DeclineExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Хеш сметы расхода. */
proposal_hash: string,
/** Причина отклонения СЗ-отчёта (до 1000 символов). */
reason: string
};
["DeclineRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -32817,12 +32627,16 @@ export type GraphQLTypes = {
item_hash: string,
/** Способ оплаты (ADVANCE / DIRECT). */
mechanics: GraphQLTypes["ExpenseMechanics"],
/** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */
payment_method_id?: string | undefined | null,
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
planned_amount: string,
/** Получатель (username / eosio::name организации). */
recipient: string,
/** Тип получателя. */
recipient_type: GraphQLTypes["ExpenseRecipientType"]
recipient_type: GraphQLTypes["ExpenseRecipientType"],
/** Реквизиты получателя-организации (вводятся вручную). */
requisites?: string | undefined | null
};
/** Статус строки расхода. */
["ExpenseItemStatus"]: ExpenseItemStatus;
@@ -32925,52 +32739,6 @@ export type GraphQLTypes = {
recipient_name?: string | undefined | null,
recipient_type: string,
requisites?: string | undefined | null
};
["ExpenseProposalDecisionSignedDocumentInput"]: {
/** Хэш содержимого документа */
doc_hash: string,
/** Общий хэш (doc_hash + meta_hash) */
hash: string,
/** Метаинформация решения по СЗ */
meta: GraphQLTypes["ExpenseProposalDecisionSignedMetaDocumentInput"],
/** Хэш мета-данных */
meta_hash: string,
/** Вектор подписей */
signatures: Array<GraphQLTypes["SignatureInfoInput"]>,
/** Версия стандарта документа */
version: string
};
["ExpenseProposalDecisionSignedMetaDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num: number,
/** Название кооператива, связанное с документом */
coopname: string,
/** Дата и время создания документа */
created_at: string,
/** Тело решения */
decision: GraphQLTypes["ExpenseProposalDecisionBodyInput"],
/** Имя генератора, использованного для создания документа */
generator: string,
/** Позиции расхода */
items: Array<GraphQLTypes["ExpenseProposalDecisionItemInput"]>,
/** Язык документа */
lang: string,
/** Ссылки, связанные с документом */
links: Array<string>,
/** Шапка СЗ */
proposal: GraphQLTypes["ExpenseProposalDecisionHeaderInput"],
/** Хеш сметы расхода */
proposal_hash: string,
/** ID документа в реестре */
registry_id: number,
/** Часовой пояс, в котором был создан документ */
timezone: string,
/** Название документа */
title: string,
/** Имя пользователя, создавшего документ */
username: string,
/** Версия генератора, использованного для создания документа */
version: string
};
["ExpenseProposalHeaderInput"]: {
/** Описание цели расходов */
@@ -32991,10 +32759,14 @@ export type GraphQLTypes = {
mechanics: string,
/** Порядковый номер строки */
number: string,
/** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */
payment_method_id?: string | undefined | null,
/** Имя получателя */
recipient_name?: string | undefined | null,
/** Тип получателя (SELF / MEMBER / ORG) */
recipient_type: string,
/** Имя аккаунта получателя-пайщика (владелец реквизитов). */
recipient_username?: string | undefined | null,
/** Реквизиты получателя */
requisites?: string | undefined | null
};
@@ -34423,10 +34195,6 @@ export type GraphQLTypes = {
Требуемые роли: chairman. */
authorizeDecision: GraphQLTypes["Transaction"],
/** Утвердить СЗ-отчёт (закрытие сметы). Триггерит капитализацию РИД в Благоросте.
Требуемые роли: chairman. */
authorizeExpenseReport: GraphQLTypes["Transaction"],
/** Отменить заявку */
cancelRequest: GraphQLTypes["Transaction"],
/** Добавление автора проекта в CAPITAL контракте
@@ -34843,10 +34611,6 @@ export type GraphQLTypes = {
Требуемые роли: chairman. */
declineDecision: GraphQLTypes["Transaction"],
/** Отклонить СЗ-отчёт с указанием причины. Смета переходит в DECLINED.
Требуемые роли: chairman. */
declineExpenseReport: GraphQLTypes["Transaction"],
/** Отклонить заявку */
declineRequest: GraphQLTypes["Transaction"],
/** Удалить аккаунт пайщика из системы учёта провайдера. Доступно только для незавершённых регистрационных статусов (черновик, неоплачен/отклонён). Активный, заблокированный и любой зарегистрированный в блокчейне аккаунт удалить нельзя. Используется для очистки реестра и освобождения e-mail под перерегистрацию.
@@ -39065,7 +38829,6 @@ type ZEUS_VARIABLES = {
["AssetContributionStatementSignedDocumentInput"]: ValueTypes["AssetContributionStatementSignedDocumentInput"];
["AssetContributionStatementSignedMetaDocumentInput"]: ValueTypes["AssetContributionStatementSignedMetaDocumentInput"];
["AuthorizeDecisionInput"]: ValueTypes["AuthorizeDecisionInput"];
["AuthorizeExpenseReportInput"]: ValueTypes["AuthorizeExpenseReportInput"];
["BankAccountDetailsInput"]: ValueTypes["BankAccountDetailsInput"];
["BankAccountInput"]: ValueTypes["BankAccountInput"];
["BuhotchSignerType"]: ValueTypes["BuhotchSignerType"];
@@ -39155,7 +38918,6 @@ type ZEUS_VARIABLES = {
["DeclineAgreementInput"]: ValueTypes["DeclineAgreementInput"];
["DeclineApproveInput"]: ValueTypes["DeclineApproveInput"];
["DeclineDecisionInput"]: ValueTypes["DeclineDecisionInput"];
["DeclineExpenseReportInput"]: ValueTypes["DeclineExpenseReportInput"];
["DeclineRequestInput"]: ValueTypes["DeclineRequestInput"];
["DeleteAccountInput"]: ValueTypes["DeleteAccountInput"];
["DeleteBranchInput"]: ValueTypes["DeleteBranchInput"];
@@ -39182,8 +38944,6 @@ type ZEUS_VARIABLES = {
["ExpenseProposalDecisionGenerateDocumentInput"]: ValueTypes["ExpenseProposalDecisionGenerateDocumentInput"];
["ExpenseProposalDecisionHeaderInput"]: ValueTypes["ExpenseProposalDecisionHeaderInput"];
["ExpenseProposalDecisionItemInput"]: ValueTypes["ExpenseProposalDecisionItemInput"];
["ExpenseProposalDecisionSignedDocumentInput"]: ValueTypes["ExpenseProposalDecisionSignedDocumentInput"];
["ExpenseProposalDecisionSignedMetaDocumentInput"]: ValueTypes["ExpenseProposalDecisionSignedMetaDocumentInput"];
["ExpenseProposalHeaderInput"]: ValueTypes["ExpenseProposalHeaderInput"];
["ExpenseProposalItemInput"]: ValueTypes["ExpenseProposalItemInput"];
["ExpenseProposalStatementGenerateDocumentInput"]: ValueTypes["ExpenseProposalStatementGenerateDocumentInput"];
@@ -44,26 +44,25 @@ export interface Model {
export const title = 'Служебная записка-смета о расходах'
export const description = 'Заявка-смета председателю кооператива на финансирование расходов по программе. Содержит описание цели и массив позиций расхода.'
export const context = `<style>h1{margin:0;text-align:center;}h3{margin:0;padding-top:15px;text-align:center;}.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;}.summary{margin-top:20px;text-align:right;font-weight:bold;}.signature{padding-top:30px;}</style><div class="digital-document"><div style="padding-bottom:30px;"><h1 style="text-align:center">{% trans 'PROPOSAL_TITLE' %}</h1><p style="text-align:center">{{vars.full_abbr_genitive}} «{{vars.name}}»</p><p style="text-align:right;padding-top:20px">{{ coop.city }}, {{ meta.created_at }}</p></div><h3>{% trans 'PURPOSE' %}</h3><p>{{ proposal.description }}</p><h3 style="padding-top:20px;">{% trans 'ITEMS' %}</h3><table><thead><tr><th>№</th><th>{% trans 'ITEM_DESCRIPTION' %}</th><th>{% trans 'ITEM_AMOUNT' %}</th><th>{% trans 'ITEM_RECIPIENT' %}</th><th>{% trans 'ITEM_MECHANICS' %}</th><th>{% trans 'ITEM_REQUISITES' %}</th></tr></thead><tbody>{% for item in items %}<tr><td>{{ item.number }}</td><td>{{ item.description }}</td><td>{{ item.amount }}</td><td>{% if item.recipient_type == 'SELF' %}{% trans 'RECIPIENT_SELF' %}{% elif item.recipient_type == 'MEMBER' %}{% trans 'RECIPIENT_MEMBER' %}: {{ item.recipient_name }}{% else %}{% trans 'RECIPIENT_ORG' %}: {{ item.recipient_name }}{% endif %}</td><td>{% if item.mechanics == 'ADVANCE' %}{% trans 'MECH_ADVANCE' %}{% else %}{% trans 'MECH_DIRECT' %}{% endif %}</td><td>{{ item.requisites }}</td></tr>{% endfor %}</tbody></table><p class="summary">{% trans 'TOTAL' %}: {{ proposal.total_amount }} ({{ proposal.items_count }} {% trans 'ITEMS_COUNT_SUFFIX' %})</p><p style="padding-top:15px;">{% trans 'SOURCE_WALLET' %}: {{ proposal.source_wallet }}</p><div class="signature"><p>{{ user.full_name_or_short_name }}</p><p>{% trans 'SIGNED_DIGITALLY' %}</p></div></div>`
// Вёрстка по бумажному образцу служебной записки: шапка-адресат справа,
// дата слева, центрированный заголовок с идентификатором (= proposal_hash,
// по нему записку можно найти), позиции — нумерованным списком с полными
// реквизитами получателей. Кошелёк-источник в документе не указывается.
export const context = `<style>h1{margin:0;text-align:center;}.digital-document{padding:20px;}.doc-header{text-align:right;padding-bottom:25px;}.doc-header p{margin:2px 0;}.doc-id{text-align:center;font-size:12px;word-break:break-all;padding-top:6px;padding-bottom:25px;}ol.items{padding-left:25px;margin:10px 0;}ol.items li{padding-bottom:10px;}.signature{padding-top:40px;}</style><div class="digital-document"><div class="doc-header"><p>{% trans 'TO_COUNCIL' %} {{vars.full_abbr_genitive}} «{{vars.name}}»</p><p>{% trans 'FROM_MEMBER' %} {{ user.full_name_or_short_name }}</p></div><p>{% trans 'DATE' %}: {{ meta.created_at }}</p><h1 style="padding-top:25px;">{% trans 'PROPOSAL_TITLE' %}</h1><p class="doc-id">№ {{ proposal_hash }}</p><p>{% trans 'BODY_INTRO' %} {{ proposal.total_amount }}, {% trans 'BODY_NAMELY' %}:</p><ol class="items">{% for item in items %}<li>{{ item.description }} — {{ item.amount }} ({% if item.mechanics == 'ADVANCE' %}{% trans 'MECH_ADVANCE' %}{% else %}{% trans 'MECH_DIRECT' %}{% endif %}). {% trans 'ITEM_RECIPIENT' %}: {% if item.recipient_type == 'SELF' %}{{ user.full_name_or_short_name }}{% else %}{{ item.recipient_name }}{% endif %}{% if item.requisites %}. {% trans 'ITEM_REQUISITES' %}: {{ item.requisites }}{% endif %}</li>{% endfor %}</ol><p>{% trans 'PURPOSE' %}: {{ proposal.description }}</p><div class="signature"><p>{{ user.full_name_or_short_name }}</p><p>{% trans 'SIGNED_DIGITALLY' %}</p></div></div>`
export const translations = {
ru: {
PROPOSAL_TITLE: 'СЛУЖЕБНАЯ ЗАПИСКА-СМЕТА О РАСХОДАХ',
PURPOSE: 'ЦЕЛЬ РАСХОДОВ',
ITEMS: 'ПОЗИЦИИ РАСХОДА',
ITEM_DESCRIPTION: 'Описание',
ITEM_AMOUNT: 'Сумма',
TO_COUNCIL: 'В Совет',
FROM_MEMBER: 'от пайщика',
DATE: 'Дата',
PROPOSAL_TITLE: 'СЛУЖЕБНАЯ ЗАПИСКА',
BODY_INTRO: 'Прошу согласовать списание затрат в общей сумме',
BODY_NAMELY: 'а именно',
MECH_ADVANCE: 'аванс под отчёт',
MECH_DIRECT: 'прямая оплата',
ITEM_RECIPIENT: 'Получатель',
ITEM_MECHANICS: 'Механика',
ITEM_REQUISITES: 'Реквизиты',
RECIPIENT_SELF: 'Я (creator)',
RECIPIENT_MEMBER: 'Пайщик',
RECIPIENT_ORG: 'Организация',
MECH_ADVANCE: 'Аванс под отчёт',
MECH_DIRECT: 'Прямая оплата',
TOTAL: 'Итого',
ITEMS_COUNT_SUFFIX: 'поз.',
SOURCE_WALLET: 'Источник средств',
PURPOSE: 'Цель расходов',
SIGNED_DIGITALLY: 'подписано электронной подписью',
},
}
@@ -73,6 +72,7 @@ export const exampleData = {
meta: { created_at: '02.06.2026 14:00' },
vars: { full_abbr_genitive: 'ПК', name: 'Восход' },
user: { full_name_or_short_name: 'Иванов И.И.' },
proposal_hash: '55c470039a8c53ce1b4b6e842fe8063ab3d5b85ba2ba8ab0ae6e30be3ad328b7',
proposal: {
description: 'Закупка хостинга и бухгалтерских услуг на июнь 2026',
total_amount: '15000.00 RUB',
@@ -96,7 +96,7 @@ export const exampleData = {
recipient_type: 'SELF',
mechanics: 'ADVANCE',
recipient_name: '',
requisites: '',
requisites: 'Банковский перевод: счёт 40817810000000000000, Банк ВТБ (ПАО), БИК 044525187',
},
],
}
@@ -21,6 +21,8 @@ export interface ICreateProgramExpenseDraftItem {
requisites?: string;
recipient_account?: string;
item_hash?: string;
/** Идентификатор платёжного метода получателя-пайщика (SELF/MEMBER). */
payment_method_id?: string | null;
}
export interface ICreateProgramExpenseDraft {
@@ -63,6 +65,8 @@ export function useCreateProgramExpense() {
const totalAmount = draft.items.reduce((sum, it) => sum + parseFloat(it.amount || '0'), 0);
const total_amount = formatToAsset(totalAmount, symbol, precision);
// Полные реквизиты в документ подставляет сервер по payment_method_id;
// фронт знает только сокращённое представление.
const itemsForDoc = draft.items.map((it, idx) => ({
number: String(idx + 1),
description: it.description,
@@ -71,6 +75,11 @@ export function useCreateProgramExpense() {
mechanics: it.mechanics,
recipient_name: it.recipient_name ?? '',
requisites: it.requisites ?? '',
payment_method_id: it.payment_method_id ?? undefined,
recipient_username:
it.recipient_type === Zeus.ExpenseRecipientType.MEMBER
? it.recipient_account?.trim()
: undefined,
}));
const generateInput: GenerateStatementInput = {
@@ -102,6 +111,8 @@ export function useCreateProgramExpense() {
recipient: resolveRecipient(it, session.username),
description: it.description,
planned_amount: formatToAsset(it.amount, symbol, precision),
payment_method_id: it.payment_method_id ?? undefined,
requisites: it.requisites || undefined,
}));
const result = await createProgramExpense({
@@ -43,7 +43,8 @@ BaseDialog(
BaseSelect(
v-model='item.recipient_type',
:options='recipientTypeOptions',
label='Тип получателя'
label='Тип получателя',
@update:model-value='item.payment_method_id = null'
)
.col-12.col-md-6
BaseSelect(
@@ -57,7 +58,8 @@ BaseDialog(
v-model='item.recipient_account',
label='Аккаунт пайщика-получателя',
placeholder='username',
required
required,
@update:model-value='item.payment_method_id = null'
)
.col-12.col-md-6(v-if='item.recipient_type === Zeus.ExpenseRecipientType.ORG')
BaseInput(
@@ -79,11 +81,26 @@ BaseDialog(
label='Описание',
placeholder='Назначение расхода'
)
.col-12(v-if='item.recipient_type === Zeus.ExpenseRecipientType.SELF')
PaymentMethodSelect(
v-model='item.payment_method_id',
:username='session.username',
hint='Реквизиты фиксируются на момент подачи — на них придёт выплата',
required
)
.col-12(v-if='item.recipient_type === Zeus.ExpenseRecipientType.MEMBER')
PaymentMethodSelect(
v-model='item.payment_method_id',
:username='item.recipient_account?.trim() || ""',
:hint='item.recipient_account?.trim() ? "Реквизиты фиксируются на момент подачи" : "Сначала укажите аккаунт пайщика-получателя"',
required
)
.col-12(v-if='item.recipient_type === Zeus.ExpenseRecipientType.ORG')
BaseInput(
v-model='item.requisites',
label='Реквизиты получателя',
placeholder='ИНН, р/с, БИК'
placeholder='ИНН, р/с, БИК',
required
)
template(#footer)
@@ -102,11 +119,13 @@ import { computed, reactive, ref } from 'vue';
import { Zeus } from '@coopenomics/sdk';
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { useSystemStore } from 'src/entities/System/model';
import { useSessionStore } from 'src/entities/Session';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { BaseButton } from 'src/shared/ui/base/BaseButton';
import { BaseInput } from 'src/shared/ui/base/BaseInput';
import { BaseSelect } from 'src/shared/ui/base/BaseSelect';
import { EmptyState } from 'src/shared/ui/base/EmptyState';
import { PaymentMethodSelect } from 'src/shared/ui/domain/PaymentMethodSelect';
import {
useCreateProgramExpense,
type ICreateProgramExpenseDraftItem,
@@ -119,6 +138,7 @@ const emit = defineEmits<{
}>();
const system = useSystemStore();
const session = useSessionStore();
const { submitProgramExpense } = useCreateProgramExpense();
const amountPlaceholder = computed(() => {
@@ -153,7 +173,9 @@ const canSubmit = computed(
i.amount.trim() &&
i.description.trim() &&
(i.recipient_type !== Zeus.ExpenseRecipientType.MEMBER || i.recipient_account?.trim()) &&
(i.recipient_type !== Zeus.ExpenseRecipientType.ORG || i.recipient_name?.trim()),
(i.recipient_type === Zeus.ExpenseRecipientType.ORG
? Boolean(i.recipient_name?.trim() && i.requisites?.trim())
: Boolean(i.payment_method_id)),
),
);
@@ -167,6 +189,7 @@ function addItem(): void {
recipient_name: '',
requisites: '',
recipient_account: '',
payment_method_id: null,
});
}
@@ -93,8 +93,7 @@ Queries.Expenses.ExpenseFiles.query
// Кассир + админ
Mutations.Expenses.PayExpenseItem.mutation
Mutations.Expenses.SubmitExpenseReport.mutation
Mutations.Expenses.AuthorizeExpenseReport.mutation
Mutations.Expenses.DeclineExpenseReport.mutation
// Авторизация/отклонение СЗ — решение совета (повестка), мутаций нет
Mutations.Expenses.ReturnExpenseItem.mutation
Mutations.Expenses.UploadExpenseFile.mutation
@@ -24,7 +24,3 @@ export { generateExpenseProposalDecisionDocument } from './mutations/generateExp
export type { IGenerateExpenseProposalDecisionDocumentInput } from './mutations/generateExpenseProposalDecisionDocument';
export { createExpenseProposal } from './mutations/createExpenseProposal';
export type { ICreateExpenseProposalInput } from './mutations/createExpenseProposal';
export { authorizeExpenseReport } from './mutations/authorizeExpenseReport';
export type { IAuthorizeExpenseReportInput } from './mutations/authorizeExpenseReport';
export { declineExpenseReport } from './mutations/declineExpenseReport';
export type { IDeclineExpenseReportInput } from './mutations/declineExpenseReport';
@@ -1,10 +0,0 @@
import { client } from 'src/shared/api/client';
import { Mutations } from '@coopenomics/sdk';
export type IAuthorizeExpenseReportInput = Mutations.Expense.AuthorizeExpenseReport.IInput['data'];
export async function authorizeExpenseReport(data: IAuthorizeExpenseReportInput) {
return client.Mutation(Mutations.Expense.AuthorizeExpenseReport.mutation, {
variables: { data },
});
}
@@ -1,10 +0,0 @@
import { client } from 'src/shared/api/client';
import { Mutations } from '@coopenomics/sdk';
export type IDeclineExpenseReportInput = Mutations.Expense.DeclineExpenseReport.IInput['data'];
export async function declineExpenseReport(data: IDeclineExpenseReportInput) {
return client.Mutation(Mutations.Expense.DeclineExpenseReport.mutation, {
variables: { data },
});
}
@@ -5,7 +5,6 @@ import {
ExpensesRegistryPage,
ExpenseDetailPage,
ExpensesAdminApprovePage,
ExpensesAdminAuthorizePage,
CashierPage,
MyAdvancesPage,
} from './pages';
@@ -56,19 +55,6 @@ export default async function (): Promise<IWorkspaceConfig[]> {
},
children: [],
},
{
path: 'admin/authorize',
name: 'expenses-admin-authorize',
component: markRaw(ExpensesAdminAuthorizePage),
meta: {
title: 'На авторизацию совета',
icon: 'how_to_vote',
roles: ['chairman'],
agreements: agreementsBase,
requiresAuth: true,
},
children: [],
},
{
path: 'cashier',
name: 'expenses-cashier',
@@ -5,10 +5,7 @@ import type { Cooperative } from 'cooptypes';
import { generateUniqueHash } from 'src/shared/lib/utils/generateUniqueHash';
import {
generateExpenseProposalStatementDocument,
generateExpenseProposalDecisionDocument,
createExpenseProposal,
authorizeExpenseReport,
declineExpenseReport,
} from '../api';
export interface ICreateProposalDraftItem {
@@ -29,17 +26,6 @@ export interface ICreateProposalDraft {
items: ICreateProposalDraftItem[];
}
export interface IAuthorizeProposalDraft {
proposal_hash: string;
description: string;
source_wallet: string;
items: ICreateProposalDraftItem[];
decision_kind: 'approve' | 'decline';
decision_reason?: string;
protocol_number?: string;
protocol_date?: string;
}
export function useExpenseProposalActions() {
const { info } = useSystemStore();
const session = useSessionStore();
@@ -103,66 +89,7 @@ export function useExpenseProposalActions() {
} as any);
}
async function authorizeProposal(draft: IAuthorizeProposalDraft) {
// Отклонение — без протокола решения: контрактный declexp принимает
// только причину. Документ 2011 генерируется лишь при утверждении.
if (draft.decision_kind === 'decline') {
return declineExpenseReport({
coopname: info.coopname,
proposal_hash: draft.proposal_hash,
reason: draft.decision_reason || 'Отклонено председателем',
} as any);
}
const itemsForDoc = draft.items.map((it, idx) => ({
number: String(idx + 1),
description: it.description,
amount: it.amount,
recipient_type: it.recipient_type,
mechanics: it.mechanics,
recipient_name: it.recipient_name ?? '',
requisites: it.requisites ?? '',
}));
const totalAmount = draft.items.reduce((sum, it) => sum + parseFloat(it.amount || '0'), 0);
const total_amount = `${totalAmount.toFixed(4)} RUB`;
const generated = await generateExpenseProposalDecisionDocument({
coopname: info.coopname,
username: session.username,
proposal_hash: draft.proposal_hash,
proposal: {
description: draft.description,
total_amount,
items_count: draft.items.length,
source_wallet: draft.source_wallet,
},
items: itemsForDoc,
decision: {
kind: draft.decision_kind,
reason: draft.decision_reason,
protocol_number: draft.protocol_number,
protocol_date: draft.protocol_date,
},
} as any);
const docKey = 'generateExpenseProposalDecisionDocument' as const;
const generatedDoc = (generated as any)[docKey];
const digital = new DigitalDocument(generatedDoc);
const signed = await digital.sign<Cooperative.Registry.ExpenseProposalDecision.Meta>(
session.username,
1,
);
return authorizeExpenseReport({
coopname: info.coopname,
proposal_hash: draft.proposal_hash,
decision: signed as any,
} as any);
}
return { submitProposal, authorizeProposal };
return { submitProposal };
}
function generateItemHash(proposal_hash: string, idx: number): string {
@@ -1,203 +0,0 @@
<template lang="pug">
BaseDialog(
:model-value='modelValue',
title='Авторизация служебной записки',
size='md',
@update:model-value='$emit("update:modelValue", $event)'
)
.auth-form(v-if='proposal')
.banner.banner--info.q-mb-md
q-icon.banner__icon(name='gavel', size='20px')
.banner__body
| Решение председателя/совета по СЗ {{ truncateHash(proposal.proposal_hash) }}.
| Документ-протокол подписывается вашим ключом и публикуется в блокчейне.
.field-group
.t-section.q-mb-sm Сводка
DataRow(label='Пайщик', :value='proposal.username || "—"')
DataRow(label='Назначение', :value='proposalPurpose || "—"')
DataRow(label='Сумма (план)', :value='proposal.total_planned || "—"')
DataRow(label='Сумма (факт)', :value='proposal.total_actual || "—"')
.field-group
.t-section.q-mb-sm Решение
q-option-group(
v-model='form.kind',
:options='kindOptions',
type='radio',
color='primary'
)
BaseInput(
v-if='form.kind === "decline"',
v-model='form.reason',
label='Причина отказа',
placeholder='Например: «Превышение лимита программы»',
required
)
.row.q-col-gutter-sm
.col-12.col-md-6
BaseInput(
v-model='form.protocol_number',
label='Номер протокола',
placeholder='Например: 15'
)
.col-12.col-md-6
BaseInput(
v-model='form.protocol_date',
label='Дата протокола',
placeholder='03.06.2026'
)
template(#footer)
.footer-bar
BaseButton(variant='ghost', @click='close') Отмена
BaseButton(
variant='primary',
:loading='submitting',
:disabled='!canSubmit',
@click='submit'
) {{ form.kind === 'approve' ? 'Утвердить' : 'Отклонить' }}
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { BaseButton } from 'src/shared/ui/base/BaseButton';
import { BaseInput } from 'src/shared/ui/base/BaseInput';
import { DataRow } from 'src/shared/ui/domain';
import {
getExpenseProposal,
type IExpenseProposalResult,
} from '../api';
import { useExpenseProposalActions, type ICreateProposalDraftItem } from '../model';
const props = defineProps<{
modelValue: boolean;
proposalHash: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'authorized'): void;
}>();
const { authorizeProposal } = useExpenseProposalActions();
const proposal = ref<NonNullable<IExpenseProposalResult> | null>(null);
const form = reactive({
kind: 'approve' as 'approve' | 'decline',
reason: '',
protocol_number: '',
protocol_date: '',
});
const submitting = ref(false);
const kindOptions = [
{ label: 'Утвердить', value: 'approve' },
{ label: 'Отклонить', value: 'decline' },
];
const canSubmit = computed(() => {
if (!proposal.value) return false;
if (form.kind === 'decline' && !form.reason.trim()) return false;
return true;
});
// Назначение СЗ = перечень позиций (в зеркале шасси нет отдельного описания —
// оно живёт в документе-заявлении 2010).
const proposalPurpose = computed(() =>
(proposal.value?.items ?? []).map((it: any) => it.description).filter(Boolean).join('; '),
);
watch(
() => [props.modelValue, props.proposalHash],
async ([open]) => {
if (!open || !props.proposalHash) return;
try {
const result = await getExpenseProposal({ proposal_hash: props.proposalHash });
proposal.value = result as NonNullable<IExpenseProposalResult> | null;
} catch (e) {
FailAlert(e);
}
},
{ immediate: true },
);
function truncateHash(hash?: string | null): string {
if (!hash) return '';
if (hash.length <= 16) return hash;
return `${hash.slice(0, 8)}${hash.slice(-6)}`;
}
function close(): void {
emit('update:modelValue', false);
}
async function submit(): Promise<void> {
if (!proposal.value) return;
try {
submitting.value = true;
const items: ICreateProposalDraftItem[] = (proposal.value.items ?? []).map((it: any, idx: number) => ({
number: String(idx + 1),
description: it.description ?? '',
amount: it.planned_amount ?? '',
recipient_type: (it.recipient_type ?? 'SELF') as 'SELF' | 'MEMBER' | 'ORG',
mechanics: (it.mechanics ?? 'ADVANCE') as 'ADVANCE' | 'DIRECT',
recipient_name: it.recipient ?? '',
requisites: '',
item_hash: it.item_hash,
recipient_account: it.recipient ?? '',
}));
await authorizeProposal({
proposal_hash: proposal.value.proposal_hash,
description: proposalPurpose.value || 'Расход',
source_wallet: proposal.value.source_wallet ?? '',
items,
decision_kind: form.kind,
decision_reason: form.reason || undefined,
protocol_number: form.protocol_number || undefined,
protocol_date: form.protocol_date || undefined,
});
SuccessAlert(
form.kind === 'approve'
? 'СЗ утверждена — решение подписано и отправлено в блокчейн'
: 'СЗ отклонена — решение подписано и отправлено в блокчейн',
);
emit('authorized');
close();
} catch (e) {
FailAlert(e);
} finally {
submitting.value = false;
}
}
</script>
<style lang="scss" scoped>
.auth-form {
display: flex;
flex-direction: column;
gap: var(--p-4);
}
.field-group {
display: flex;
flex-direction: column;
gap: var(--p-3);
}
.footer-bar {
display: flex;
justify-content: flex-end;
gap: var(--p-2);
padding: var(--p-3) var(--p-4);
border-top: 1px solid var(--p-line);
background: var(--p-canvas);
}
</style>
@@ -1,241 +0,0 @@
<template lang="pug">
.q-pa-md
PageHead(
eyebrow='Шасси расходов · Председатель',
title='На авторизацию отчётов',
subtitle='Очередь служебных записок со статусом «Отчёт подан» — ждут авторизации председателя'
)
.admin-queue
TableSkeleton(
v-if='loading && !filtered.length',
:columns='skeletonColumns',
:rows='6',
min-width='880px'
)
.table-wrap(v-else-if='filtered.length')
.table-scroll
table.table
thead
tr
th Пайщик
th.col-date Дата создания
th.col-num Сумма (план)
th.col-num Сумма (факт)
th Хеш
th.col-actions
tbody
tr.data-row(
v-for='row in filtered',
:key='row.proposal_hash',
@click='openDetail(row.proposal_hash)'
)
td.cell-name {{ row.username || '—' }}
td {{ formatCreatedAt(row.created_at) }}
td.col-num {{ row.total_planned || '—' }}
td.col-num {{ row.total_actual || '—' }}
td.col-hash.t-mono-sm {{ truncateHash(row.proposal_hash) }}
td.col-actions
.row-actions
BaseButton(
variant='primary',
size='sm',
icon='gavel',
@click.stop='openAuthorize(row.proposal_hash)'
) Авторизовать
BaseButton(
variant='ghost',
size='sm',
icon='chevron_right',
@click.stop='openDetail(row.proposal_hash)'
) Открыть
.table-foot
span {{ rangeLabel }}
BaseButton(
v-if='hasMore',
variant='ghost',
size='sm',
:loading='loading',
@click='loadMore'
) Загрузить ещё
EmptyState(
v-else,
title='Очередь пуста',
body='Отчёты по расходам не ожидают авторизации.'
)
template(#icon)
q-icon(name='task_alt', size='48px')
ExpenseProposalAuthorizeDialog(
v-model='authorizeOpen',
:proposal-hash='authorizingHash',
@authorized='onAuthorized'
)
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Zeus } from '@coopenomics/sdk';
import { PageHead } from 'src/shared/ui/layout';
import { BaseButton } from 'src/shared/ui/base/BaseButton';
import { EmptyState } from 'src/shared/ui/base/EmptyState';
import { TableSkeleton } from 'src/shared/ui/base/TableSkeleton';
import type { TableSkeletonColumn } from 'src/shared/ui/base/TableSkeleton';
import { FailAlert } from 'src/shared/api';
import {
getExpenseProposalsByCooperative,
type IExpenseProposalsByCooperativeResult,
} from '../api';
import ExpenseProposalAuthorizeDialog from './ExpenseProposalAuthorizeDialog.vue';
type IProposalRow = NonNullable<IExpenseProposalsByCooperativeResult['items']>[number];
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const items = ref<IProposalRow[]>([]);
const currentPage = ref(1);
const totalPages = ref(1);
const totalCount = ref(0);
const PAGE_LIMIT = 25;
const skeletonColumns = computed<TableSkeletonColumn[]>(() => [
{ label: 'Пайщик', cell: 'text' },
{ label: 'Дата создания', cell: 'text', cellWidth: '120px' },
{ label: 'Сумма (план)', class: 'col-num', cell: 'text', cellWidth: '110px' },
{ label: 'Сумма (факт)', class: 'col-num', cell: 'text', cellWidth: '110px' },
{ label: 'Хеш', cell: 'text', cellWidth: '160px' },
{ label: '', cell: 'text', cellWidth: '120px' },
]);
const filtered = computed(() =>
items.value.filter((p) => p.status === Zeus.ExpenseProposalStatus.REPORT_SUBMITTED),
);
const hasMore = computed(() => currentPage.value < totalPages.value);
const rangeLabel = computed(() => {
const shown = filtered.value.length;
return `${shown} в очереди · загружено ${items.value.length} из ${totalCount.value}`;
});
function formatCreatedAt(createdAt?: string | null): string {
if (!createdAt) return '—';
const date = new Date(createdAt);
if (Number.isNaN(date.getTime())) return createdAt;
return date.toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
}
function truncateHash(hash: string): string {
if (!hash) return '';
if (hash.length <= 16) return hash;
return `${hash.slice(0, 8)}${hash.slice(-6)}`;
}
async function loadPage(page = 1): Promise<void> {
const coopname = route.params.coopname as string;
if (!coopname) return;
try {
loading.value = true;
const result = await getExpenseProposalsByCooperative({
coopname,
options: {
page,
limit: PAGE_LIMIT,
sortBy: 'created_at',
sortOrder: 'ASC',
},
});
const incoming = (result.items ?? []) as IProposalRow[];
items.value = page === 1 ? incoming : [...items.value, ...incoming];
currentPage.value = result.currentPage ?? page;
totalPages.value = result.totalPages ?? 1;
totalCount.value = result.totalCount ?? items.value.length;
} catch (e) {
FailAlert(e);
} finally {
loading.value = false;
}
}
function loadMore(): void {
if (loading.value || !hasMore.value) return;
void loadPage(currentPage.value + 1);
}
function openDetail(hash: string): void {
void router.push({ name: 'expenses-detail', params: { hash } });
}
const authorizeOpen = ref(false);
const authorizingHash = ref('');
function openAuthorize(hash: string): void {
authorizingHash.value = hash;
authorizeOpen.value = true;
}
function onAuthorized(): void {
void loadPage(1);
}
onMounted(() => {
void loadPage(1);
});
</script>
<style lang="scss" scoped>
.admin-queue {
width: 100%;
}
.table-scroll {
overflow-x: auto;
}
.table {
table-layout: fixed;
min-width: 880px;
}
.col-date {
width: 132px;
white-space: nowrap;
}
.col-num {
width: 120px;
white-space: nowrap;
text-align: right;
}
.col-hash {
width: 160px;
color: var(--p-ink-2);
}
.col-actions {
width: 220px;
text-align: right;
}
.row-actions {
display: flex;
gap: var(--p-2);
justify-content: flex-end;
}
.cell-name {
overflow-wrap: anywhere;
}
.data-row {
cursor: pointer;
}
</style>
@@ -1,7 +1,6 @@
export { default as ExpensesRegistryPage } from './ExpensesRegistryPage.vue';
export { default as ExpenseDetailPage } from './ExpenseDetailPage.vue';
export { default as ExpensesAdminApprovePage } from './ExpensesAdminApprovePage.vue';
export { default as ExpensesAdminAuthorizePage } from './ExpensesAdminAuthorizePage.vue';
export { default as CashierPage } from './CashierPage.vue';
export { default as MyAdvancesPage } from './MyAdvancesPage.vue';
export { default as ExpenseProposalCreateDialog } from './ExpenseProposalCreateDialog.vue';
@@ -0,0 +1,82 @@
<template lang="pug">
BaseSelect(
:model-value='modelValue',
:options='options',
:label='label',
:placeholder='placeholder',
:hint='hint',
:error='error || loadError',
:disabled='disabled || loading',
:required='required',
@update:model-value='$emit("update:modelValue", $event === null ? null : String($event))'
)
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { client } from 'src/shared/api/client';
import { Queries } from '@coopenomics/sdk';
import { BaseSelect } from 'src/shared/ui/base/BaseSelect';
import type { BaseSelectOption } from 'src/shared/ui/base/BaseSelect';
import {
formatPaymentMethodShort,
type IPaymentMethodLike,
} from './formatPaymentMethod';
const props = withDefaults(
defineProps<{
modelValue?: string | null;
/** Чьи реквизиты выбираем — владелец платёжных методов. */
username: string;
label?: string;
placeholder?: string;
hint?: string;
error?: string;
disabled?: boolean;
required?: boolean;
}>(),
{
modelValue: null,
label: 'Реквизиты получателя',
placeholder: 'Выберите способ получения средств',
},
);
defineEmits<{ (e: 'update:modelValue', value: string | null): void }>();
const methods = ref<IPaymentMethodLike[]>([]);
const loading = ref(false);
const loadError = ref('');
const options = computed<BaseSelectOption[]>(() =>
methods.value.map((m) => ({
value: String(m.method_id),
label: formatPaymentMethodShort(m),
})),
);
watch(
() => props.username,
async (username) => {
methods.value = [];
loadError.value = '';
if (!username) return;
loading.value = true;
try {
const result = await client.Query(Queries.PaymentMethods.GetPaymentMethods.query, {
variables: { data: { username, limit: 100, page: 1 } },
});
methods.value = (result.getPaymentMethods.items as unknown as IPaymentMethodLike[]) ?? [];
if (!methods.value.length) {
loadError.value = 'У получателя нет сохранённых реквизитов';
}
} catch (e) {
console.error('Ошибка загрузки платёжных методов:', e);
loadError.value = 'Не удалось загрузить реквизиты получателя';
} finally {
loading.value = false;
}
},
{ immediate: true },
);
</script>
@@ -0,0 +1,22 @@
/**
* Сокращённое отображение реквизитов платёжного метода для UI: телефон СБП —
* со звёздочками, банковский счёт — банк + последние 4 цифры. Полные реквизиты
* в интерфейсе не показываются — они подставляются сервером в документы.
*/
export interface IPaymentMethodLike {
method_id: string | number;
method_type: string;
data: Record<string, unknown>;
}
export function formatPaymentMethodShort(method: IPaymentMethodLike): string {
if (method.method_type === 'sbp') {
const phone = String(method.data?.phone ?? '');
const masked = phone.length > 4 ? `${phone.slice(0, 2)}***${phone.slice(-2)}` : phone;
return `СБП (${masked})`;
}
const account = String(method.data?.account_number ?? '');
const bank = String(method.data?.bank_name ?? '');
const last4 = account.slice(-4);
return bank ? `${bank} (***${last4})` : `Банковский счёт ***${last4}`;
}
@@ -0,0 +1,2 @@
export { default as PaymentMethodSelect } from './PaymentMethodSelect.vue';
export { formatPaymentMethodShort, type IPaymentMethodLike } from './formatPaymentMethod';
@@ -14,6 +14,7 @@ export * from './FilterBar';
export * from './IdentityPanel';
export * from './NotificationCenter';
export * from './OtpInput';
export * from './PaymentMethodSelect';
export * from './PersonCard';
export * from './RailUserCard';
export * from './SignatureCard';
@@ -81,6 +81,21 @@ export interface InterExpensePaginatedResult<T> {
totalCount: number;
}
/** Строка СЗ для снимка реквизитов получателя (фиксация «куда платить» на момент создания). */
export interface InterExpenseRequisiteItemInput {
proposalHash: string;
itemHash: string;
/** Получатель: username пайщика (владелец платёжного метода) либо пусто для организации. */
recipient: string;
/** true — получатель-организация: реквизиты приходят строкой `requisites`. */
isOrganization: boolean;
/** Идентификатор платёжного метода получателя-пайщика. */
paymentMethodId?: string;
/** Реквизиты строкой (организации — ручной ввод из формы). */
requisites?: string;
}
export interface InterExpenseChassisPort {
/**
* Чтение proposal'а по хэшу. Возвращает null, если шасси не видит
@@ -105,4 +120,17 @@ export interface InterExpenseChassisPort {
ownerAction?: string,
pagination?: InterExpensePagination,
): Promise<InterExpensePaginatedResult<InterExpenseProposalRead>>;
/**
* Валидация реквизитов строк СЗ ДО постановки on-chain заявки: у каждой
* строки с получателем-пайщиком должен существовать указанный платёжный метод.
*/
validateRequisites(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise<void>;
/**
* Снимок реквизитов ПОСЛЕ успешной on-chain заявки: данные платёжного метода
* копируются в хранилище шасси на момент создания СЗ — последующее изменение
* метода пайщиком не меняет то, куда платить по уже поданной смете.
*/
snapshotRequisites(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise<void>;
}
+1
View File
@@ -69,4 +69,5 @@ export type {
InterExpensePaginatedResult,
InterExpenseProposalRead,
InterExpenseProposalStatus,
InterExpenseRequisiteItemInput,
} from './expense-chassis.port';
@@ -1,19 +0,0 @@
import { rawTransactionSelector } from '../../selectors'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'authorizeExpenseReport'
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'AuthorizeExpenseReportInput!') }, rawTransactionSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['AuthorizeExpenseReportInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -1,19 +0,0 @@
import { rawTransactionSelector } from '../../selectors'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'declineExpenseReport'
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'DeclineExpenseReportInput!') }, rawTransactionSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['DeclineExpenseReportInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -6,5 +6,3 @@ export * as ReportExpenseItem from './reportExpenseItem'
export * as ReturnExpenseItem from './returnExpenseItem'
export * as OverspendExpenseItem from './overspendExpenseItem'
export * as SubmitExpenseReport from './submitExpenseReport'
export * as AuthorizeExpenseReport from './authorizeExpenseReport'
export * as DeclineExpenseReport from './declineExpenseReport'
-23
View File
@@ -125,9 +125,6 @@ export const AllTypesProps: Record<string,any> = {
AuthorizeDecisionInput:{
document:"SignedDigitalDocumentInput"
},
AuthorizeExpenseReportInput:{
decision:"ExpenseProposalDecisionSignedDocumentInput"
},
BankAccountDetailsInput:{
},
@@ -396,9 +393,6 @@ export const AllTypesProps: Record<string,any> = {
},
DeclineDecisionInput:{
},
DeclineExpenseReportInput:{
},
DeclineRequestInput:{
@@ -472,15 +466,6 @@ export const AllTypesProps: Record<string,any> = {
},
ExpenseProposalDecisionItemInput:{
},
ExpenseProposalDecisionSignedDocumentInput:{
meta:"ExpenseProposalDecisionSignedMetaDocumentInput",
signatures:"SignatureInfoInput"
},
ExpenseProposalDecisionSignedMetaDocumentInput:{
decision:"ExpenseProposalDecisionBodyInput",
items:"ExpenseProposalDecisionItemInput",
proposal:"ExpenseProposalDecisionHeaderInput"
},
ExpenseProposalHeaderInput:{
@@ -727,9 +712,6 @@ export const AllTypesProps: Record<string,any> = {
authorizeDecision:{
data:"AuthorizeDecisionInput"
},
authorizeExpenseReport:{
data:"AuthorizeExpenseReportInput"
},
cancelRequest:{
data:"CancelRequestInput"
},
@@ -1073,9 +1055,6 @@ export const AllTypesProps: Record<string,any> = {
declineDecision:{
data:"DeclineDecisionInput"
},
declineExpenseReport:{
data:"DeclineExpenseReportInput"
},
declineRequest:{
data:"DeclineRequestInput"
},
@@ -3802,7 +3781,6 @@ export const ReturnTypes: Record<string,any> = {
addPaymentMethod:"PaymentMethod",
addTrustedAccount:"Branch",
authorizeDecision:"Transaction",
authorizeExpenseReport:"Transaction",
cancelRequest:"Transaction",
capitalAddAuthor:"CapitalProject",
capitalApproveCommit:"CapitalCommit",
@@ -3911,7 +3889,6 @@ export const ReturnTypes: Record<string,any> = {
deactivateWebPushSubscriptionById:"Boolean",
declineAgreement:"Transaction",
declineDecision:"Transaction",
declineExpenseReport:"Transaction",
declineRequest:"Transaction",
deleteAccount:"Boolean",
deleteBranch:"Boolean",
+36 -276
View File
@@ -1940,14 +1940,6 @@ export type ValueTypes = {
decision_id: number | Variable<any, string>,
/** Подписанный председателем документ утверждения решения */
document: ValueTypes["SignedDigitalDocumentInput"] | Variable<any, string>
};
["AuthorizeExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string | Variable<any, string>,
/** Подписанное решение совета (document2, registry 2011). */
decision: ValueTypes["ExpenseProposalDecisionSignedDocumentInput"] | Variable<any, string>,
/** Хеш сметы расхода. */
proposal_hash: string | Variable<any, string>
};
["AvailableReport"]: AliasType<{
deadline?:boolean | `@${string}`,
@@ -4924,14 +4916,6 @@ export type ValueTypes = {
coopname: string | Variable<any, string>,
/** Идентификатор решения */
decision_id: number | Variable<any, string>
};
["DeclineExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string | Variable<any, string>,
/** Хеш сметы расхода. */
proposal_hash: string | Variable<any, string>,
/** Причина отклонения СЗ-отчёта (до 1000 символов). */
reason: string | Variable<any, string>
};
["DeclineRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -5296,12 +5280,16 @@ export type ValueTypes = {
item_hash: string | Variable<any, string>,
/** Способ оплаты (ADVANCE / DIRECT). */
mechanics: ValueTypes["ExpenseMechanics"] | Variable<any, string>,
/** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */
payment_method_id?: string | undefined | null | Variable<any, string>,
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
planned_amount: string | Variable<any, string>,
/** Получатель (username / eosio::name организации). */
recipient: string | Variable<any, string>,
/** Тип получателя. */
recipient_type: ValueTypes["ExpenseRecipientType"] | Variable<any, string>
recipient_type: ValueTypes["ExpenseRecipientType"] | Variable<any, string>,
/** Реквизиты получателя-организации (вводятся вручную). */
requisites?: string | undefined | null | Variable<any, string>
};
/** Статус строки расхода. */
["ExpenseItemStatus"]:ExpenseItemStatus;
@@ -5404,52 +5392,6 @@ export type ValueTypes = {
recipient_name?: string | undefined | null | Variable<any, string>,
recipient_type: string | Variable<any, string>,
requisites?: string | undefined | null | Variable<any, string>
};
["ExpenseProposalDecisionSignedDocumentInput"]: {
/** Хэш содержимого документа */
doc_hash: string | Variable<any, string>,
/** Общий хэш (doc_hash + meta_hash) */
hash: string | Variable<any, string>,
/** Метаинформация решения по СЗ */
meta: ValueTypes["ExpenseProposalDecisionSignedMetaDocumentInput"] | Variable<any, string>,
/** Хэш мета-данных */
meta_hash: string | Variable<any, string>,
/** Вектор подписей */
signatures: Array<ValueTypes["SignatureInfoInput"]> | Variable<any, string>,
/** Версия стандарта документа */
version: string | Variable<any, string>
};
["ExpenseProposalDecisionSignedMetaDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num: number | Variable<any, string>,
/** Название кооператива, связанное с документом */
coopname: string | Variable<any, string>,
/** Дата и время создания документа */
created_at: string | Variable<any, string>,
/** Тело решения */
decision: ValueTypes["ExpenseProposalDecisionBodyInput"] | Variable<any, string>,
/** Имя генератора, использованного для создания документа */
generator: string | Variable<any, string>,
/** Позиции расхода */
items: Array<ValueTypes["ExpenseProposalDecisionItemInput"]> | Variable<any, string>,
/** Язык документа */
lang: string | Variable<any, string>,
/** Ссылки, связанные с документом */
links: Array<string> | Variable<any, string>,
/** Шапка СЗ */
proposal: ValueTypes["ExpenseProposalDecisionHeaderInput"] | Variable<any, string>,
/** Хеш сметы расхода */
proposal_hash: string | Variable<any, string>,
/** ID документа в реестре */
registry_id: number | Variable<any, string>,
/** Часовой пояс, в котором был создан документ */
timezone: string | Variable<any, string>,
/** Название документа */
title: string | Variable<any, string>,
/** Имя пользователя, создавшего документ */
username: string | Variable<any, string>,
/** Версия генератора, использованного для создания документа */
version: string | Variable<any, string>
};
["ExpenseProposalHeaderInput"]: {
/** Описание цели расходов */
@@ -5470,10 +5412,14 @@ export type ValueTypes = {
mechanics: string | Variable<any, string>,
/** Порядковый номер строки */
number: string | Variable<any, string>,
/** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */
payment_method_id?: string | undefined | null | Variable<any, string>,
/** Имя получателя */
recipient_name?: string | undefined | null | Variable<any, string>,
/** Тип получателя (SELF / MEMBER / ORG) */
recipient_type: string | Variable<any, string>,
/** Имя аккаунта получателя-пайщика (владелец реквизитов). */
recipient_username?: string | undefined | null | Variable<any, string>,
/** Реквизиты получателя */
requisites?: string | undefined | null | Variable<any, string>
};
@@ -6890,7 +6836,6 @@ addParticipant?: [{ data: ValueTypes["AddParticipantInput"] | Variable<any, stri
addPaymentMethod?: [{ data: ValueTypes["AddPaymentMethodInput"] | Variable<any, string>},ValueTypes["PaymentMethod"]],
addTrustedAccount?: [{ data: ValueTypes["AddTrustedAccountInput"] | Variable<any, string>},ValueTypes["Branch"]],
authorizeDecision?: [{ data: ValueTypes["AuthorizeDecisionInput"] | Variable<any, string>},ValueTypes["Transaction"]],
authorizeExpenseReport?: [{ data: ValueTypes["AuthorizeExpenseReportInput"] | Variable<any, string>},ValueTypes["Transaction"]],
cancelRequest?: [{ data: ValueTypes["CancelRequestInput"] | Variable<any, string>},ValueTypes["Transaction"]],
capitalAddAuthor?: [{ data: ValueTypes["AddAuthorInput"] | Variable<any, string>},ValueTypes["CapitalProject"]],
capitalApproveCommit?: [{ data: ValueTypes["CommitApproveInput"] | Variable<any, string>},ValueTypes["CapitalCommit"]],
@@ -7002,7 +6947,6 @@ createWithdraw?: [{ data: ValueTypes["CreateWithdrawInput"] | Variable<any, stri
deactivateWebPushSubscriptionById?: [{ data: ValueTypes["DeactivateSubscriptionInput"] | Variable<any, string>},boolean | `@${string}`],
declineAgreement?: [{ data: ValueTypes["DeclineAgreementInput"] | Variable<any, string>},ValueTypes["Transaction"]],
declineDecision?: [{ data: ValueTypes["DeclineDecisionInput"] | Variable<any, string>},ValueTypes["Transaction"]],
declineExpenseReport?: [{ data: ValueTypes["DeclineExpenseReportInput"] | Variable<any, string>},ValueTypes["Transaction"]],
declineRequest?: [{ data: ValueTypes["DeclineRequestInput"] | Variable<any, string>},ValueTypes["Transaction"]],
deleteAccount?: [{ data: ValueTypes["DeleteAccountInput"] | Variable<any, string>},boolean | `@${string}`],
deleteBranch?: [{ data: ValueTypes["DeleteBranchInput"] | Variable<any, string>},boolean | `@${string}`],
@@ -11111,14 +11055,6 @@ export type ResolverInputTypes = {
decision_id: number,
/** Подписанный председателем документ утверждения решения */
document: ResolverInputTypes["SignedDigitalDocumentInput"]
};
["AuthorizeExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Подписанное решение совета (document2, registry 2011). */
decision: ResolverInputTypes["ExpenseProposalDecisionSignedDocumentInput"],
/** Хеш сметы расхода. */
proposal_hash: string
};
["AvailableReport"]: AliasType<{
deadline?:boolean | `@${string}`,
@@ -14018,14 +13954,6 @@ export type ResolverInputTypes = {
coopname: string,
/** Идентификатор решения */
decision_id: number
};
["DeclineExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Хеш сметы расхода. */
proposal_hash: string,
/** Причина отклонения СЗ-отчёта (до 1000 символов). */
reason: string
};
["DeclineRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -14378,12 +14306,16 @@ export type ResolverInputTypes = {
item_hash: string,
/** Способ оплаты (ADVANCE / DIRECT). */
mechanics: ResolverInputTypes["ExpenseMechanics"],
/** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */
payment_method_id?: string | undefined | null,
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
planned_amount: string,
/** Получатель (username / eosio::name организации). */
recipient: string,
/** Тип получателя. */
recipient_type: ResolverInputTypes["ExpenseRecipientType"]
recipient_type: ResolverInputTypes["ExpenseRecipientType"],
/** Реквизиты получателя-организации (вводятся вручную). */
requisites?: string | undefined | null
};
/** Статус строки расхода. */
["ExpenseItemStatus"]:ExpenseItemStatus;
@@ -14485,52 +14417,6 @@ export type ResolverInputTypes = {
recipient_name?: string | undefined | null,
recipient_type: string,
requisites?: string | undefined | null
};
["ExpenseProposalDecisionSignedDocumentInput"]: {
/** Хэш содержимого документа */
doc_hash: string,
/** Общий хэш (doc_hash + meta_hash) */
hash: string,
/** Метаинформация решения по СЗ */
meta: ResolverInputTypes["ExpenseProposalDecisionSignedMetaDocumentInput"],
/** Хэш мета-данных */
meta_hash: string,
/** Вектор подписей */
signatures: Array<ResolverInputTypes["SignatureInfoInput"]>,
/** Версия стандарта документа */
version: string
};
["ExpenseProposalDecisionSignedMetaDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num: number,
/** Название кооператива, связанное с документом */
coopname: string,
/** Дата и время создания документа */
created_at: string,
/** Тело решения */
decision: ResolverInputTypes["ExpenseProposalDecisionBodyInput"],
/** Имя генератора, использованного для создания документа */
generator: string,
/** Позиции расхода */
items: Array<ResolverInputTypes["ExpenseProposalDecisionItemInput"]>,
/** Язык документа */
lang: string,
/** Ссылки, связанные с документом */
links: Array<string>,
/** Шапка СЗ */
proposal: ResolverInputTypes["ExpenseProposalDecisionHeaderInput"],
/** Хеш сметы расхода */
proposal_hash: string,
/** ID документа в реестре */
registry_id: number,
/** Часовой пояс, в котором был создан документ */
timezone: string,
/** Название документа */
title: string,
/** Имя пользователя, создавшего документ */
username: string,
/** Версия генератора, использованного для создания документа */
version: string
};
["ExpenseProposalHeaderInput"]: {
/** Описание цели расходов */
@@ -14551,10 +14437,14 @@ export type ResolverInputTypes = {
mechanics: string,
/** Порядковый номер строки */
number: string,
/** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */
payment_method_id?: string | undefined | null,
/** Имя получателя */
recipient_name?: string | undefined | null,
/** Тип получателя (SELF / MEMBER / ORG) */
recipient_type: string,
/** Имя аккаунта получателя-пайщика (владелец реквизитов). */
recipient_username?: string | undefined | null,
/** Реквизиты получателя */
requisites?: string | undefined | null
};
@@ -15932,7 +15822,6 @@ addParticipant?: [{ data: ResolverInputTypes["AddParticipantInput"]},ResolverInp
addPaymentMethod?: [{ data: ResolverInputTypes["AddPaymentMethodInput"]},ResolverInputTypes["PaymentMethod"]],
addTrustedAccount?: [{ data: ResolverInputTypes["AddTrustedAccountInput"]},ResolverInputTypes["Branch"]],
authorizeDecision?: [{ data: ResolverInputTypes["AuthorizeDecisionInput"]},ResolverInputTypes["Transaction"]],
authorizeExpenseReport?: [{ data: ResolverInputTypes["AuthorizeExpenseReportInput"]},ResolverInputTypes["Transaction"]],
cancelRequest?: [{ data: ResolverInputTypes["CancelRequestInput"]},ResolverInputTypes["Transaction"]],
capitalAddAuthor?: [{ data: ResolverInputTypes["AddAuthorInput"]},ResolverInputTypes["CapitalProject"]],
capitalApproveCommit?: [{ data: ResolverInputTypes["CommitApproveInput"]},ResolverInputTypes["CapitalCommit"]],
@@ -16044,7 +15933,6 @@ createWithdraw?: [{ data: ResolverInputTypes["CreateWithdrawInput"]},ResolverInp
deactivateWebPushSubscriptionById?: [{ data: ResolverInputTypes["DeactivateSubscriptionInput"]},boolean | `@${string}`],
declineAgreement?: [{ data: ResolverInputTypes["DeclineAgreementInput"]},ResolverInputTypes["Transaction"]],
declineDecision?: [{ data: ResolverInputTypes["DeclineDecisionInput"]},ResolverInputTypes["Transaction"]],
declineExpenseReport?: [{ data: ResolverInputTypes["DeclineExpenseReportInput"]},ResolverInputTypes["Transaction"]],
declineRequest?: [{ data: ResolverInputTypes["DeclineRequestInput"]},ResolverInputTypes["Transaction"]],
deleteAccount?: [{ data: ResolverInputTypes["DeleteAccountInput"]},boolean | `@${string}`],
deleteBranch?: [{ data: ResolverInputTypes["DeleteBranchInput"]},boolean | `@${string}`],
@@ -20033,14 +19921,6 @@ export type ModelTypes = {
decision_id: number,
/** Подписанный председателем документ утверждения решения */
document: ModelTypes["SignedDigitalDocumentInput"]
};
["AuthorizeExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Подписанное решение совета (document2, registry 2011). */
decision: ModelTypes["ExpenseProposalDecisionSignedDocumentInput"],
/** Хеш сметы расхода. */
proposal_hash: string
};
["AvailableReport"]: {
deadline: string,
@@ -22855,14 +22735,6 @@ export type ModelTypes = {
coopname: string,
/** Идентификатор решения */
decision_id: number
};
["DeclineExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Хеш сметы расхода. */
proposal_hash: string,
/** Причина отклонения СЗ-отчёта (до 1000 символов). */
reason: string
};
["DeclineRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -23201,12 +23073,16 @@ export type ModelTypes = {
item_hash: string,
/** Способ оплаты (ADVANCE / DIRECT). */
mechanics: ModelTypes["ExpenseMechanics"],
/** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */
payment_method_id?: string | undefined | null,
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
planned_amount: string,
/** Получатель (username / eosio::name организации). */
recipient: string,
/** Тип получателя. */
recipient_type: ModelTypes["ExpenseRecipientType"]
recipient_type: ModelTypes["ExpenseRecipientType"],
/** Реквизиты получателя-организации (вводятся вручную). */
requisites?: string | undefined | null
};
["ExpenseItemStatus"]:ExpenseItemStatus;
["ExpenseMechanics"]:ExpenseMechanics;
@@ -23305,52 +23181,6 @@ export type ModelTypes = {
recipient_name?: string | undefined | null,
recipient_type: string,
requisites?: string | undefined | null
};
["ExpenseProposalDecisionSignedDocumentInput"]: {
/** Хэш содержимого документа */
doc_hash: string,
/** Общий хэш (doc_hash + meta_hash) */
hash: string,
/** Метаинформация решения по СЗ */
meta: ModelTypes["ExpenseProposalDecisionSignedMetaDocumentInput"],
/** Хэш мета-данных */
meta_hash: string,
/** Вектор подписей */
signatures: Array<ModelTypes["SignatureInfoInput"]>,
/** Версия стандарта документа */
version: string
};
["ExpenseProposalDecisionSignedMetaDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num: number,
/** Название кооператива, связанное с документом */
coopname: string,
/** Дата и время создания документа */
created_at: string,
/** Тело решения */
decision: ModelTypes["ExpenseProposalDecisionBodyInput"],
/** Имя генератора, использованного для создания документа */
generator: string,
/** Позиции расхода */
items: Array<ModelTypes["ExpenseProposalDecisionItemInput"]>,
/** Язык документа */
lang: string,
/** Ссылки, связанные с документом */
links: Array<string>,
/** Шапка СЗ */
proposal: ModelTypes["ExpenseProposalDecisionHeaderInput"],
/** Хеш сметы расхода */
proposal_hash: string,
/** ID документа в реестре */
registry_id: number,
/** Часовой пояс, в котором был создан документ */
timezone: string,
/** Название документа */
title: string,
/** Имя пользователя, создавшего документ */
username: string,
/** Версия генератора, использованного для создания документа */
version: string
};
["ExpenseProposalHeaderInput"]: {
/** Описание цели расходов */
@@ -23371,10 +23201,14 @@ export type ModelTypes = {
mechanics: string,
/** Порядковый номер строки */
number: string,
/** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */
payment_method_id?: string | undefined | null,
/** Имя получателя */
recipient_name?: string | undefined | null,
/** Тип получателя (SELF / MEMBER / ORG) */
recipient_type: string,
/** Имя аккаунта получателя-пайщика (владелец реквизитов). */
recipient_username?: string | undefined | null,
/** Реквизиты получателя */
requisites?: string | undefined | null
};
@@ -24713,10 +24547,6 @@ export type ModelTypes = {
Требуемые роли: chairman. */
authorizeDecision: ModelTypes["Transaction"],
/** Утвердить СЗ-отчёт (закрытие сметы). Триггерит капитализацию РИД в Благоросте.
Требуемые роли: chairman. */
authorizeExpenseReport: ModelTypes["Transaction"],
/** Отменить заявку */
cancelRequest: ModelTypes["Transaction"],
/** Добавление автора проекта в CAPITAL контракте
@@ -25133,10 +24963,6 @@ export type ModelTypes = {
Требуемые роли: chairman. */
declineDecision: ModelTypes["Transaction"],
/** Отклонить СЗ-отчёт с указанием причины. Смета переходит в DECLINED.
Требуемые роли: chairman. */
declineExpenseReport: ModelTypes["Transaction"],
/** Отклонить заявку */
declineRequest: ModelTypes["Transaction"],
/** Удалить аккаунт пайщика из системы учёта провайдера. Доступно только для незавершённых регистрационных статусов (черновик, неоплачен/отклонён). Активный, заблокированный и любой зарегистрированный в блокчейне аккаунт удалить нельзя. Используется для очистки реестра и освобождения e-mail под перерегистрацию.
@@ -29461,14 +29287,6 @@ export type GraphQLTypes = {
decision_id: number,
/** Подписанный председателем документ утверждения решения */
document: GraphQLTypes["SignedDigitalDocumentInput"]
};
["AuthorizeExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Подписанное решение совета (document2, registry 2011). */
decision: GraphQLTypes["ExpenseProposalDecisionSignedDocumentInput"],
/** Хеш сметы расхода. */
proposal_hash: string
};
["AvailableReport"]: {
__typename: "AvailableReport",
@@ -32445,14 +32263,6 @@ export type GraphQLTypes = {
coopname: string,
/** Идентификатор решения */
decision_id: number
};
["DeclineExpenseReportInput"]: {
/** Имя кооператива. */
coopname: string,
/** Хеш сметы расхода. */
proposal_hash: string,
/** Причина отклонения СЗ-отчёта (до 1000 символов). */
reason: string
};
["DeclineRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -32817,12 +32627,16 @@ export type GraphQLTypes = {
item_hash: string,
/** Способ оплаты (ADVANCE / DIRECT). */
mechanics: GraphQLTypes["ExpenseMechanics"],
/** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */
payment_method_id?: string | undefined | null,
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
planned_amount: string,
/** Получатель (username / eosio::name организации). */
recipient: string,
/** Тип получателя. */
recipient_type: GraphQLTypes["ExpenseRecipientType"]
recipient_type: GraphQLTypes["ExpenseRecipientType"],
/** Реквизиты получателя-организации (вводятся вручную). */
requisites?: string | undefined | null
};
/** Статус строки расхода. */
["ExpenseItemStatus"]: ExpenseItemStatus;
@@ -32925,52 +32739,6 @@ export type GraphQLTypes = {
recipient_name?: string | undefined | null,
recipient_type: string,
requisites?: string | undefined | null
};
["ExpenseProposalDecisionSignedDocumentInput"]: {
/** Хэш содержимого документа */
doc_hash: string,
/** Общий хэш (doc_hash + meta_hash) */
hash: string,
/** Метаинформация решения по СЗ */
meta: GraphQLTypes["ExpenseProposalDecisionSignedMetaDocumentInput"],
/** Хэш мета-данных */
meta_hash: string,
/** Вектор подписей */
signatures: Array<GraphQLTypes["SignatureInfoInput"]>,
/** Версия стандарта документа */
version: string
};
["ExpenseProposalDecisionSignedMetaDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num: number,
/** Название кооператива, связанное с документом */
coopname: string,
/** Дата и время создания документа */
created_at: string,
/** Тело решения */
decision: GraphQLTypes["ExpenseProposalDecisionBodyInput"],
/** Имя генератора, использованного для создания документа */
generator: string,
/** Позиции расхода */
items: Array<GraphQLTypes["ExpenseProposalDecisionItemInput"]>,
/** Язык документа */
lang: string,
/** Ссылки, связанные с документом */
links: Array<string>,
/** Шапка СЗ */
proposal: GraphQLTypes["ExpenseProposalDecisionHeaderInput"],
/** Хеш сметы расхода */
proposal_hash: string,
/** ID документа в реестре */
registry_id: number,
/** Часовой пояс, в котором был создан документ */
timezone: string,
/** Название документа */
title: string,
/** Имя пользователя, создавшего документ */
username: string,
/** Версия генератора, использованного для создания документа */
version: string
};
["ExpenseProposalHeaderInput"]: {
/** Описание цели расходов */
@@ -32991,10 +32759,14 @@ export type GraphQLTypes = {
mechanics: string,
/** Порядковый номер строки */
number: string,
/** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */
payment_method_id?: string | undefined | null,
/** Имя получателя */
recipient_name?: string | undefined | null,
/** Тип получателя (SELF / MEMBER / ORG) */
recipient_type: string,
/** Имя аккаунта получателя-пайщика (владелец реквизитов). */
recipient_username?: string | undefined | null,
/** Реквизиты получателя */
requisites?: string | undefined | null
};
@@ -34423,10 +34195,6 @@ export type GraphQLTypes = {
Требуемые роли: chairman. */
authorizeDecision: GraphQLTypes["Transaction"],
/** Утвердить СЗ-отчёт (закрытие сметы). Триггерит капитализацию РИД в Благоросте.
Требуемые роли: chairman. */
authorizeExpenseReport: GraphQLTypes["Transaction"],
/** Отменить заявку */
cancelRequest: GraphQLTypes["Transaction"],
/** Добавление автора проекта в CAPITAL контракте
@@ -34843,10 +34611,6 @@ export type GraphQLTypes = {
Требуемые роли: chairman. */
declineDecision: GraphQLTypes["Transaction"],
/** Отклонить СЗ-отчёт с указанием причины. Смета переходит в DECLINED.
Требуемые роли: chairman. */
declineExpenseReport: GraphQLTypes["Transaction"],
/** Отклонить заявку */
declineRequest: GraphQLTypes["Transaction"],
/** Удалить аккаунт пайщика из системы учёта провайдера. Доступно только для незавершённых регистрационных статусов (черновик, неоплачен/отклонён). Активный, заблокированный и любой зарегистрированный в блокчейне аккаунт удалить нельзя. Используется для очистки реестра и освобождения e-mail под перерегистрацию.
@@ -39065,7 +38829,6 @@ type ZEUS_VARIABLES = {
["AssetContributionStatementSignedDocumentInput"]: ValueTypes["AssetContributionStatementSignedDocumentInput"];
["AssetContributionStatementSignedMetaDocumentInput"]: ValueTypes["AssetContributionStatementSignedMetaDocumentInput"];
["AuthorizeDecisionInput"]: ValueTypes["AuthorizeDecisionInput"];
["AuthorizeExpenseReportInput"]: ValueTypes["AuthorizeExpenseReportInput"];
["BankAccountDetailsInput"]: ValueTypes["BankAccountDetailsInput"];
["BankAccountInput"]: ValueTypes["BankAccountInput"];
["BuhotchSignerType"]: ValueTypes["BuhotchSignerType"];
@@ -39155,7 +38918,6 @@ type ZEUS_VARIABLES = {
["DeclineAgreementInput"]: ValueTypes["DeclineAgreementInput"];
["DeclineApproveInput"]: ValueTypes["DeclineApproveInput"];
["DeclineDecisionInput"]: ValueTypes["DeclineDecisionInput"];
["DeclineExpenseReportInput"]: ValueTypes["DeclineExpenseReportInput"];
["DeclineRequestInput"]: ValueTypes["DeclineRequestInput"];
["DeleteAccountInput"]: ValueTypes["DeleteAccountInput"];
["DeleteBranchInput"]: ValueTypes["DeleteBranchInput"];
@@ -39182,8 +38944,6 @@ type ZEUS_VARIABLES = {
["ExpenseProposalDecisionGenerateDocumentInput"]: ValueTypes["ExpenseProposalDecisionGenerateDocumentInput"];
["ExpenseProposalDecisionHeaderInput"]: ValueTypes["ExpenseProposalDecisionHeaderInput"];
["ExpenseProposalDecisionItemInput"]: ValueTypes["ExpenseProposalDecisionItemInput"];
["ExpenseProposalDecisionSignedDocumentInput"]: ValueTypes["ExpenseProposalDecisionSignedDocumentInput"];
["ExpenseProposalDecisionSignedMetaDocumentInput"]: ValueTypes["ExpenseProposalDecisionSignedMetaDocumentInput"];
["ExpenseProposalHeaderInput"]: ValueTypes["ExpenseProposalHeaderInput"];
["ExpenseProposalItemInput"]: ValueTypes["ExpenseProposalItemInput"];
["ExpenseProposalStatementGenerateDocumentInput"]: ValueTypes["ExpenseProposalStatementGenerateDocumentInput"];