feat(gateway): ядровый «чек об оплате» исходящих платежей по payment_hash
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Successful in 14m26s

Культура денег: входящие — подтверждением, исходящие — чеком. Единый механизм
на уровне ядра (gateway), привязка по payment_hash — один на все исходящие
(возврат паевого/withdrawal/registration-refund/аванс расхода), переиспользуется
расширениями. Контроль мягкий: статус не трогаем, но зеркалим proof_count →
реестр рисует «чек приложен / не приложен».

Backend (core gateway):
- таблица payment_files + бакет gateway:files (@UseBucket), PaymentFilesService
  (upload/read-url/list/delete + зеркалирование proof_count в платёж)
- enum PaymentFileKind (PAYMENT_PROOF), DTO, резолвер uploadPaymentProof /
  paymentProofs / paymentFile; провайдеры в typeorm.module + gateway.module

SDK: selectors/mutations/queries gateway + zeus regen

Desktop:
- features/Payment/AttachPaymentProof — панель чека по payment-hash
- реестр: панель + индикатор «чек приложен» для ЛЮБОГО исходящего (PAID/COMPLETED)
- конвергенция expense: чек ушёл в ядро, AttachExpenseProof оставляет только
  закрывающие документы (DIRECT)

Терминология: «чек об оплате» (не «платёжка»/«первичка»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
coopops
2026-06-16 09:17:09 +00:00
parent 39433753c2
commit 7fdfd8385a
28 changed files with 1424 additions and 104 deletions
@@ -0,0 +1,65 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { PaymentFileKind } from '~/domain/gateway/enums/payment-file-kind.enum';
import type { IPaymentFileDatabaseData } from '~/domain/gateway/interfaces/payment-file-database.interface';
/**
* Output DTO записи о файле платежа (чеке об оплате) в MinIO-бакете.
*
* `read_url` — короткоживущий HMAC-signed URL (TTL = `defaultUrlTtlSeconds` бакета),
* выдаётся после проверки прав; держатель URL может скачать файл до истечения TTL.
*/
@ObjectType('PaymentFile', { description: 'Запись о файле, приложенном к платежу (чек об оплате).' })
export class PaymentFileOutputDTO {
@Field(() => Int, { description: 'Внутренний ID записи.' })
id!: number;
@Field(() => String, { description: 'Имя кооператива (scope).' })
coopname!: string;
@Field(() => String, { description: 'Хеш платежа.' })
payment_hash!: string;
@Field(() => PaymentFileKind, { description: 'Назначение файла.' })
kind!: PaymentFileKind;
@Field(() => String, { description: 'SHA-256 содержимого, hex-lowercase.' })
checksum_sha256!: string;
@Field(() => String, { description: 'MIME-тип содержимого.' })
mime_type!: string;
@Field(() => Int, { description: 'Размер файла в байтах.' })
size_bytes!: number;
@Field(() => String, { description: 'MinIO-ключ внутри бакета.' })
storage_key!: string;
@Field(() => String, { nullable: true, description: 'Оригинальное имя загруженного файла.' })
original_filename?: string;
@Field(() => String, { description: 'Кто загрузил (username).' })
uploaded_by_username!: string;
@Field(() => Date, { description: 'Когда загружено.' })
uploaded_at!: Date;
@Field(() => String, { nullable: true, description: 'Короткоживущий URL на скачивание (HMAC-signed).' })
read_url?: string;
static fromDomain(data: IPaymentFileDatabaseData, readUrl?: string): PaymentFileOutputDTO {
const dto = new PaymentFileOutputDTO();
dto.id = data.id ?? 0;
dto.coopname = data.coopname;
dto.payment_hash = data.payment_hash;
dto.kind = data.kind;
dto.checksum_sha256 = data.checksum_sha256;
dto.mime_type = data.mime_type;
dto.size_bytes = data.size_bytes;
dto.storage_key = data.storage_key;
dto.original_filename = data.original_filename ?? undefined;
dto.uploaded_by_username = data.uploaded_by_username;
dto.uploaded_at = data.uploaded_at;
dto.read_url = readUrl;
return dto;
}
}
@@ -0,0 +1,47 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsBase64, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Matches, Max, Min } from 'class-validator';
const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'application/pdf'] as const;
/**
* Input для `uploadPaymentProof` — приложить чек об оплате к платежу.
*
* Привязка по `payment_hash`. MVP-передача: base64-содержимое внутри mutation
* (до 20 МБ — хватает для фото/PDF чека). Вид файла всегда PAYMENT_PROOF
* (чек об оплате) — поэтому в input не выносится.
*/
@InputType('UploadPaymentProofInput')
export class UploadPaymentProofInputDTO {
@Field(() => String, { description: 'Имя кооператива.' })
@IsNotEmpty()
@IsString()
coopname!: string;
@Field(() => String, { description: 'Хеш платежа, к которому прикладывается чек.' })
@IsNotEmpty()
@IsString()
payment_hash!: string;
@Field(() => String, { description: 'MIME-тип содержимого.' })
@IsIn([...ALLOWED_MIME], { message: 'mime_type должен быть одним из allowed.' })
mime_type!: string;
@Field(() => String, { nullable: true, description: 'Оригинальное имя файла — для отображения и поиска.' })
@IsOptional()
@IsString()
original_filename?: string;
@Field(() => Number, { description: 'Размер файла в байтах (для серверной валидации).' })
@IsInt()
@Min(1)
@Max(20 * 1024 * 1024)
size_bytes!: number;
@Field(() => String, { description: 'SHA-256 содержимого, hex-lowercase (64 hex-символа).' })
@Matches(/^[a-f0-9]{64}$/, { message: 'checksum_sha256 должен быть 64-символьным lowercase hex.' })
checksum_sha256!: string;
@Field(() => String, { description: 'Содержимое файла, base64 без префикса data:.' })
@IsBase64()
content_base64!: string;
}
@@ -1,6 +1,8 @@
import { Module, forwardRef } from '@nestjs/common';
import { GatewayResolver } from './resolvers/gateway.resolver';
import { PaymentFilesResolver } from './resolvers/payment-files.resolver';
import { GatewayService } from './services/gateway.service';
import { PaymentFilesService } from './services/payment-files.service';
import { PaymentNotificationService } from './services/payment-notification.service';
import { WithdrawAuthorizationListener } from './services/withdraw-authorization.listener';
import { PaymentController } from './controllers/payment.controller';
@@ -13,6 +15,7 @@ import { SystemModule } from '~/application/system/system.module';
import { GatewayInfrastructureModule } from '~/infrastructure/gateway/gateway-infrastructure.module';
import { UserInfrastructureModule } from '~/infrastructure/user/user-infrastructure.module';
import { RedisModule } from '~/infrastructure/redis/redis.module';
import { FileStorageInfrastructureModule } from '~/infrastructure/file-storage';
@Module({
imports: [
@@ -24,16 +27,20 @@ import { RedisModule } from '~/infrastructure/redis/redis.module';
AccountInfrastructureModule,
SystemModule,
RedisModule,
// Чек об оплате (gateway:files) — ядровый механизм, бакет по @UseBucket.
FileStorageInfrastructureModule.forFeature([PaymentFilesService]),
],
controllers: [PaymentController],
providers: [
GatewayResolver,
PaymentFilesResolver,
GatewayService,
PaymentFilesService,
PaymentNotificationService,
GatewayInteractor,
GatewayNotificationHandler,
WithdrawAuthorizationListener,
],
exports: [GatewayService, PaymentNotificationService, GatewayInteractor],
exports: [GatewayService, PaymentNotificationService, GatewayInteractor, PaymentFilesService],
})
export class GatewayModule {}
@@ -0,0 +1,60 @@
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
import { RolesGuard } from '~/application/auth/guards/roles.guard';
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
import { PaymentFilesService } from '../services/payment-files.service';
import { UploadPaymentProofInputDTO } from '../dto/upload-payment-proof.input';
import { PaymentFileOutputDTO } from '../dto/payment-file.output';
/**
* GraphQL для чеков об оплате платежей (ядро gateway).
*
* - Прикладывает чек кассир (chairman/member) — подтверждение исполненной оплаты.
* - Списки и read-URL видят те же роли и пайщик (свой чек). ACL уточнится после E2E.
*/
@Resolver(() => PaymentFileOutputDTO)
export class PaymentFilesResolver {
constructor(private readonly paymentFiles: PaymentFilesService) {}
@Mutation(() => PaymentFileOutputDTO, {
name: 'uploadPaymentProof',
description: 'Приложить чек об оплате к платежу (бакет gateway:files).',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async uploadPaymentProof(
@Args('data', { type: () => UploadPaymentProofInputDTO }) data: UploadPaymentProofInputDTO,
@CurrentUser() user: MonoAccountDomainInterface
): Promise<PaymentFileOutputDTO> {
const { data: saved, readUrl } = await this.paymentFiles.uploadProof(data, user.username);
return PaymentFileOutputDTO.fromDomain(saved, readUrl);
}
@Query(() => PaymentFileOutputDTO, {
name: 'paymentFile',
description: 'Получить запись о файле платежа + свежий короткоживущий read-URL.',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member', 'user'])
async getPaymentFile(@Args('id', { type: () => Int }) id: number): Promise<PaymentFileOutputDTO> {
const { data, readUrl } = await this.paymentFiles.getReadUrl(id);
return PaymentFileOutputDTO.fromDomain(data, readUrl);
}
@Query(() => [PaymentFileOutputDTO], {
name: 'paymentProofs',
description: 'Список чеков об оплате платежа (без read-URL — запрос отдельно по id).',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member', 'user'])
async listByPayment(
@Args('coopname', { type: () => String }) coopname: string,
@Args('payment_hash', { type: () => String }) paymentHash: string
): Promise<PaymentFileOutputDTO[]> {
const items = await this.paymentFiles.listByPayment(coopname, paymentHash);
return items.map((d) => PaymentFileOutputDTO.fromDomain(d));
}
}
@@ -0,0 +1,151 @@
import {
BadRequestException,
ConflictException,
Inject,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { createHash } from 'crypto';
import type { InterFileStorageBucket } from '@coopenomics/inter';
import { InjectBucket, UseBucket } from '~/infrastructure/file-storage';
import { PAYMENT_REPOSITORY, type PaymentRepository } from '~/domain/gateway/repositories/payment.repository';
import {
PAYMENT_FILE_REPOSITORY,
type PaymentFileRepository,
} from '~/domain/gateway/repositories/payment-file.repository';
import type { IPaymentFileDatabaseData } from '~/domain/gateway/interfaces/payment-file-database.interface';
import { PaymentFileKind } from '~/domain/gateway/enums/payment-file-kind.enum';
import { GATEWAY_BUCKET } from '~/domain/gateway/constants/gateway-bucket';
import { UploadPaymentProofInputDTO } from '../dto/upload-payment-proof.input';
const EXTENSION_BY_MIME: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/webp': 'webp',
'image/heic': 'heic',
'application/pdf': 'pdf',
};
/**
* Ядровый сервис файлов платежей: чек об оплате исходящего платежа.
*
* «Культура денег»: входящие подтверждаем, исходящие сопровождаем чеком.
* Привязка по `payment_hash` — единый ключ любого платежа, поэтому механизм
* один на все исходящие (возврат паевого/withdrawal/registration-refund/аванс
* расхода) и переиспользуется расширениями. Контроль мягкий: статус платежа не
* трогаем (деньги уже ушли on-chain), но зеркалим число чеков в
* `blockchain_data.proof_count` — реестр по нему рисует «чек приложен / нет».
*
* Ключ MinIO детерминирован: `{coopname}/gateway/{payment_hash}/{kind}/{checksum}.{ext}`.
* Повторная загрузка того же содержимого идемпотентна на бакете, в БД защищаемся
* явным `findByChecksum` (409 при дубле).
*/
@UseBucket(GATEWAY_BUCKET)
@Injectable()
export class PaymentFilesService {
constructor(
@InjectBucket() private readonly bucket: InterFileStorageBucket,
@Inject(PAYMENT_FILE_REPOSITORY) private readonly files: PaymentFileRepository,
@Inject(PAYMENT_REPOSITORY) private readonly payments: PaymentRepository
) {}
async uploadProof(
input: UploadPaymentProofInputDTO,
uploadedByUsername: string
): Promise<{ data: IPaymentFileDatabaseData; readUrl: string }> {
const body = Buffer.from(input.content_base64, 'base64');
if (body.byteLength !== input.size_bytes) {
throw new BadRequestException(
`size_bytes (${input.size_bytes}) не совпадает с фактическим размером base64-контента (${body.byteLength}).`
);
}
const actualChecksum = createHash('sha256').update(body).digest('hex');
if (actualChecksum !== input.checksum_sha256.toLowerCase()) {
throw new BadRequestException(`checksum_sha256 не совпадает с реальным SHA-256 содержимого.`);
}
const payment = await this.payments.findByHash(input.payment_hash);
if (!payment) {
throw new NotFoundException(`Платёж ${input.payment_hash} не найден.`);
}
const existing = await this.files.findByChecksum(input.coopname, actualChecksum);
if (existing) {
throw new ConflictException(
`Файл с таким SHA-256 уже зарегистрирован в этом кооперативе (id=${existing.id}).`
);
}
const storageKey = this.buildKey({
coopname: input.coopname,
paymentHash: input.payment_hash,
kind: PaymentFileKind.PAYMENT_PROOF,
checksum: actualChecksum,
mimeType: input.mime_type,
});
await this.bucket.put(storageKey, new Uint8Array(body), { contentType: input.mime_type });
const saved = await this.files.create({
coopname: input.coopname,
payment_hash: input.payment_hash.toLowerCase(),
kind: PaymentFileKind.PAYMENT_PROOF,
checksum_sha256: actualChecksum,
mime_type: input.mime_type,
size_bytes: body.byteLength,
storage_key: storageKey,
original_filename: input.original_filename ?? null,
uploaded_by_username: uploadedByUsername,
uploaded_at: new Date(),
});
await this.syncProofMark(saved.coopname, saved.payment_hash);
const readUrl = await this.bucket.getReadUrl(storageKey);
return { data: saved, readUrl };
}
async getReadUrl(fileId: number): Promise<{ data: IPaymentFileDatabaseData; readUrl: string }> {
const file = await this.files.findById(fileId);
if (!file) throw new NotFoundException(`Файл платежа #${fileId} не найден.`);
const readUrl = await this.bucket.getReadUrl(file.storage_key);
return { data: file, readUrl };
}
async listByPayment(coopname: string, paymentHash: string): Promise<IPaymentFileDatabaseData[]> {
return this.files.findByPayment(coopname, paymentHash.toLowerCase());
}
async deleteFile(fileId: number): Promise<void> {
const file = await this.files.findById(fileId);
if (!file) throw new NotFoundException(`Файл платежа #${fileId} не найден.`);
await this.bucket.delete(file.storage_key);
await this.files.delete(fileId);
await this.syncProofMark(file.coopname, file.payment_hash);
}
/**
* Зеркалит число приложенных чеков в платёж (`blockchain_data.proof_count`) —
* реестр показывает по нему «чек приложен / не приложен», не дёргая хранилище
* файлов на каждую строку. Статус платежа не трогаем (им управляет gateway).
*/
private async syncProofMark(coopname: string, paymentHash: string): Promise<void> {
const payment = await this.payments.findByHash(paymentHash);
if (!payment?.id) return;
const proofs = await this.files.findByPayment(coopname, paymentHash);
await this.payments.update(payment.id, {
blockchain_data: { ...(payment.blockchain_data ?? {}), proof_count: proofs.length },
});
}
private buildKey(params: {
coopname: string;
paymentHash: string;
kind: PaymentFileKind;
checksum: string;
mimeType: string;
}): string {
const ext = EXTENSION_BY_MIME[params.mimeType] ?? 'bin';
return `${params.coopname}/gateway/${params.paymentHash.toLowerCase()}/${params.kind}/${params.checksum}.${ext}`;
}
}
@@ -0,0 +1,23 @@
/**
* Spec MinIO-бакета `gateway:files` — ядровое хранилище файлов платежей.
*
* Содержимое: чеки об оплате (PAYMENT_PROOF) по исходящим платежам — единая
* «культура денег» (входящие подтверждаем, исходящие сопровождаем чеком).
* Привязка по `payment_hash`; переиспользуется всеми расширениями.
*
* Ключ объекта: `{coopname}/gateway/{payment_hash}/{kind}/{checksum}.{ext}`.
*/
export const GATEWAY_BUCKET = {
name: 'gateway:files',
maxBytes: 20 * 1024 * 1024,
allowedMime: [
'image/jpeg',
'image/png',
'image/webp',
'image/heic',
'application/pdf',
] as const,
defaultUrlTtlSeconds: 600,
} as const;
export type GatewayBucketAllowedMime = (typeof GATEWAY_BUCKET.allowedMime)[number];
@@ -0,0 +1,20 @@
import { registerEnumType } from '@nestjs/graphql';
/**
* Назначение файла, приложенного к платежу (бакет `gateway:files`).
*
* Ядровая «культура денег»: каждый ИСХОДЯЩИЙ платёж сопровождается чеком об
* оплате (подтверждающий документ — для кассира «чем оплатил», для пайщика «на
* что»). Сейчас единственный вид — PAYMENT_PROOF; enum оставлен расширяемым.
*/
export enum PaymentFileKind {
PAYMENT_PROOF = 'PAYMENT_PROOF',
}
registerEnumType(PaymentFileKind, {
name: 'PaymentFileKind',
description: 'Тип файла, приложенного к платежу.',
valuesMap: {
PAYMENT_PROOF: { description: 'Чек об оплате — подтверждающий документ по исполненному платежу.' },
},
});
@@ -0,0 +1,24 @@
import { PaymentFileKind } from '../enums/payment-file-kind.enum';
/**
* Запись о файле, приложенном к платежу (чек об оплате). Сам бинарь лежит в
* MinIO-бакете `gateway:files`; в блокчейне файла нет — это чистая БД-сущность,
* для проверки целостности используется `checksum_sha256`.
*
* Привязка — по `payment_hash` (единый ключ любого платежа). Ключ MinIO:
* `{coopname}/gateway/{payment_hash}/{kind}/{checksum}.{ext}`.
*/
export interface IPaymentFileDatabaseData {
id?: number;
coopname: string;
payment_hash: string;
kind: PaymentFileKind;
checksum_sha256: string;
mime_type: string;
size_bytes: number;
storage_key: string;
/** Оригинальное имя загруженного файла — для отображения и поиска */
original_filename?: string | null;
uploaded_by_username: string;
uploaded_at: Date;
}
@@ -0,0 +1,16 @@
import type { IPaymentFileDatabaseData } from '../interfaces/payment-file-database.interface';
/**
* Реестр файлов, приложенных к платежам (чеки об оплате). Файл — чистая БД-сущность
* (MinIO + метаданные), блокчейна за ним нет, поэтому интерфейс не наследует
* sync-репозиторий.
*/
export interface PaymentFileRepository {
create(data: IPaymentFileDatabaseData): Promise<IPaymentFileDatabaseData>;
findById(id: number): Promise<IPaymentFileDatabaseData | null>;
findByChecksum(coopname: string, checksum: string): Promise<IPaymentFileDatabaseData | null>;
findByPayment(coopname: string, paymentHash: string): Promise<IPaymentFileDatabaseData[]>;
delete(id: number): Promise<void>;
}
export const PAYMENT_FILE_REPOSITORY = Symbol('PaymentFileRepository');
@@ -0,0 +1,50 @@
import { Entity, Column, Index, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
import { PaymentFileKind } from '~/domain/gateway/enums/payment-file-kind.enum';
export const EntityName = 'payment_files';
/**
* Файл, приложенный к платежу (чек об оплате). Бинарь — в MinIO `gateway:files`,
* здесь только метаданные. Привязка — по `payment_hash` (единый ключ платежа).
*/
@Entity(EntityName)
@Index(`uq_${EntityName}_checksum`, ['coopname', 'checksum_sha256'], { unique: true })
@Index(`idx_${EntityName}_payment`, ['coopname', 'payment_hash'])
export class PaymentFileEntity {
static getTableName(): string {
return EntityName;
}
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar' })
coopname!: string;
@Column({ type: 'varchar' })
payment_hash!: string;
@Column({ type: 'enum', enum: PaymentFileKind })
kind!: PaymentFileKind;
@Column({ type: 'varchar', length: 64 })
checksum_sha256!: string;
@Column({ type: 'varchar', length: 120 })
mime_type!: string;
@Column({ type: 'integer' })
size_bytes!: number;
@Column({ type: 'varchar', length: 512 })
storage_key!: string;
@Column({ type: 'varchar', length: 255, nullable: true, comment: 'Оригинальное имя загруженного файла — для отображения и поиска' })
original_filename!: string | null;
@Column({ type: 'varchar' })
uploaded_by_username!: string;
@CreateDateColumn({ type: 'timestamp' })
uploaded_at!: Date;
}
@@ -0,0 +1,73 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PaymentFileEntity } from '../entities/payment-file.entity';
import type { PaymentFileRepository } from '~/domain/gateway/repositories/payment-file.repository';
import type { IPaymentFileDatabaseData } from '~/domain/gateway/interfaces/payment-file-database.interface';
/**
* TypeORM-репозиторий файлов платежей (чеков об оплате). БД-only сущность —
* маппинг симметричный, без доменной логики.
*/
@Injectable()
export class TypeormPaymentFileRepository implements PaymentFileRepository {
constructor(
@InjectRepository(PaymentFileEntity)
private readonly repository: Repository<PaymentFileEntity>
) {}
async create(data: IPaymentFileDatabaseData): Promise<IPaymentFileDatabaseData> {
const entity = this.repository.create({
coopname: data.coopname,
payment_hash: data.payment_hash,
kind: data.kind,
checksum_sha256: data.checksum_sha256,
mime_type: data.mime_type,
size_bytes: data.size_bytes,
storage_key: data.storage_key,
original_filename: data.original_filename ?? null,
uploaded_by_username: data.uploaded_by_username,
uploaded_at: data.uploaded_at,
});
const saved = await this.repository.save(entity);
return this.toDomain(saved);
}
async findById(id: number): Promise<IPaymentFileDatabaseData | null> {
const entity = await this.repository.findOne({ where: { id } });
return entity ? this.toDomain(entity) : null;
}
async findByChecksum(coopname: string, checksum: string): Promise<IPaymentFileDatabaseData | null> {
const entity = await this.repository.findOne({ where: { coopname, checksum_sha256: checksum } });
return entity ? this.toDomain(entity) : null;
}
async findByPayment(coopname: string, paymentHash: string): Promise<IPaymentFileDatabaseData[]> {
const entities = await this.repository.find({
where: { coopname, payment_hash: paymentHash.toLowerCase() },
order: { uploaded_at: 'DESC' },
});
return entities.map((e) => this.toDomain(e));
}
async delete(id: number): Promise<void> {
await this.repository.delete({ id });
}
private toDomain(entity: PaymentFileEntity): IPaymentFileDatabaseData {
return {
id: entity.id,
coopname: entity.coopname,
payment_hash: entity.payment_hash,
kind: entity.kind,
checksum_sha256: entity.checksum_sha256,
mime_type: entity.mime_type,
size_bytes: entity.size_bytes,
storage_key: entity.storage_key,
original_filename: entity.original_filename,
uploaded_by_username: entity.uploaded_by_username,
uploaded_at: entity.uploaded_at,
};
}
}
@@ -24,6 +24,9 @@ import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-proc
import { PaymentEntity } from './entities/payment.entity';
import { PAYMENT_REPOSITORY } from '~/domain/gateway/repositories/payment.repository';
import { TypeOrmPaymentRepository } from './repositories/typeorm-payment.repository';
import { PaymentFileEntity } from './entities/payment-file.entity';
import { PAYMENT_FILE_REPOSITORY } from '~/domain/gateway/repositories/payment-file.repository';
import { TypeormPaymentFileRepository } from './repositories/typeorm-payment-file.repository';
import { WebPushSubscriptionEntity } from './entities/web-push-subscription.entity';
import { NOTIFICATION_SUBSCRIPTION_PORT } from '~/domain/notification/interfaces/web-push-subscription.port';
import { TypeOrmWebPushSubscriptionRepository } from './repositories/typeorm-web-push-subscription.repository';
@@ -121,6 +124,7 @@ import { NotificationInboxTypeormEntity } from './entities/notification-inbox.ty
MigrationEntity,
CandidateEntity,
PaymentEntity,
PaymentFileEntity,
WebPushSubscriptionEntity,
LedgerOperationEntity,
AgreementTypeormEntity,
@@ -175,6 +179,10 @@ import { NotificationInboxTypeormEntity } from './entities/notification-inbox.ty
provide: PAYMENT_REPOSITORY,
useClass: TypeOrmPaymentRepository,
},
{
provide: PAYMENT_FILE_REPOSITORY,
useClass: TypeormPaymentFileRepository,
},
{
provide: NOTIFICATION_SUBSCRIPTION_PORT,
useClass: TypeOrmWebPushSubscriptionRepository,
@@ -276,6 +284,7 @@ import { NotificationInboxTypeormEntity } from './entities/notification-inbox.ty
MIGRATION_REPOSITORY,
CANDIDATE_REPOSITORY,
PAYMENT_REPOSITORY,
PAYMENT_FILE_REPOSITORY,
NOTIFICATION_SUBSCRIPTION_PORT,
LEDGER_OPERATION_REPOSITORY,
AGREEMENT_REPOSITORY,
+30
View File
@@ -1370,6 +1370,9 @@ export const AllTypesProps: Record<string,any> = {
uploadExpenseFile:{
data:"UploadExpenseFileInput"
},
uploadPaymentProof:{
data:"UploadPaymentProofInput"
},
verifyEmail:{
data:"VerifyEmailInputDTO"
},
@@ -1427,6 +1430,7 @@ export const AllTypesProps: Record<string,any> = {
},
PaymentDirection: "enum" as const,
PaymentFileKind: "enum" as const,
PaymentFiltersInput:{
direction:"PaymentDirection",
status:"PaymentStatus",
@@ -1821,6 +1825,12 @@ export const AllTypesProps: Record<string,any> = {
},
onecoopGetDocuments:{
data:"GetOneCoopDocumentsInput"
},
paymentFile:{
},
paymentProofs:{
},
process:{
@@ -2120,6 +2130,9 @@ export const AllTypesProps: Record<string,any> = {
},
UploadExpenseFileInput:{
kind:"ExpenseFileKind"
},
UploadPaymentProofInput:{
},
UserStatus: "enum" as const,
VarsInput:{
@@ -4060,6 +4073,7 @@ export const ReturnTypes: Record<string,any> = {
updateSettings:"Settings",
updateSystem:"SystemInfo",
uploadExpenseFile:"ExpenseFile",
uploadPaymentProof:"PaymentFile",
verifyEmail:"Boolean",
voteOnAnnualGeneralMeet:"MeetAggregate",
walmoveWallets:"Ledger2AdjustmentResult"
@@ -4341,6 +4355,20 @@ export const ReturnTypes: Record<string,any> = {
fee_percent:"Float",
tolerance_percent:"Float"
},
PaymentFile:{
checksum_sha256:"String",
coopname:"String",
id:"Int",
kind:"PaymentFileKind",
mime_type:"String",
original_filename:"String",
payment_hash:"String",
read_url:"String",
size_bytes:"Int",
storage_key:"String",
uploaded_at:"DateTime",
uploaded_by_username:"String"
},
PaymentMethod:{
created_at:"DateTime",
data:"PaymentMethodData",
@@ -4656,6 +4684,8 @@ export const ReturnTypes: Record<string,any> = {
membershipExit:"MembershipExit",
membershipExitReturnPreview:"MembershipExitReturnPreview",
onecoopGetDocuments:"OneCoopDocumentsResponse",
paymentFile:"PaymentFile",
paymentProofs:"PaymentFile",
process:"ProcessView",
processes:"ProcessSummaryPaginationResult",
searchDocuments:"SearchResult",
+222 -2
View File
@@ -7227,6 +7227,7 @@ updateRequest?: [{ data: ValueTypes["UpdateRequestInput"] | Variable<any, string
updateSettings?: [{ data: ValueTypes["UpdateSettingsInput"] | Variable<any, string>},ValueTypes["Settings"]],
updateSystem?: [{ data: ValueTypes["Update"] | Variable<any, string>},ValueTypes["SystemInfo"]],
uploadExpenseFile?: [{ data: ValueTypes["UploadExpenseFileInput"] | Variable<any, string>},ValueTypes["ExpenseFile"]],
uploadPaymentProof?: [{ data: ValueTypes["UploadPaymentProofInput"] | Variable<any, string>},ValueTypes["PaymentFile"]],
verifyEmail?: [{ data: ValueTypes["VerifyEmailInputDTO"] | Variable<any, string>},boolean | `@${string}`],
voteOnAnnualGeneralMeet?: [{ data: ValueTypes["VoteOnAnnualGeneralMeetInput"] | Variable<any, string>},ValueTypes["MeetAggregate"]],
walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},ValueTypes["Ledger2AdjustmentResult"]],
@@ -7964,6 +7965,37 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},V
}>;
/** Направление платежа */
["PaymentDirection"]:PaymentDirection;
/** Запись о файле, приложенном к платежу (чек об оплате). */
["PaymentFile"]: AliasType<{
/** SHA-256 содержимого, hex-lowercase. */
checksum_sha256?:boolean | `@${string}`,
/** Имя кооператива (scope). */
coopname?:boolean | `@${string}`,
/** Внутренний ID записи. */
id?:boolean | `@${string}`,
/** Назначение файла. */
kind?:boolean | `@${string}`,
/** MIME-тип содержимого. */
mime_type?:boolean | `@${string}`,
/** Оригинальное имя загруженного файла. */
original_filename?:boolean | `@${string}`,
/** Хеш платежа. */
payment_hash?:boolean | `@${string}`,
/** Короткоживущий URL на скачивание (HMAC-signed). */
read_url?:boolean | `@${string}`,
/** Размер файла в байтах. */
size_bytes?:boolean | `@${string}`,
/** MinIO-ключ внутри бакета. */
storage_key?:boolean | `@${string}`,
/** Когда загружено. */
uploaded_at?:boolean | `@${string}`,
/** Кто загрузил (username). */
uploaded_by_username?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on PaymentFile']?: Omit<ValueTypes["PaymentFile"], "...on PaymentFile">
}>;
/** Тип файла, приложенного к платежу. */
["PaymentFileKind"]:PaymentFileKind;
["PaymentFiltersInput"]: {
/** Название кооператива */
coopname?: string | undefined | null | Variable<any, string>,
@@ -8689,6 +8721,8 @@ listReportDrafts?: [{ filter?: ValueTypes["ListReportDraftsFilterInput"] | undef
membershipExit?: [{ coopname: string | Variable<any, string>, username: string | Variable<any, string>},ValueTypes["MembershipExit"]],
membershipExitReturnPreview?: [{ coopname: string | Variable<any, string>, username: string | Variable<any, string>},ValueTypes["MembershipExitReturnPreview"]],
onecoopGetDocuments?: [{ data: ValueTypes["GetOneCoopDocumentsInput"] | Variable<any, string>},ValueTypes["OneCoopDocumentsResponse"]],
paymentFile?: [{ id: number | Variable<any, string>},ValueTypes["PaymentFile"]],
paymentProofs?: [{ coopname: string | Variable<any, string>, payment_hash: string | Variable<any, string>},ValueTypes["PaymentFile"]],
process?: [{ coopname: string | Variable<any, string>, hash: string | Variable<any, string>},ValueTypes["ProcessView"]],
processes?: [{ filter: ValueTypes["ProcessesFilter"] | Variable<any, string>, pagination: ValueTypes["PaginationInput"] | Variable<any, string>},ValueTypes["ProcessSummaryPaginationResult"]],
searchDocuments?: [{ data: ValueTypes["SearchDocumentsInput"] | Variable<any, string>},ValueTypes["SearchResult"]],
@@ -10214,6 +10248,22 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
proposal_hash: string | Variable<any, string>,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number | Variable<any, string>
};
["UploadPaymentProofInput"]: {
/** SHA-256 содержимого, hex-lowercase (64 hex-символа). */
checksum_sha256: string | Variable<any, string>,
/** Содержимое файла, base64 без префикса data:. */
content_base64: string | Variable<any, string>,
/** Имя кооператива. */
coopname: string | Variable<any, string>,
/** MIME-тип содержимого. */
mime_type: string | Variable<any, string>,
/** Оригинальное имя файла — для отображения и поиска. */
original_filename?: string | undefined | null | Variable<any, string>,
/** Хеш платежа, к которому прикладывается чек. */
payment_hash: string | Variable<any, string>,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number | Variable<any, string>
};
["UserAccount"]: AliasType<{
/** Метаинформация */
@@ -16411,6 +16461,7 @@ updateRequest?: [{ data: ResolverInputTypes["UpdateRequestInput"]},ResolverInput
updateSettings?: [{ data: ResolverInputTypes["UpdateSettingsInput"]},ResolverInputTypes["Settings"]],
updateSystem?: [{ data: ResolverInputTypes["Update"]},ResolverInputTypes["SystemInfo"]],
uploadExpenseFile?: [{ data: ResolverInputTypes["UploadExpenseFileInput"]},ResolverInputTypes["ExpenseFile"]],
uploadPaymentProof?: [{ data: ResolverInputTypes["UploadPaymentProofInput"]},ResolverInputTypes["PaymentFile"]],
verifyEmail?: [{ data: ResolverInputTypes["VerifyEmailInputDTO"]},boolean | `@${string}`],
voteOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["VoteOnAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]],
walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputTypes["Ledger2AdjustmentResult"]],
@@ -17109,6 +17160,36 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType
}>;
/** Направление платежа */
["PaymentDirection"]:PaymentDirection;
/** Запись о файле, приложенном к платежу (чек об оплате). */
["PaymentFile"]: AliasType<{
/** SHA-256 содержимого, hex-lowercase. */
checksum_sha256?:boolean | `@${string}`,
/** Имя кооператива (scope). */
coopname?:boolean | `@${string}`,
/** Внутренний ID записи. */
id?:boolean | `@${string}`,
/** Назначение файла. */
kind?:boolean | `@${string}`,
/** MIME-тип содержимого. */
mime_type?:boolean | `@${string}`,
/** Оригинальное имя загруженного файла. */
original_filename?:boolean | `@${string}`,
/** Хеш платежа. */
payment_hash?:boolean | `@${string}`,
/** Короткоживущий URL на скачивание (HMAC-signed). */
read_url?:boolean | `@${string}`,
/** Размер файла в байтах. */
size_bytes?:boolean | `@${string}`,
/** MinIO-ключ внутри бакета. */
storage_key?:boolean | `@${string}`,
/** Когда загружено. */
uploaded_at?:boolean | `@${string}`,
/** Кто загрузил (username). */
uploaded_by_username?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
/** Тип файла, приложенного к платежу. */
["PaymentFileKind"]:PaymentFileKind;
["PaymentFiltersInput"]: {
/** Название кооператива */
coopname?: string | undefined | null,
@@ -17812,6 +17893,8 @@ listReportDrafts?: [{ filter?: ResolverInputTypes["ListReportDraftsFilterInput"]
membershipExit?: [{ coopname: string, username: string},ResolverInputTypes["MembershipExit"]],
membershipExitReturnPreview?: [{ coopname: string, username: string},ResolverInputTypes["MembershipExitReturnPreview"]],
onecoopGetDocuments?: [{ data: ResolverInputTypes["GetOneCoopDocumentsInput"]},ResolverInputTypes["OneCoopDocumentsResponse"]],
paymentFile?: [{ id: number},ResolverInputTypes["PaymentFile"]],
paymentProofs?: [{ coopname: string, payment_hash: string},ResolverInputTypes["PaymentFile"]],
process?: [{ coopname: string, hash: string},ResolverInputTypes["ProcessView"]],
processes?: [{ filter: ResolverInputTypes["ProcessesFilter"], pagination: ResolverInputTypes["PaginationInput"]},ResolverInputTypes["ProcessSummaryPaginationResult"]],
searchDocuments?: [{ data: ResolverInputTypes["SearchDocumentsInput"]},ResolverInputTypes["SearchResult"]],
@@ -19298,6 +19381,22 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
proposal_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UploadPaymentProofInput"]: {
/** SHA-256 содержимого, hex-lowercase (64 hex-символа). */
checksum_sha256: string,
/** Содержимое файла, base64 без префикса data:. */
content_base64: string,
/** Имя кооператива. */
coopname: string,
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя файла — для отображения и поиска. */
original_filename?: string | undefined | null,
/** Хеш платежа, к которому прикладывается чек. */
payment_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UserAccount"]: AliasType<{
/** Метаинформация */
@@ -25843,6 +25942,10 @@ export type ModelTypes = {
Требуемые роли: chairman, member, user. */
uploadExpenseFile: ModelTypes["ExpenseFile"],
/** Приложить чек об оплате к платежу (бакет gateway:files).
Требуемые роли: chairman, member. */
uploadPaymentProof: ModelTypes["PaymentFile"],
/** Подтвердить email адрес пользователя */
verifyEmail: boolean,
/** Голосование на общем собрании пайщиков
@@ -26503,6 +26606,34 @@ export type ModelTypes = {
tolerance_percent: number
};
["PaymentDirection"]:PaymentDirection;
/** Запись о файле, приложенном к платежу (чек об оплате). */
["PaymentFile"]: {
/** SHA-256 содержимого, hex-lowercase. */
checksum_sha256: string,
/** Имя кооператива (scope). */
coopname: string,
/** Внутренний ID записи. */
id: number,
/** Назначение файла. */
kind: ModelTypes["PaymentFileKind"],
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя загруженного файла. */
original_filename?: string | undefined | null,
/** Хеш платежа. */
payment_hash: string,
/** Короткоживущий URL на скачивание (HMAC-signed). */
read_url?: string | undefined | null,
/** Размер файла в байтах. */
size_bytes: number,
/** MinIO-ключ внутри бакета. */
storage_key: string,
/** Когда загружено. */
uploaded_at: ModelTypes["DateTime"],
/** Кто загрузил (username). */
uploaded_by_username: string
};
["PaymentFileKind"]:PaymentFileKind;
["PaymentFiltersInput"]: {
/** Название кооператива */
coopname?: string | undefined | null,
@@ -27092,7 +27223,7 @@ export type ModelTypes = {
capitalResults: ModelTypes["PaginatedCapitalResultsPaginationResult"],
/** Получение одного сегмента кооператива по фильтрам */
capitalSegment?: ModelTypes["CapitalSegment"] | undefined | null,
/** Получение списка сементов кооператива с фильтрацией и пагинацией */
/** Получение списка сегментов кооператива с фильтрацией и пагинацией */
capitalSegments: ModelTypes["PaginatedCapitalSegmentsPaginationResult"],
/** Получение полного состояния CAPITAL контракта кооператива */
capitalState?: ModelTypes["CapitalState"] | undefined | null,
@@ -27394,6 +27525,14 @@ export type ModelTypes = {
membershipExitReturnPreview: ModelTypes["MembershipExitReturnPreview"],
/** Получение документов кооператива для синхронизации с 1С. Требует секретный ключ в заголовке x-onecoop-secret-key. */
onecoopGetDocuments: ModelTypes["OneCoopDocumentsResponse"],
/** Получить запись о файле платежа + свежий короткоживущий read-URL.
Требуемые роли: chairman, member, user. */
paymentFile: ModelTypes["PaymentFile"],
/** Список чеков об оплате платежа (без read-URL запрос отдельно по id).
Требуемые роли: chairman, member, user. */
paymentProofs: Array<ModelTypes["PaymentFile"]>,
/** Получить полную картину процесса ledger2 по process_hash
Требуемые роли: chairman, member. */
@@ -28847,6 +28986,22 @@ export type ModelTypes = {
proposal_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UploadPaymentProofInput"]: {
/** SHA-256 содержимого, hex-lowercase (64 hex-символа). */
checksum_sha256: string,
/** Содержимое файла, base64 без префикса data:. */
content_base64: string,
/** Имя кооператива. */
coopname: string,
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя файла — для отображения и поиска. */
original_filename?: string | undefined | null,
/** Хеш платежа, к которому прикладывается чек. */
payment_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UserAccount"]: {
/** Метаинформация */
@@ -35703,6 +35858,10 @@ export type GraphQLTypes = {
Требуемые роли: chairman, member, user. */
uploadExpenseFile: GraphQLTypes["ExpenseFile"],
/** Приложить чек об оплате к платежу (бакет gateway:files).
Требуемые роли: chairman, member. */
uploadPaymentProof: GraphQLTypes["PaymentFile"],
/** Подтвердить email адрес пользователя */
verifyEmail: boolean,
/** Голосование на общем собрании пайщиков
@@ -36446,6 +36605,37 @@ export type GraphQLTypes = {
};
/** Направление платежа */
["PaymentDirection"]: PaymentDirection;
/** Запись о файле, приложенном к платежу (чек об оплате). */
["PaymentFile"]: {
__typename: "PaymentFile",
/** SHA-256 содержимого, hex-lowercase. */
checksum_sha256: string,
/** Имя кооператива (scope). */
coopname: string,
/** Внутренний ID записи. */
id: number,
/** Назначение файла. */
kind: GraphQLTypes["PaymentFileKind"],
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя загруженного файла. */
original_filename?: string | undefined | null,
/** Хеш платежа. */
payment_hash: string,
/** Короткоживущий URL на скачивание (HMAC-signed). */
read_url?: string | undefined | null,
/** Размер файла в байтах. */
size_bytes: number,
/** MinIO-ключ внутри бакета. */
storage_key: string,
/** Когда загружено. */
uploaded_at: GraphQLTypes["DateTime"],
/** Кто загрузил (username). */
uploaded_by_username: string,
['...on PaymentFile']: Omit<GraphQLTypes["PaymentFile"], "...on PaymentFile">
};
/** Тип файла, приложенного к платежу. */
["PaymentFileKind"]: PaymentFileKind;
["PaymentFiltersInput"]: {
/** Название кооператива */
coopname?: string | undefined | null,
@@ -37098,7 +37288,7 @@ export type GraphQLTypes = {
capitalResults: GraphQLTypes["PaginatedCapitalResultsPaginationResult"],
/** Получение одного сегмента кооператива по фильтрам */
capitalSegment?: GraphQLTypes["CapitalSegment"] | undefined | null,
/** Получение списка сементов кооператива с фильтрацией и пагинацией */
/** Получение списка сегментов кооператива с фильтрацией и пагинацией */
capitalSegments: GraphQLTypes["PaginatedCapitalSegmentsPaginationResult"],
/** Получение полного состояния CAPITAL контракта кооператива */
capitalState?: GraphQLTypes["CapitalState"] | undefined | null,
@@ -37400,6 +37590,14 @@ export type GraphQLTypes = {
membershipExitReturnPreview: GraphQLTypes["MembershipExitReturnPreview"],
/** Получение документов кооператива для синхронизации с 1С. Требует секретный ключ в заголовке x-onecoop-secret-key. */
onecoopGetDocuments: GraphQLTypes["OneCoopDocumentsResponse"],
/** Получить запись о файле платежа + свежий короткоживущий read-URL.
Требуемые роли: chairman, member, user. */
paymentFile: GraphQLTypes["PaymentFile"],
/** Список чеков об оплате платежа (без read-URL запрос отдельно по id).
Требуемые роли: chairman, member, user. */
paymentProofs: Array<GraphQLTypes["PaymentFile"]>,
/** Получить полную картину процесса ledger2 по process_hash
Требуемые роли: chairman, member. */
@@ -38937,6 +39135,22 @@ export type GraphQLTypes = {
proposal_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UploadPaymentProofInput"]: {
/** SHA-256 содержимого, hex-lowercase (64 hex-символа). */
checksum_sha256: string,
/** Содержимое файла, base64 без префикса data:. */
content_base64: string,
/** Имя кооператива. */
coopname: string,
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя файла — для отображения и поиска. */
original_filename?: string | undefined | null,
/** Хеш платежа, к которому прикладывается чек. */
payment_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UserAccount"]: {
__typename: "UserAccount",
@@ -39476,6 +39690,10 @@ export enum PaymentDirection {
INCOMING = "INCOMING",
OUTGOING = "OUTGOING"
}
/** Тип файла, приложенного к платежу. */
export enum PaymentFileKind {
PAYMENT_PROOF = "PAYMENT_PROOF"
}
/** Статус платежа */
export enum PaymentStatus {
AWAITING_AUTHORIZATION = "AWAITING_AUTHORIZATION",
@@ -39885,6 +40103,7 @@ type ZEUS_VARIABLES = {
["PassportInput"]: ValueTypes["PassportInput"];
["PayExpenseItemInput"]: ValueTypes["PayExpenseItemInput"];
["PaymentDirection"]: ValueTypes["PaymentDirection"];
["PaymentFileKind"]: ValueTypes["PaymentFileKind"];
["PaymentFiltersInput"]: ValueTypes["PaymentFiltersInput"];
["PaymentStatus"]: ValueTypes["PaymentStatus"];
["PaymentType"]: ValueTypes["PaymentType"];
@@ -40000,6 +40219,7 @@ type ZEUS_VARIABLES = {
["UpdateSettingsInput"]: ValueTypes["UpdateSettingsInput"];
["UpdateStoryInput"]: ValueTypes["UpdateStoryInput"];
["UploadExpenseFileInput"]: ValueTypes["UploadExpenseFileInput"];
["UploadPaymentProofInput"]: ValueTypes["UploadPaymentProofInput"];
["UserStatus"]: ValueTypes["UserStatus"];
["VarsInput"]: ValueTypes["VarsInput"];
["VerifyEmailInputDTO"]: ValueTypes["VerifyEmailInputDTO"];
@@ -1,34 +1,9 @@
<template lang="pug">
.attach-proof
.exp-step(v-if='step')
.exp-step__num {{ step.number }}
.exp-step__title {{ step.title }}
//- Секция 1 — платёжка/квитанция об исполненной оплате (для любой механики).
//- Закрывающие документы организации (акт, счёт-фактура, накладная) — только
//- прямая оплата по счёту (DIRECT). Чек об оплате теперь общий, в ядре
//- (AttachPaymentProofPanel по payment_hash) — здесь остаётся expense-специфика.
.attach-proof(v-if='isDirect')
.attach-proof__section
.t-sm.t-muted(v-if='step') Приложите платёжку или квитанцию это подтверждает, что оплата исполнена.
.t-sm.t-muted(v-else) Платёжка об оплате
.files(v-if='proofFiles.length')
button.file-link(
v-for='file in proofFiles',
:key='file.id',
type='button',
:disabled='openingId === file.id',
@click='openFile(file)'
)
q-icon(name='attach_file', size='16px')
span {{ fileLabel(file) }}
q-spinner(v-if='openingId === file.id', size='14px')
FileUploader(
v-model='pendingProof',
accept='image/jpeg,image/png,image/webp,image/heic,application/pdf',
:max-size='20 * 1024 * 1024',
title='Приложите платёжку или квитанцию',
hint='Изображение или PDF до 20 МБ — отправится сразу',
:disabled='uploading'
)
//- Секция 2 — закрывающие документы организации (только прямая оплата по счёту).
.attach-proof__section(v-if='isDirect')
.t-sm.t-muted Закрывающие документы (акт, счёт-фактура, накладная)
.files(v-if='closingFiles.length')
button.file-link(
@@ -62,28 +37,15 @@ import { api, type IExpenseFile } from '../api';
const props = defineProps<{
proposalHash: string;
itemHash: string;
// Опциональный заголовок-этап (номер + название) для последовательной подачи
// на столе кассира.
step?: { number: number | string; title: string };
}>();
const emit = defineEmits<{
// Срабатывает только при загрузке ПЛАТЁЖКИ — реестр по ней инкрементит отметку
// proof_count (закрывающие документы на эту отметку не влияют).
(e: 'uploaded'): void;
}>();
const system = useSystemStore();
const files = ref<IExpenseFile[]>([]);
const pendingProof = ref<File | null>(null);
const pendingClosing = ref<File | null>(null);
const uploading = ref(false);
const mechanics = ref<Zeus.ExpenseMechanics | null>(null);
const isDirect = computed(() => mechanics.value === Zeus.ExpenseMechanics.DIRECT);
const proofFiles = computed(() =>
files.value.filter((f) => f.kind === Zeus.ExpenseFileKind.PAYMENT_PROOF),
);
const closingFiles = computed(() =>
files.value.filter((f) => f.kind === Zeus.ExpenseFileKind.CLOSING_DOC),
);
@@ -96,7 +58,6 @@ async function refresh(): Promise<void> {
props.itemHash,
);
} catch {
// список файлов — вспомогательный; ошибки загрузки списка не прерывают кассира
files.value = [];
}
}
@@ -107,8 +68,6 @@ function fileLabel(file: IExpenseFile): string {
return `Документ от ${date}`;
}
// Списочные запросы файлов отдают записи без read_url (он короткоживущий) —
// свежая ссылка запрашивается по id в момент клика и сразу открывается.
const openingId = ref<number | null>(null);
async function openFile(file: IExpenseFile): Promise<void> {
try {
@@ -137,7 +96,7 @@ function toBase64(buffer: ArrayBuffer): string {
return btoa(binary);
}
async function upload(file: File, kind: Zeus.ExpenseFileKind): Promise<void> {
async function upload(file: File): Promise<void> {
try {
uploading.value = true;
const buffer = await file.arrayBuffer();
@@ -145,21 +104,15 @@ async function upload(file: File, kind: Zeus.ExpenseFileKind): Promise<void> {
coopname: system.info.coopname,
proposal_hash: props.proposalHash,
item_hash: props.itemHash,
kind,
kind: Zeus.ExpenseFileKind.CLOSING_DOC,
mime_type: file.type,
size_bytes: file.size,
checksum_sha256: await sha256Hex(buffer),
content_base64: toBase64(buffer),
original_filename: file.name,
});
SuccessAlert(
kind === Zeus.ExpenseFileKind.PAYMENT_PROOF
? 'Платёжка приложена'
: 'Закрывающий документ приложен',
);
SuccessAlert('Закрывающий документ приложен');
await refresh();
// proof_count в реестре считает только платёжки.
if (kind === Zeus.ExpenseFileKind.PAYMENT_PROOF) emit('uploaded');
} catch (e) {
FailAlert(e);
} finally {
@@ -167,16 +120,9 @@ async function upload(file: File, kind: Zeus.ExpenseFileKind): Promise<void> {
}
}
// Выбор файла = загрузка: отдельная кнопка «Загрузить» не нужна.
watch(pendingProof, (file) => {
if (!file || uploading.value) return;
void upload(file, Zeus.ExpenseFileKind.PAYMENT_PROOF).then(() => {
pendingProof.value = null;
});
});
watch(pendingClosing, (file) => {
if (!file || uploading.value) return;
void upload(file, Zeus.ExpenseFileKind.CLOSING_DOC).then(() => {
void upload(file).then(() => {
pendingClosing.value = null;
});
});
@@ -198,31 +144,6 @@ onMounted(async () => {
gap: var(--p-3);
}
.exp-step {
display: flex;
align-items: center;
gap: var(--p-2);
}
.exp-step__num {
flex: 0 0 auto;
width: 22px;
height: 22px;
border-radius: var(--p-r-pill);
background: var(--p-primary-soft);
color: var(--p-primary);
font-size: var(--p-fs-body-sm);
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
}
.exp-step__title {
font-weight: 600;
color: var(--p-ink);
}
.attach-proof__section {
display: flex;
flex-direction: column;
@@ -0,0 +1,38 @@
import { client } from 'src/shared/api/client';
import { Mutations, Queries } from '@coopenomics/sdk';
export type IUploadPaymentProofInput = Mutations.Gateway.UploadPaymentProof.IInput['data'];
export type IPaymentFile =
Queries.Gateway.PaymentProofs.IOutput[typeof Queries.Gateway.PaymentProofs.name][number];
async function uploadPaymentProof(data: IUploadPaymentProofInput): Promise<IPaymentFile> {
const { [Mutations.Gateway.UploadPaymentProof.name]: result } = await client.Mutation(
Mutations.Gateway.UploadPaymentProof.mutation,
{ variables: { data } },
);
return result;
}
async function loadPaymentProofs(coopname: string, payment_hash: string): Promise<IPaymentFile[]> {
const { [Queries.Gateway.PaymentProofs.name]: result } = await client.Query(
Queries.Gateway.PaymentProofs.query,
{ variables: { coopname, payment_hash } },
);
return result;
}
// Списочные запросы отдают файлы без read_url (он короткоживущий) — свежий URL
// запрашивается по id в момент клика.
async function getPaymentFileReadUrl(id: number): Promise<string | undefined> {
const { [Queries.Gateway.PaymentFile.name]: result } = await client.Query(
Queries.Gateway.PaymentFile.query,
{ variables: { id } },
);
return result.read_url ?? undefined;
}
export const api = {
uploadPaymentProof,
loadPaymentProofs,
getPaymentFileReadUrl,
};
@@ -0,0 +1,2 @@
export { default as AttachPaymentProofPanel } from './ui/AttachPaymentProofPanel.vue';
export { api as attachPaymentProofApi, type IPaymentFile } from './api';
@@ -0,0 +1,199 @@
<template lang="pug">
.attach-proof
.exp-step(v-if='step')
.exp-step__num {{ step.number }}
.exp-step__title {{ step.title }}
.attach-proof__section
.t-sm.t-muted(v-if='step') Приложите чек об оплате подтверждение, что платёж исполнен.
.t-sm.t-muted(v-else) Чек об оплате
.files(v-if='proofFiles.length')
button.file-link(
v-for='file in proofFiles',
:key='file.id',
type='button',
:disabled='openingId === file.id',
@click='openFile(file)'
)
q-icon(name='attach_file', size='16px')
span {{ fileLabel(file) }}
q-spinner(v-if='openingId === file.id', size='14px')
FileUploader(
v-model='pendingProof',
accept='image/jpeg,image/png,image/webp,image/heic,application/pdf',
:max-size='20 * 1024 * 1024',
title='Приложите чек об оплате',
hint='Изображение или PDF до 20 МБ — отправится сразу',
:disabled='uploading'
)
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { useSystemStore } from 'src/entities/System/model';
import { FileUploader } from 'src/shared/ui/domain/FileUploader';
import { api, type IPaymentFile } from '../api';
const props = defineProps<{
paymentHash: string;
// Опциональный заголовок-этап (номер + название) для последовательной подачи
// на столе кассира.
step?: { number: number | string; title: string };
}>();
const emit = defineEmits<{
// Срабатывает при загрузке чека — реестр по нему инкрементит отметку proof_count.
(e: 'uploaded'): void;
}>();
const system = useSystemStore();
const files = ref<IPaymentFile[]>([]);
const pendingProof = ref<File | null>(null);
const uploading = ref(false);
const proofFiles = computed(() => files.value);
async function refresh(): Promise<void> {
try {
files.value = await api.loadPaymentProofs(system.info.coopname, props.paymentHash);
} catch {
// список файлов — вспомогательный; ошибки загрузки списка не прерывают кассира
files.value = [];
}
}
function fileLabel(file: IPaymentFile): string {
if (file.original_filename) return file.original_filename;
const date = file.uploaded_at ? new Date(String(file.uploaded_at)).toLocaleString('ru-RU') : '';
return `Чек от ${date}`;
}
const openingId = ref<number | null>(null);
async function openFile(file: IPaymentFile): Promise<void> {
try {
openingId.value = file.id;
const url = await api.getPaymentFileReadUrl(file.id);
if (!url) throw new Error('Не удалось получить ссылку на файл');
window.open(url, '_blank', 'noopener');
} catch (e) {
FailAlert(e);
} finally {
openingId.value = null;
}
}
async function sha256Hex(buffer: ArrayBuffer): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', buffer);
return Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
function toBase64(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
async function upload(file: File): Promise<void> {
try {
uploading.value = true;
const buffer = await file.arrayBuffer();
await api.uploadPaymentProof({
coopname: system.info.coopname,
payment_hash: props.paymentHash,
mime_type: file.type,
size_bytes: file.size,
checksum_sha256: await sha256Hex(buffer),
content_base64: toBase64(buffer),
original_filename: file.name,
});
SuccessAlert('Чек об оплате приложен');
await refresh();
emit('uploaded');
} catch (e) {
FailAlert(e);
} finally {
uploading.value = false;
}
}
// Выбор файла = загрузка: отдельная кнопка «Загрузить» не нужна.
watch(pendingProof, (file) => {
if (!file || uploading.value) return;
void upload(file).then(() => {
pendingProof.value = null;
});
});
onMounted(refresh);
</script>
<style lang="scss" scoped>
.attach-proof {
display: flex;
flex-direction: column;
gap: var(--p-3);
}
.exp-step {
display: flex;
align-items: center;
gap: var(--p-2);
}
.exp-step__num {
flex: 0 0 auto;
width: 22px;
height: 22px;
border-radius: var(--p-r-pill);
background: var(--p-primary-soft);
color: var(--p-primary);
font-size: var(--p-fs-body-sm);
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
}
.exp-step__title {
font-weight: 600;
color: var(--p-ink);
}
.attach-proof__section {
display: flex;
flex-direction: column;
gap: var(--p-2);
}
.files {
display: flex;
flex-direction: column;
gap: var(--p-1);
}
.file-link {
display: inline-flex;
align-items: center;
gap: var(--p-1);
padding: 0;
border: none;
background: transparent;
color: var(--p-primary);
text-decoration: none;
font-size: var(--p-fs-body-sm);
cursor: pointer;
text-align: left;
&:hover {
text-decoration: underline;
}
&:disabled {
opacity: 0.6;
cursor: default;
}
}
</style>
@@ -68,15 +68,19 @@
tr.expand-row(v-if='expanded.get(row.id)')
td(:colspan='hideActions ? 7 : 8')
PaymentDetails(:payment='row')
//- Стол совета: кассир прикладывает платёжку и закрывающие документы;
//- при личной передаче чеков — отчитывается за пайщика (панель
//- отчёта сама скрывается для прямой оплаты организации, DIRECT).
//- Чек об оплате — ядро, для любого исходящего платежа (стол кассира).
.q-mt-sm(v-if='!hideActions && paymentProofHash(row)')
AttachPaymentProofPanel(
:payment-hash='paymentProofHash(row) ?? ""',
:step='expenseProofRef(row) ? { number: 1, title: "Подтвердите оплату" } : undefined',
@uploaded='onProofUploaded(row)'
)
//- Расход: закрывающие документы (DIRECT, панель сама скрывается)
//- + отчёт пайщика.
.expense-flow.q-mt-sm(v-if='!hideActions && expenseProofRef(row)')
AttachExpenseProofPanel(
:proposal-hash='expenseProofRef(row)?.proposal_hash ?? ""',
:item-hash='expenseProofRef(row)?.item_hash ?? ""',
:step='{ number: 1, title: "Подтвердите оплату" }',
@uploaded='onProofUploaded(row)'
:item-hash='expenseProofRef(row)?.item_hash ?? ""'
)
ReportExpenseAdvancePanel(
:proposal-hash='expenseProofRef(row)?.proposal_hash ?? ""',
@@ -151,12 +155,17 @@
SetOrderRefundedStatusButton(v-if='!isRefundType(row.type)', :id='row.id')
template(v-if='expanded.get(row.id)')
PaymentDetails.pay-card__details(:payment='row')
//- Чек об оплате — ядро, для любого исходящего платежа (стол кассира).
.q-mt-sm(v-if='!hideActions && paymentProofHash(row)')
AttachPaymentProofPanel(
:payment-hash='paymentProofHash(row) ?? ""',
:step='expenseProofRef(row) ? { number: 1, title: "Подтвердите оплату" } : undefined',
@uploaded='onProofUploaded(row)'
)
.expense-flow.q-mt-sm(v-if='!hideActions && expenseProofRef(row)')
AttachExpenseProofPanel(
:proposal-hash='expenseProofRef(row)?.proposal_hash ?? ""',
:item-hash='expenseProofRef(row)?.item_hash ?? ""',
:step='{ number: 1, title: "Подтвердите оплату" }',
@uploaded='onProofUploaded(row)'
:item-hash='expenseProofRef(row)?.item_hash ?? ""'
)
ReportExpenseAdvancePanel(
:proposal-hash='expenseProofRef(row)?.proposal_hash ?? ""',
@@ -212,6 +221,7 @@ import { usePaymentStore } from 'src/entities/Payment/model';
import { SetOrderPaidStatusButton } from 'src/features/Payment/SetStatus/ui/SetOrderPaidStatusButton';
import { SetOrderRefundedStatusButton } from 'src/features/Payment/SetStatus/ui/SetOrderRefundedStatusButton';
import { AttachExpenseProofPanel } from 'src/features/Payment/AttachExpenseProof';
import { AttachPaymentProofPanel } from 'src/features/Payment/AttachPaymentProof';
import { ReportExpenseAdvancePanel } from 'src/features/Payment/ReportExpenseAdvance';
import { ExpenseSettlementBasisPanel } from 'src/features/Payment/ExpenseSettlementBasis';
import { useSessionStore } from 'src/entities/Session';
@@ -319,6 +329,15 @@ const expenseProofRef = (
return { proposal_hash: data.proposal_hash, item_hash: data.item_hash };
};
// Ядровый «чек об оплате»: любой ИСХОДЯЩИЙ платёж после оплаты (PAID/COMPLETED)
// сопровождается чеком, привязка по hash платежа. Единый механизм на все
// исходящие (возврат паевого/withdrawal/registration-refund/аванс расхода).
const paymentProofHash = (row: IPaymentRow): string | null => {
if (row.direction !== Zeus.PaymentDirection.OUTGOING) return null;
if (![Zeus.PaymentStatus.PAID, Zeus.PaymentStatus.COMPLETED].includes(row.status)) return null;
return row.hash ?? null;
};
// Личный реестр пайщика (hideActions): получатель аванса под отчёт прикладывает
// чек и подаёт отчёт по своей позиции. Механику (ADVANCE/DIRECT) панель
// проверяет сама по данным СЗ и для DIRECT не отображается.
@@ -376,10 +395,11 @@ const reportBadge = (
return { label: reportStateLabel(state), variant: reportStateVariant(state) };
};
// Отметка отчитанности оплаченного расхода: бэкенд зеркалит число платёжек
// в blockchain_data.proof_count при каждой загрузке/удалении файла.
// Отметка «чек об оплате приложен»: бэкенд зеркалит число чеков в
// blockchain_data.proof_count при каждой загрузке/удалении файла. Для любого
// исходящего платежа после оплаты.
const proofState = (row: IPaymentRow): 'attached' | 'missing' | 'none' => {
if (!expenseProofRef(row)) return 'none';
if (!paymentProofHash(row)) return 'none';
const count = (row.blockchain_data as { proof_count?: number } | null)?.proof_count ?? 0;
return count > 0 ? 'attached' : 'missing';
};
@@ -6,3 +6,6 @@ export * as CreateInitialPayment from './createInitialPayment'
/** Создание объекта паевого платежа производится мутацией createDepositPayment. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера. */
export * as CreateDepositPayment from './createDepositPayment'
/** Приложить чек об оплате к платежу (ядровая «культура денег»: исходящие сопровождаем чеком). Привязка по payment_hash, файл в бакете gateway:files. */
export * as UploadPaymentProof from './uploadPaymentProof'
@@ -0,0 +1,19 @@
import { rawPaymentFileSelector } from '../../selectors/gateway/paymentFileSelector'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'uploadPaymentProof'
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'UploadPaymentProofInput!') }, rawPaymentFileSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['UploadPaymentProofInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -1,2 +1,8 @@
/** Получить список платежей с возможностью фильтрации по типу, статусу и направлению. */
export * as GetPayments from './getPayments'
/** Список чеков об оплате платежа (без read-URL — за ним отдельный запрос PaymentFile по id). */
export * as PaymentProofs from './paymentProofs'
/** Запись о файле платежа + свежий короткоживущий read-URL (по id). */
export * as PaymentFile from './paymentFile'
@@ -0,0 +1,19 @@
import { paymentFileSelector } from '../../selectors/gateway/paymentFileSelector'
import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'paymentFile'
export const query = Selector('Query')({
[name]: [{ id: $('id', 'Int!') }, paymentFileSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
id: number
}
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
@@ -0,0 +1,26 @@
import { rawPaymentFileSelector } from '../../selectors/gateway/paymentFileSelector'
import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'paymentProofs'
export const query = Selector('Query')({
[name]: [
{
coopname: $('coopname', 'String!'),
payment_hash: $('payment_hash', 'String!'),
},
rawPaymentFileSelector,
],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
coopname: string
payment_hash: string
}
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
@@ -1 +1,2 @@
export * from './paymentSelector'
export * from './paymentFileSelector'
@@ -0,0 +1,21 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
export const rawPaymentFileSelector = {
id: true,
coopname: true,
payment_hash: true,
kind: true,
storage_key: true,
mime_type: true,
size_bytes: true,
checksum_sha256: true,
original_filename: true,
read_url: true,
uploaded_at: true,
uploaded_by_username: true,
}
const _validate: MakeAllFieldsRequired<ValueTypes['PaymentFile']> = rawPaymentFileSelector
export const paymentFileSelector = Selector('PaymentFile')(rawPaymentFileSelector)
+30
View File
@@ -1370,6 +1370,9 @@ export const AllTypesProps: Record<string,any> = {
uploadExpenseFile:{
data:"UploadExpenseFileInput"
},
uploadPaymentProof:{
data:"UploadPaymentProofInput"
},
verifyEmail:{
data:"VerifyEmailInputDTO"
},
@@ -1427,6 +1430,7 @@ export const AllTypesProps: Record<string,any> = {
},
PaymentDirection: "enum" as const,
PaymentFileKind: "enum" as const,
PaymentFiltersInput:{
direction:"PaymentDirection",
status:"PaymentStatus",
@@ -1821,6 +1825,12 @@ export const AllTypesProps: Record<string,any> = {
},
onecoopGetDocuments:{
data:"GetOneCoopDocumentsInput"
},
paymentFile:{
},
paymentProofs:{
},
process:{
@@ -2120,6 +2130,9 @@ export const AllTypesProps: Record<string,any> = {
},
UploadExpenseFileInput:{
kind:"ExpenseFileKind"
},
UploadPaymentProofInput:{
},
UserStatus: "enum" as const,
VarsInput:{
@@ -4060,6 +4073,7 @@ export const ReturnTypes: Record<string,any> = {
updateSettings:"Settings",
updateSystem:"SystemInfo",
uploadExpenseFile:"ExpenseFile",
uploadPaymentProof:"PaymentFile",
verifyEmail:"Boolean",
voteOnAnnualGeneralMeet:"MeetAggregate",
walmoveWallets:"Ledger2AdjustmentResult"
@@ -4341,6 +4355,20 @@ export const ReturnTypes: Record<string,any> = {
fee_percent:"Float",
tolerance_percent:"Float"
},
PaymentFile:{
checksum_sha256:"String",
coopname:"String",
id:"Int",
kind:"PaymentFileKind",
mime_type:"String",
original_filename:"String",
payment_hash:"String",
read_url:"String",
size_bytes:"Int",
storage_key:"String",
uploaded_at:"DateTime",
uploaded_by_username:"String"
},
PaymentMethod:{
created_at:"DateTime",
data:"PaymentMethodData",
@@ -4656,6 +4684,8 @@ export const ReturnTypes: Record<string,any> = {
membershipExit:"MembershipExit",
membershipExitReturnPreview:"MembershipExitReturnPreview",
onecoopGetDocuments:"OneCoopDocumentsResponse",
paymentFile:"PaymentFile",
paymentProofs:"PaymentFile",
process:"ProcessView",
processes:"ProcessSummaryPaginationResult",
searchDocuments:"SearchResult",
+222 -2
View File
@@ -7227,6 +7227,7 @@ updateRequest?: [{ data: ValueTypes["UpdateRequestInput"] | Variable<any, string
updateSettings?: [{ data: ValueTypes["UpdateSettingsInput"] | Variable<any, string>},ValueTypes["Settings"]],
updateSystem?: [{ data: ValueTypes["Update"] | Variable<any, string>},ValueTypes["SystemInfo"]],
uploadExpenseFile?: [{ data: ValueTypes["UploadExpenseFileInput"] | Variable<any, string>},ValueTypes["ExpenseFile"]],
uploadPaymentProof?: [{ data: ValueTypes["UploadPaymentProofInput"] | Variable<any, string>},ValueTypes["PaymentFile"]],
verifyEmail?: [{ data: ValueTypes["VerifyEmailInputDTO"] | Variable<any, string>},boolean | `@${string}`],
voteOnAnnualGeneralMeet?: [{ data: ValueTypes["VoteOnAnnualGeneralMeetInput"] | Variable<any, string>},ValueTypes["MeetAggregate"]],
walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},ValueTypes["Ledger2AdjustmentResult"]],
@@ -7964,6 +7965,37 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},V
}>;
/** Направление платежа */
["PaymentDirection"]:PaymentDirection;
/** Запись о файле, приложенном к платежу (чек об оплате). */
["PaymentFile"]: AliasType<{
/** SHA-256 содержимого, hex-lowercase. */
checksum_sha256?:boolean | `@${string}`,
/** Имя кооператива (scope). */
coopname?:boolean | `@${string}`,
/** Внутренний ID записи. */
id?:boolean | `@${string}`,
/** Назначение файла. */
kind?:boolean | `@${string}`,
/** MIME-тип содержимого. */
mime_type?:boolean | `@${string}`,
/** Оригинальное имя загруженного файла. */
original_filename?:boolean | `@${string}`,
/** Хеш платежа. */
payment_hash?:boolean | `@${string}`,
/** Короткоживущий URL на скачивание (HMAC-signed). */
read_url?:boolean | `@${string}`,
/** Размер файла в байтах. */
size_bytes?:boolean | `@${string}`,
/** MinIO-ключ внутри бакета. */
storage_key?:boolean | `@${string}`,
/** Когда загружено. */
uploaded_at?:boolean | `@${string}`,
/** Кто загрузил (username). */
uploaded_by_username?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on PaymentFile']?: Omit<ValueTypes["PaymentFile"], "...on PaymentFile">
}>;
/** Тип файла, приложенного к платежу. */
["PaymentFileKind"]:PaymentFileKind;
["PaymentFiltersInput"]: {
/** Название кооператива */
coopname?: string | undefined | null | Variable<any, string>,
@@ -8689,6 +8721,8 @@ listReportDrafts?: [{ filter?: ValueTypes["ListReportDraftsFilterInput"] | undef
membershipExit?: [{ coopname: string | Variable<any, string>, username: string | Variable<any, string>},ValueTypes["MembershipExit"]],
membershipExitReturnPreview?: [{ coopname: string | Variable<any, string>, username: string | Variable<any, string>},ValueTypes["MembershipExitReturnPreview"]],
onecoopGetDocuments?: [{ data: ValueTypes["GetOneCoopDocumentsInput"] | Variable<any, string>},ValueTypes["OneCoopDocumentsResponse"]],
paymentFile?: [{ id: number | Variable<any, string>},ValueTypes["PaymentFile"]],
paymentProofs?: [{ coopname: string | Variable<any, string>, payment_hash: string | Variable<any, string>},ValueTypes["PaymentFile"]],
process?: [{ coopname: string | Variable<any, string>, hash: string | Variable<any, string>},ValueTypes["ProcessView"]],
processes?: [{ filter: ValueTypes["ProcessesFilter"] | Variable<any, string>, pagination: ValueTypes["PaginationInput"] | Variable<any, string>},ValueTypes["ProcessSummaryPaginationResult"]],
searchDocuments?: [{ data: ValueTypes["SearchDocumentsInput"] | Variable<any, string>},ValueTypes["SearchResult"]],
@@ -10214,6 +10248,22 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
proposal_hash: string | Variable<any, string>,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number | Variable<any, string>
};
["UploadPaymentProofInput"]: {
/** SHA-256 содержимого, hex-lowercase (64 hex-символа). */
checksum_sha256: string | Variable<any, string>,
/** Содержимое файла, base64 без префикса data:. */
content_base64: string | Variable<any, string>,
/** Имя кооператива. */
coopname: string | Variable<any, string>,
/** MIME-тип содержимого. */
mime_type: string | Variable<any, string>,
/** Оригинальное имя файла — для отображения и поиска. */
original_filename?: string | undefined | null | Variable<any, string>,
/** Хеш платежа, к которому прикладывается чек. */
payment_hash: string | Variable<any, string>,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number | Variable<any, string>
};
["UserAccount"]: AliasType<{
/** Метаинформация */
@@ -16411,6 +16461,7 @@ updateRequest?: [{ data: ResolverInputTypes["UpdateRequestInput"]},ResolverInput
updateSettings?: [{ data: ResolverInputTypes["UpdateSettingsInput"]},ResolverInputTypes["Settings"]],
updateSystem?: [{ data: ResolverInputTypes["Update"]},ResolverInputTypes["SystemInfo"]],
uploadExpenseFile?: [{ data: ResolverInputTypes["UploadExpenseFileInput"]},ResolverInputTypes["ExpenseFile"]],
uploadPaymentProof?: [{ data: ResolverInputTypes["UploadPaymentProofInput"]},ResolverInputTypes["PaymentFile"]],
verifyEmail?: [{ data: ResolverInputTypes["VerifyEmailInputDTO"]},boolean | `@${string}`],
voteOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["VoteOnAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]],
walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputTypes["Ledger2AdjustmentResult"]],
@@ -17109,6 +17160,36 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType
}>;
/** Направление платежа */
["PaymentDirection"]:PaymentDirection;
/** Запись о файле, приложенном к платежу (чек об оплате). */
["PaymentFile"]: AliasType<{
/** SHA-256 содержимого, hex-lowercase. */
checksum_sha256?:boolean | `@${string}`,
/** Имя кооператива (scope). */
coopname?:boolean | `@${string}`,
/** Внутренний ID записи. */
id?:boolean | `@${string}`,
/** Назначение файла. */
kind?:boolean | `@${string}`,
/** MIME-тип содержимого. */
mime_type?:boolean | `@${string}`,
/** Оригинальное имя загруженного файла. */
original_filename?:boolean | `@${string}`,
/** Хеш платежа. */
payment_hash?:boolean | `@${string}`,
/** Короткоживущий URL на скачивание (HMAC-signed). */
read_url?:boolean | `@${string}`,
/** Размер файла в байтах. */
size_bytes?:boolean | `@${string}`,
/** MinIO-ключ внутри бакета. */
storage_key?:boolean | `@${string}`,
/** Когда загружено. */
uploaded_at?:boolean | `@${string}`,
/** Кто загрузил (username). */
uploaded_by_username?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
/** Тип файла, приложенного к платежу. */
["PaymentFileKind"]:PaymentFileKind;
["PaymentFiltersInput"]: {
/** Название кооператива */
coopname?: string | undefined | null,
@@ -17812,6 +17893,8 @@ listReportDrafts?: [{ filter?: ResolverInputTypes["ListReportDraftsFilterInput"]
membershipExit?: [{ coopname: string, username: string},ResolverInputTypes["MembershipExit"]],
membershipExitReturnPreview?: [{ coopname: string, username: string},ResolverInputTypes["MembershipExitReturnPreview"]],
onecoopGetDocuments?: [{ data: ResolverInputTypes["GetOneCoopDocumentsInput"]},ResolverInputTypes["OneCoopDocumentsResponse"]],
paymentFile?: [{ id: number},ResolverInputTypes["PaymentFile"]],
paymentProofs?: [{ coopname: string, payment_hash: string},ResolverInputTypes["PaymentFile"]],
process?: [{ coopname: string, hash: string},ResolverInputTypes["ProcessView"]],
processes?: [{ filter: ResolverInputTypes["ProcessesFilter"], pagination: ResolverInputTypes["PaginationInput"]},ResolverInputTypes["ProcessSummaryPaginationResult"]],
searchDocuments?: [{ data: ResolverInputTypes["SearchDocumentsInput"]},ResolverInputTypes["SearchResult"]],
@@ -19298,6 +19381,22 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
proposal_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UploadPaymentProofInput"]: {
/** SHA-256 содержимого, hex-lowercase (64 hex-символа). */
checksum_sha256: string,
/** Содержимое файла, base64 без префикса data:. */
content_base64: string,
/** Имя кооператива. */
coopname: string,
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя файла — для отображения и поиска. */
original_filename?: string | undefined | null,
/** Хеш платежа, к которому прикладывается чек. */
payment_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UserAccount"]: AliasType<{
/** Метаинформация */
@@ -25843,6 +25942,10 @@ export type ModelTypes = {
Требуемые роли: chairman, member, user. */
uploadExpenseFile: ModelTypes["ExpenseFile"],
/** Приложить чек об оплате к платежу (бакет gateway:files).
Требуемые роли: chairman, member. */
uploadPaymentProof: ModelTypes["PaymentFile"],
/** Подтвердить email адрес пользователя */
verifyEmail: boolean,
/** Голосование на общем собрании пайщиков
@@ -26503,6 +26606,34 @@ export type ModelTypes = {
tolerance_percent: number
};
["PaymentDirection"]:PaymentDirection;
/** Запись о файле, приложенном к платежу (чек об оплате). */
["PaymentFile"]: {
/** SHA-256 содержимого, hex-lowercase. */
checksum_sha256: string,
/** Имя кооператива (scope). */
coopname: string,
/** Внутренний ID записи. */
id: number,
/** Назначение файла. */
kind: ModelTypes["PaymentFileKind"],
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя загруженного файла. */
original_filename?: string | undefined | null,
/** Хеш платежа. */
payment_hash: string,
/** Короткоживущий URL на скачивание (HMAC-signed). */
read_url?: string | undefined | null,
/** Размер файла в байтах. */
size_bytes: number,
/** MinIO-ключ внутри бакета. */
storage_key: string,
/** Когда загружено. */
uploaded_at: ModelTypes["DateTime"],
/** Кто загрузил (username). */
uploaded_by_username: string
};
["PaymentFileKind"]:PaymentFileKind;
["PaymentFiltersInput"]: {
/** Название кооператива */
coopname?: string | undefined | null,
@@ -27092,7 +27223,7 @@ export type ModelTypes = {
capitalResults: ModelTypes["PaginatedCapitalResultsPaginationResult"],
/** Получение одного сегмента кооператива по фильтрам */
capitalSegment?: ModelTypes["CapitalSegment"] | undefined | null,
/** Получение списка сементов кооператива с фильтрацией и пагинацией */
/** Получение списка сегментов кооператива с фильтрацией и пагинацией */
capitalSegments: ModelTypes["PaginatedCapitalSegmentsPaginationResult"],
/** Получение полного состояния CAPITAL контракта кооператива */
capitalState?: ModelTypes["CapitalState"] | undefined | null,
@@ -27394,6 +27525,14 @@ export type ModelTypes = {
membershipExitReturnPreview: ModelTypes["MembershipExitReturnPreview"],
/** Получение документов кооператива для синхронизации с 1С. Требует секретный ключ в заголовке x-onecoop-secret-key. */
onecoopGetDocuments: ModelTypes["OneCoopDocumentsResponse"],
/** Получить запись о файле платежа + свежий короткоживущий read-URL.
Требуемые роли: chairman, member, user. */
paymentFile: ModelTypes["PaymentFile"],
/** Список чеков об оплате платежа (без read-URL запрос отдельно по id).
Требуемые роли: chairman, member, user. */
paymentProofs: Array<ModelTypes["PaymentFile"]>,
/** Получить полную картину процесса ledger2 по process_hash
Требуемые роли: chairman, member. */
@@ -28847,6 +28986,22 @@ export type ModelTypes = {
proposal_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UploadPaymentProofInput"]: {
/** SHA-256 содержимого, hex-lowercase (64 hex-символа). */
checksum_sha256: string,
/** Содержимое файла, base64 без префикса data:. */
content_base64: string,
/** Имя кооператива. */
coopname: string,
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя файла — для отображения и поиска. */
original_filename?: string | undefined | null,
/** Хеш платежа, к которому прикладывается чек. */
payment_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UserAccount"]: {
/** Метаинформация */
@@ -35703,6 +35858,10 @@ export type GraphQLTypes = {
Требуемые роли: chairman, member, user. */
uploadExpenseFile: GraphQLTypes["ExpenseFile"],
/** Приложить чек об оплате к платежу (бакет gateway:files).
Требуемые роли: chairman, member. */
uploadPaymentProof: GraphQLTypes["PaymentFile"],
/** Подтвердить email адрес пользователя */
verifyEmail: boolean,
/** Голосование на общем собрании пайщиков
@@ -36446,6 +36605,37 @@ export type GraphQLTypes = {
};
/** Направление платежа */
["PaymentDirection"]: PaymentDirection;
/** Запись о файле, приложенном к платежу (чек об оплате). */
["PaymentFile"]: {
__typename: "PaymentFile",
/** SHA-256 содержимого, hex-lowercase. */
checksum_sha256: string,
/** Имя кооператива (scope). */
coopname: string,
/** Внутренний ID записи. */
id: number,
/** Назначение файла. */
kind: GraphQLTypes["PaymentFileKind"],
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя загруженного файла. */
original_filename?: string | undefined | null,
/** Хеш платежа. */
payment_hash: string,
/** Короткоживущий URL на скачивание (HMAC-signed). */
read_url?: string | undefined | null,
/** Размер файла в байтах. */
size_bytes: number,
/** MinIO-ключ внутри бакета. */
storage_key: string,
/** Когда загружено. */
uploaded_at: GraphQLTypes["DateTime"],
/** Кто загрузил (username). */
uploaded_by_username: string,
['...on PaymentFile']: Omit<GraphQLTypes["PaymentFile"], "...on PaymentFile">
};
/** Тип файла, приложенного к платежу. */
["PaymentFileKind"]: PaymentFileKind;
["PaymentFiltersInput"]: {
/** Название кооператива */
coopname?: string | undefined | null,
@@ -37098,7 +37288,7 @@ export type GraphQLTypes = {
capitalResults: GraphQLTypes["PaginatedCapitalResultsPaginationResult"],
/** Получение одного сегмента кооператива по фильтрам */
capitalSegment?: GraphQLTypes["CapitalSegment"] | undefined | null,
/** Получение списка сементов кооператива с фильтрацией и пагинацией */
/** Получение списка сегментов кооператива с фильтрацией и пагинацией */
capitalSegments: GraphQLTypes["PaginatedCapitalSegmentsPaginationResult"],
/** Получение полного состояния CAPITAL контракта кооператива */
capitalState?: GraphQLTypes["CapitalState"] | undefined | null,
@@ -37400,6 +37590,14 @@ export type GraphQLTypes = {
membershipExitReturnPreview: GraphQLTypes["MembershipExitReturnPreview"],
/** Получение документов кооператива для синхронизации с 1С. Требует секретный ключ в заголовке x-onecoop-secret-key. */
onecoopGetDocuments: GraphQLTypes["OneCoopDocumentsResponse"],
/** Получить запись о файле платежа + свежий короткоживущий read-URL.
Требуемые роли: chairman, member, user. */
paymentFile: GraphQLTypes["PaymentFile"],
/** Список чеков об оплате платежа (без read-URL запрос отдельно по id).
Требуемые роли: chairman, member, user. */
paymentProofs: Array<GraphQLTypes["PaymentFile"]>,
/** Получить полную картину процесса ledger2 по process_hash
Требуемые роли: chairman, member. */
@@ -38937,6 +39135,22 @@ export type GraphQLTypes = {
proposal_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UploadPaymentProofInput"]: {
/** SHA-256 содержимого, hex-lowercase (64 hex-символа). */
checksum_sha256: string,
/** Содержимое файла, base64 без префикса data:. */
content_base64: string,
/** Имя кооператива. */
coopname: string,
/** MIME-тип содержимого. */
mime_type: string,
/** Оригинальное имя файла — для отображения и поиска. */
original_filename?: string | undefined | null,
/** Хеш платежа, к которому прикладывается чек. */
payment_hash: string,
/** Размер файла в байтах (для серверной валидации). */
size_bytes: number
};
["UserAccount"]: {
__typename: "UserAccount",
@@ -39476,6 +39690,10 @@ export enum PaymentDirection {
INCOMING = "INCOMING",
OUTGOING = "OUTGOING"
}
/** Тип файла, приложенного к платежу. */
export enum PaymentFileKind {
PAYMENT_PROOF = "PAYMENT_PROOF"
}
/** Статус платежа */
export enum PaymentStatus {
AWAITING_AUTHORIZATION = "AWAITING_AUTHORIZATION",
@@ -39885,6 +40103,7 @@ type ZEUS_VARIABLES = {
["PassportInput"]: ValueTypes["PassportInput"];
["PayExpenseItemInput"]: ValueTypes["PayExpenseItemInput"];
["PaymentDirection"]: ValueTypes["PaymentDirection"];
["PaymentFileKind"]: ValueTypes["PaymentFileKind"];
["PaymentFiltersInput"]: ValueTypes["PaymentFiltersInput"];
["PaymentStatus"]: ValueTypes["PaymentStatus"];
["PaymentType"]: ValueTypes["PaymentType"];
@@ -40000,6 +40219,7 @@ type ZEUS_VARIABLES = {
["UpdateSettingsInput"]: ValueTypes["UpdateSettingsInput"];
["UpdateStoryInput"]: ValueTypes["UpdateStoryInput"];
["UploadExpenseFileInput"]: ValueTypes["UploadExpenseFileInput"];
["UploadPaymentProofInput"]: ValueTypes["UploadPaymentProofInput"];
["UserStatus"]: ValueTypes["UserStatus"];
["VarsInput"]: ValueTypes["VarsInput"];
["VerifyEmailInputDTO"]: ValueTypes["VerifyEmailInputDTO"];