feat(gateway): ядровый «чек об оплате» исходящих платежей по payment_hash
Культура денег: входящие — подтверждением, исходящие — чеком. Единый механизм на уровне ядра (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:
@@ -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: 'Чек об оплате — подтверждающий документ по исполненному платежу.' },
|
||||
},
|
||||
});
|
||||
+24
@@ -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');
|
||||
+50
@@ -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;
|
||||
}
|
||||
+73
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"];
|
||||
|
||||
+8
-87
@@ -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';
|
||||
+199
@@ -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>
|
||||
+32
-12
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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"];
|
||||
|
||||
Reference in New Issue
Block a user