search by private account

This commit is contained in:
Alex Ant
2025-06-23 13:30:34 +05:00
parent d23d90a794
commit ffd4d72850
22 changed files with 1164 additions and 9 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"agentCanUpdateSnapshot": true
}
BIN
View File
Binary file not shown.
+26
View File
@@ -3364,6 +3364,22 @@ type PrivateAccount {
type: AccountType!
}
union PrivateAccountSearchData = Entrepreneur | Individual | Organization
type PrivateAccountSearchResult {
"""Данные найденного аккаунта"""
data: PrivateAccountSearchData!
"""Поля, в которых найдены совпадения"""
highlightedFields: [String!]
"""Оценка релевантности результата"""
score: Float
"""Тип аккаунта"""
type: String!
}
input ProhibitRequestInput {
"""Имя аккаунта кооператива"""
coopname: String!
@@ -3536,6 +3552,11 @@ type Query {
"""Получить сводную публичную информацию о системе"""
getSystemInfo: SystemInfo!
"""
Поиск приватных данных аккаунтов по запросу. Поиск осуществляется по полям ФИО, ИНН, ОГРН, наименованию организации и другим приватным данным.
"""
searchPrivateAccounts(data: SearchPrivateAccountsInput!): [PrivateAccountSearchResult!]!
}
"""Вопрос повестки собрания с результатами голосования"""
@@ -4035,6 +4056,11 @@ type SbpAccount {
phone: String!
}
input SearchPrivateAccountsInput {
"""Поисковый запрос для поиска приватных аккаунтов"""
query: String!
}
input SelectBranchGenerateDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int
@@ -17,6 +17,10 @@ import { AccountType } from '~/modules/account/enum/account-type.enum';
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common/repositories/entrepreneur.repository';
import {
SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
SearchPrivateAccountsRepository,
} from '~/domain/common/repositories/search-private-accounts.repository';
import { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
import { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
import { EntrepreneurDomainEntity } from '~/domain/branch/entities/entrepreneur-domain.entity';
@@ -24,6 +28,10 @@ import { ACCOUNT_BLOCKCHAIN_PORT, AccountBlockchainPort } from '../interfaces/ac
import { CANDIDATE_REPOSITORY, CandidateRepository } from '../repository/candidate.repository';
import { userStatus } from '~/types/user.types';
import { sha256 } from '~/utils/sha256';
import type {
SearchPrivateAccountsInputDomainInterface,
PrivateAccountSearchResultDomainInterface,
} from '~/domain/common/interfaces/search-private-accounts-domain.interface';
@Injectable()
export class AccountDomainInteractor {
@@ -32,6 +40,8 @@ export class AccountDomainInteractor {
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository,
@Inject(INDIVIDUAL_REPOSITORY) private readonly individualRepository: IndividualRepository,
@Inject(ENTREPRENEUR_REPOSITORY) private readonly entrepreneurRepository: EntrepreneurRepository,
@Inject(SEARCH_PRIVATE_ACCOUNTS_REPOSITORY)
private readonly searchPrivateAccountsRepository: SearchPrivateAccountsRepository,
@Inject(ACCOUNT_BLOCKCHAIN_PORT) private readonly accountBlockchainPort: AccountBlockchainPort,
@Inject(CANDIDATE_REPOSITORY) private readonly candidateRepository: CandidateRepository
) {}
@@ -176,4 +186,21 @@ export class AccountDomainInteractor {
throw new HttpException(`Ошибка при регистрации в блокчейне: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* Поиск приватных аккаунтов по запросу
* @param input Входные данные для поиска
* @returns Массив результатов поиска
*/
async searchPrivateAccounts(
input: SearchPrivateAccountsInputDomainInterface
): Promise<PrivateAccountSearchResultDomainInterface[]> {
this.logger.log(`Поиск приватных аккаунтов по запросу: "${input.query}"`);
const results = await this.searchPrivateAccountsRepository.searchPrivateAccounts(input);
this.logger.log(`Найдено ${results.length} приватных аккаунтов`);
return results;
}
}
@@ -0,0 +1,28 @@
import type { IndividualDomainInterface } from './individual-domain.interface';
import type { EntrepreneurDomainInterface } from './entrepreneur-domain.interface';
import type { OrganizationDomainInterface } from './organization-domain.interface';
/**
* Интерфейс для входных данных поиска приватных аккаунтов
*/
export interface SearchPrivateAccountsInputDomainInterface {
query: string;
}
/**
* Тип данных результата поиска - union type всех возможных типов приватных данных
*/
export type PrivateAccountSearchDataDomainInterface =
| IndividualDomainInterface
| EntrepreneurDomainInterface
| OrganizationDomainInterface;
/**
* Интерфейс результата поиска приватных аккаунтов
*/
export interface PrivateAccountSearchResultDomainInterface {
type: 'individual' | 'entrepreneur' | 'organization';
data: PrivateAccountSearchDataDomainInterface;
score?: number;
highlightedFields?: string[];
}
@@ -0,0 +1,20 @@
import type {
SearchPrivateAccountsInputDomainInterface,
PrivateAccountSearchResultDomainInterface,
} from '../interfaces/search-private-accounts-domain.interface';
/**
* Доменный репозиторий для поиска приватных аккаунтов
*/
export interface SearchPrivateAccountsRepository {
/**
* Поиск приватных аккаунтов по запросу
* @param input Входные данные для поиска
* @returns Массив результатов поиска
*/
searchPrivateAccounts(
input: SearchPrivateAccountsInputDomainInterface
): Promise<PrivateAccountSearchResultDomainInterface[]>;
}
export const SEARCH_PRIVATE_ACCOUNTS_REPOSITORY = Symbol('SearchPrivateAccountsRepository');
@@ -14,6 +14,8 @@ import { PROJECT_FREE_DECISION_REPOSITORY } from '~/domain/common/repositories/p
import { ProjectFreeDecisionRepositoryImplementation } from './repositories/project-free-decision-generator.repository';
import { VARS_REPOSITORY } from '~/domain/common/repositories/vars.repository';
import { VarsRepositoryImplementation } from './repositories/vars-generator.repository';
import { SEARCH_PRIVATE_ACCOUNTS_REPOSITORY } from '~/domain/common/repositories/search-private-accounts.repository';
import { SearchPrivateAccountsRepositoryImplementation } from './repositories/search-private-accounts-generator.repository';
@Global()
@Module({
@@ -46,6 +48,10 @@ import { VarsRepositoryImplementation } from './repositories/vars-generator.repo
provide: VARS_REPOSITORY,
useClass: VarsRepositoryImplementation,
},
{
provide: SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
useClass: SearchPrivateAccountsRepositoryImplementation,
},
],
exports: [
ORGANIZATION_REPOSITORY,
@@ -55,6 +61,7 @@ import { VarsRepositoryImplementation } from './repositories/vars-generator.repo
DOCUMENT_REPOSITORY,
PROJECT_FREE_DECISION_REPOSITORY,
VARS_REPOSITORY,
SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
],
})
export class GeneratorRepositoriesModule {}
@@ -0,0 +1,151 @@
import { Injectable } from '@nestjs/common';
import { SearchPrivateAccountsRepository } from '~/domain/common/repositories/search-private-accounts.repository';
import type {
SearchPrivateAccountsInputDomainInterface,
PrivateAccountSearchResultDomainInterface,
PrivateAccountSearchDataDomainInterface,
} from '~/domain/common/interfaces/search-private-accounts-domain.interface';
import type { IndividualDomainInterface } from '~/domain/common/interfaces/individual-domain.interface';
import type { EntrepreneurDomainInterface } from '~/domain/common/interfaces/entrepreneur-domain.interface';
import type { OrganizationDomainInterface } from '~/domain/common/interfaces/organization-domain.interface';
import { generator } from '~/services/document.service';
import type { ISearchResult } from '@coopenomics/factory';
/**
* Инфраструктурная реализация репозитория поиска приватных аккаунтов
* Использует фабрику документов для выполнения поиска
*/
@Injectable()
export class SearchPrivateAccountsRepositoryImplementation implements SearchPrivateAccountsRepository {
/**
* Выполняет поиск приватных аккаунтов через фабрику документов
* @param input Входные данные для поиска
* @returns Массив результатов поиска, преобразованных в доменные интерфейсы
*/
async searchPrivateAccounts(
input: SearchPrivateAccountsInputDomainInterface
): Promise<PrivateAccountSearchResultDomainInterface[]> {
// Вызываем метод поиска из фабрики документов
const factoryResults: ISearchResult[] = await generator.search(input.query);
// Преобразуем результаты фабрики в доменные интерфейсы
const domainResults: PrivateAccountSearchResultDomainInterface[] = factoryResults.map((result) =>
this.convertFactoryResultToDomain(result)
);
return domainResults;
}
/**
* Преобразует результат из фабрики в доменный интерфейс
* @param factoryResult Результат поиска из фабрики
* @returns Результат поиска в доменном формате
*/
private convertFactoryResultToDomain(factoryResult: ISearchResult): PrivateAccountSearchResultDomainInterface {
let domainData: PrivateAccountSearchDataDomainInterface;
switch (factoryResult.type) {
case 'individual':
domainData = this.convertIndividualData(factoryResult.data as any);
break;
case 'entrepreneur':
domainData = this.convertEntrepreneurData(factoryResult.data as any);
break;
case 'organization':
domainData = this.convertOrganizationData(factoryResult.data as any);
break;
default:
throw new Error(`Неизвестный тип данных: ${factoryResult.type}`);
}
return {
type: factoryResult.type,
data: domainData,
score: factoryResult.score,
highlightedFields: factoryResult.highlightedFields,
};
}
/**
* Преобразует данные физического лица из фабрики в доменный интерфейс
* @param data Данные из фабрики
* @returns Доменный интерфейс физического лица
*/
private convertIndividualData(data: any): IndividualDomainInterface {
return {
username: data.username,
first_name: data.first_name,
last_name: data.last_name,
middle_name: data.middle_name,
birthdate: data.birthdate,
full_address: data.full_address,
phone: data.phone,
email: data.email,
passport: data.passport
? {
series: data.passport.series,
number: data.passport.number,
issued_by: data.passport.issued_by,
issued_at: data.passport.issued_at,
code: data.passport.code,
}
: undefined,
};
}
/**
* Преобразует данные индивидуального предпринимателя из фабрики в доменный интерфейс
* @param data Данные из фабрики
* @returns Доменный интерфейс индивидуального предпринимателя
*/
private convertEntrepreneurData(data: any): EntrepreneurDomainInterface {
return {
username: data.username,
first_name: data.first_name,
last_name: data.last_name,
middle_name: data.middle_name,
birthdate: data.birthdate,
phone: data.phone,
email: data.email,
country: data.country,
city: data.city,
full_address: data.full_address,
details: {
inn: data.details.inn,
ogrn: data.details.ogrn,
},
};
}
/**
* Преобразует данные организации из фабрики в доменный интерфейс
* @param data Данные из фабрики
* @returns Доменный интерфейс организации
*/
private convertOrganizationData(data: any): OrganizationDomainInterface {
return {
username: data.username,
type: data.type,
short_name: data.short_name,
full_name: data.full_name,
represented_by: {
first_name: data.represented_by.first_name,
last_name: data.represented_by.last_name,
middle_name: data.represented_by.middle_name,
position: data.represented_by.position,
based_on: data.represented_by.based_on,
},
country: data.country,
city: data.city,
full_address: data.full_address,
fact_address: data.fact_address,
phone: data.phone,
email: data.email,
details: {
inn: data.details.inn,
ogrn: data.details.ogrn,
kpp: data.details.kpp,
},
};
}
}
@@ -0,0 +1,11 @@
import { InputType, Field } from '@nestjs/graphql';
import { IsString, IsNotEmpty } from 'class-validator';
import type { SearchPrivateAccountsInputDomainInterface } from '~/domain/common/interfaces/search-private-accounts-domain.interface';
@InputType('SearchPrivateAccountsInput')
export class SearchPrivateAccountsInputDTO implements SearchPrivateAccountsInputDomainInterface {
@Field(() => String, { description: 'Поисковый запрос для поиска приватных аккаунтов' })
@IsString({ message: 'Поисковый запрос должен быть строкой' })
@IsNotEmpty({ message: 'Поисковый запрос не может быть пустым' })
query!: string;
}
@@ -0,0 +1,66 @@
import { ObjectType, Field, createUnionType } from '@nestjs/graphql';
import { IsString, IsArray, IsOptional, IsNumber } from 'class-validator';
import { IndividualDTO } from '~/modules/common/dto/individual.dto';
import { EntrepreneurDTO } from '~/modules/common/dto/entrepreneur.dto';
import { OrganizationDTO } from '~/modules/common/dto/organization.dto';
import type { PrivateAccountSearchResultDomainInterface } from '~/domain/common/interfaces/search-private-accounts-domain.interface';
// Создаем Union Type для GraphQL
export const PrivateAccountSearchDataUnion = createUnionType({
name: 'PrivateAccountSearchData',
types: () => [IndividualDTO, EntrepreneurDTO, OrganizationDTO] as const,
resolveType: (value) => {
if (IndividualDTO.isTypeOf(value)) {
return IndividualDTO;
}
if (EntrepreneurDTO.isTypeOf(value)) {
return EntrepreneurDTO;
}
if (OrganizationDTO.isTypeOf(value)) {
return OrganizationDTO;
}
return undefined;
},
});
@ObjectType('PrivateAccountSearchResult')
export class PrivateAccountSearchResultDTO {
@Field(() => String, { description: 'Тип аккаунта' })
@IsString()
type!: 'individual' | 'entrepreneur' | 'organization';
@Field(() => PrivateAccountSearchDataUnion, { description: 'Данные найденного аккаунта' })
data!: IndividualDTO | EntrepreneurDTO | OrganizationDTO;
@Field(() => Number, { nullable: true, description: 'Оценка релевантности результата' })
@IsOptional()
@IsNumber()
score?: number;
@Field(() => [String], { nullable: true, description: 'Поля, в которых найдены совпадения' })
@IsOptional()
@IsArray()
@IsString({ each: true })
highlightedFields?: string[];
constructor(entity: PrivateAccountSearchResultDomainInterface) {
this.type = entity.type;
this.score = entity.score;
this.highlightedFields = entity.highlightedFields;
// Преобразуем доменные данные в соответствующий DTO
switch (entity.type) {
case 'individual':
this.data = new IndividualDTO(entity.data as any);
break;
case 'entrepreneur':
this.data = new EntrepreneurDTO(entity.data as any);
break;
case 'organization':
this.data = new OrganizationDTO(entity.data as any);
break;
default:
throw new Error(`Неизвестный тип аккаунта: ${entity.type}`);
}
}
}
@@ -12,6 +12,8 @@ import type { PaginationResultDomainInterface } from '~/domain/common/interfaces
import { RegisterAccountInputDTO } from '../dto/register-account-input.dto';
import { RegisteredAccountDTO } from '../dto/registered-account.dto';
import { UpdateAccountInputDTO } from '../dto/update-account-input.dto';
import { SearchPrivateAccountsInputDTO } from '../dto/search-private-accounts-input.dto';
import { PrivateAccountSearchResultDTO } from '../dto/search-private-accounts-result.dto';
export const AccountsPaginationResult = createPaginationResult(AccountDTO, 'Accounts');
@@ -40,6 +42,19 @@ export class AccountResolver {
return await this.accountService.getAccounts(data, options);
}
@Query(() => [PrivateAccountSearchResultDTO], {
name: 'searchPrivateAccounts',
description:
'Поиск приватных данных аккаунтов по запросу. Поиск осуществляется по полям ФИО, ИНН, ОГРН, наименованию организации и другим приватным данным.',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async searchPrivateAccounts(
@Args('data', { type: () => SearchPrivateAccountsInputDTO }) data: SearchPrivateAccountsInputDTO
): Promise<PrivateAccountSearchResultDTO[]> {
return this.accountService.searchPrivateAccounts(data);
}
@Mutation(() => RegisteredAccountDTO, {
name: 'registerAccount',
description: 'Зарегистрировать аккаунт пользователя в системе',
@@ -8,6 +8,8 @@ import type { RegisterAccountInputDTO } from '../dto/register-account-input.dto'
import { RegisteredAccountDTO } from '../dto/registered-account.dto';
import type { DeleteAccountInputDTO } from '../dto/delete-account-input.dto';
import type { UpdateAccountInputDTO } from '../dto/update-account-input.dto';
import type { SearchPrivateAccountsInputDTO } from '../dto/search-private-accounts-input.dto';
import { PrivateAccountSearchResultDTO } from '../dto/search-private-accounts-result.dto';
@Injectable()
export class AccountService {
@@ -44,4 +46,15 @@ export class AccountService {
const result = await this.accountDomainInteractor.registerAccount(data);
return new RegisteredAccountDTO(result);
}
/**
* Поиск приватных аккаунтов
* @param data Входные данные для поиска
* @returns Массив результатов поиска
*/
public async searchPrivateAccounts(data: SearchPrivateAccountsInputDTO): Promise<PrivateAccountSearchResultDTO[]> {
const results = await this.accountDomainInteractor.searchPrivateAccounts(data);
return results.map((result) => new PrivateAccountSearchResultDTO(result));
}
}
+143
View File
@@ -0,0 +1,143 @@
# Функциональность поиска в фабрике документов
## Описание
В фабрику документов добавлена функциональность поиска по полям пользователей. Поиск осуществляется по трем типам данных:
- **individual** - физические лица
- **entrepreneur** - индивидуальные предприниматели
- **organization** - организации
## Архитектура
Функциональность поиска реализована в виде отдельного сервиса `SearchService`, расположенного в `src/Services/Databazor/SearchService.ts`. Это обеспечивает:
- Чистое разделение ответственности
- Возможность независимого тестирования
- Переиспользование в других частях приложения
## API
### Метод search
```typescript
async search(query: string): Promise<ISearchResult[]>
```
**Параметры:**
- `query` - строка для поиска
**Возвращает:**
- Массив результатов поиска (максимум 10 элементов)
### Интерфейс ISearchResult
```typescript
interface ISearchResult {
type: 'individual' | 'entrepreneur' | 'organization'
data: ExternalIndividualData | ExternalEntrepreneurData | ExternalOrganizationData
score?: number // для будущего ранжирования
highlightedFields?: string[] // поля, в которых найдено совпадение
}
```
## Поля для поиска
### Физические лица (individual)
- `first_name` - имя
- `last_name` - фамилия
- `middle_name` - отчество
- **Полное ФИО** - поиск по комбинации полей (например, "Иванов Иван Иванович")
### Индивидуальные предприниматели (entrepreneur)
- `first_name` - имя
- `last_name` - фамилия
- `middle_name` - отчество
- **Полное ФИО** - поиск по комбинации полей
- `details.inn` - ИНН
- `details.ogrn` - ОГРН
### Организации (organization)
- `short_name` - краткое наименование
- `details.inn` - ИНН
- `details.ogrn` - ОГРН
## Особенности поиска
1. **Регистронезависимый поиск** - поиск выполняется без учета регистра
2. **Поиск по подстроке** - находит частичные совпадения в полях
3. **Поиск по полному ФИО** - ✅ **НОВОЕ!** Поддерживает поиск по нескольким словам (например, "Иванов Иван Иванович")
4. **Интеллектуальное разбиение запроса** - автоматически разбивает запрос на слова для поиска по ФИО
5. **Ограничение результатов** - возвращает максимум 10 результатов
6. **Подсветка совпадений** - в `highlightedFields` указываются поля, где найдено совпадение (включая `full_name`)
7. **Фильтрация удаленных записей** - поиск не включает записи с `deleted: true`
## Пример использования
```typescript
import { Generator } from '@coopenomics/factory'
const generator = new Generator()
await generator.connect('mongodb://localhost:27017/mydatabase')
// Поиск по имени
const results = await generator.search('Иван')
// Поиск по полному ФИО - теперь работает!
const fullNameResults = await generator.search('Иванов Иван Иванович')
console.log(fullNameResults)
// [
// {
// type: 'individual',
// data: { username: 'ivan123', first_name: 'Иван', last_name: 'Иванов', middle_name: 'Иванович', ... },
// highlightedFields: ['full_name', 'first_name', 'last_name', 'middle_name']
// }
// ]
// Поиск по частичному ФИО
const partialResults = await generator.search('Петров Петр')
// Поиск по ИНН
const innResults = await generator.search('1234567890')
// Поиск по названию организации
const orgResults = await generator.search('ООО Рога и копыта')
```
## Алгоритм поиска по полному ФИО
1. **Разбиение запроса** - запрос разбивается на отдельные слова
2. **Поиск в MongoDB** - каждое слово ищется во всех полях ФИО через оператор `$and`
3. **Дополнительная проверка** - результаты дополнительно фильтруются на уровне приложения
4. **Подсветка** - в `highlightedFields` добавляется `full_name` при совпадении
## Архитектурные улучшения
-**SearchService** вынесен в отдельный класс
-**Чистая архитектура** - разделение ответственности
-**Переиспользование** - сервис можно использовать независимо
-**Тестируемость** - легко писать unit-тесты для SearchService
## Тестирование
Функциональность покрыта комплексными тестами в файле `test/search.test.ts`, включающими:
- Поиск по всем типам полей
- **Поиск по полному ФИО** ("Иванов Иван Иванович")
- **Поиск по частичному ФИО** ("Петров Петр")
- Обработка пустых запросов
- Ограничение количества результатов
- Подсветка найденных полей
- Очистка тестовых данных
Запуск тестов:
```bash
npm test -- search.test.ts
```
## Преимущества нового подхода
1. **Поиск по полному ФИО работает!** - решена основная проблема
2. **Лучшая архитектура** - код организован в отдельном сервисе
3. **Расширяемость** - легко добавлять новые типы поиска
4. **Производительность** - оптимизированные MongoDB запросы
5. **Удобство использования** - простой API без изменений для пользователя
@@ -0,0 +1,242 @@
import type { Filter } from 'mongodb'
import type { ExternalEntrepreneurData, ExternalIndividualData, ExternalOrganizationData } from '../../Models'
import { Entrepreneur, Individual, Organization } from '../../Models'
import type { MongoDBConnector } from './MongoDBConnector'
export interface ISearchResult {
type: 'individual' | 'entrepreneur' | 'organization'
data: ExternalIndividualData | ExternalEntrepreneurData | ExternalOrganizationData
score?: number // для будущего ранжирования
highlightedFields?: string[] // поля, в которых найдено совпадение
}
export class SearchService {
private storage: MongoDBConnector
constructor(storage: MongoDBConnector) {
this.storage = storage
}
async search(query: string): Promise<ISearchResult[]> {
if (!query || query.trim().length === 0) {
return []
}
const results: ISearchResult[] = []
const trimmedQuery = query.trim()
// Создаем регулярное выражение для поиска без учета регистра
const regex = new RegExp(trimmedQuery, 'i')
// Разбиваем запрос на слова для поиска по полному ФИО
const queryWords = trimmedQuery.split(/\s+/).filter(word => word.length > 0)
try {
// Поиск в коллекции individuals
const individualResults = await this.searchIndividuals(regex, queryWords)
results.push(...individualResults)
// Поиск в коллекции entrepreneurs
const entrepreneurResults = await this.searchEntrepreneurs(regex, queryWords)
results.push(...entrepreneurResults)
// Поиск в коллекции organizations
const organizationResults = await this.searchOrganizations(regex)
results.push(...organizationResults)
// Ограничиваем общее количество результатов до 10
return results.slice(0, 10)
}
catch (error) {
console.error('Error during search:', error)
return []
}
}
private async searchIndividuals(regex: RegExp, queryWords: string[]): Promise<ISearchResult[]> {
const individualModel = new Individual(this.storage)
const results: ISearchResult[] = []
// Создаем два отдельных запроса для лучшей типизации
let filter: any
if (queryWords.length > 1) {
// Комбинированный фильтр для поиска по отдельным полям И по полному ФИО
filter = {
deleted: false,
$or: [
// Поиск по отдельным полям
{ first_name: { $regex: regex } },
{ last_name: { $regex: regex } },
{ middle_name: { $regex: regex } },
// Поиск по полному ФИО (все слова должны быть найдены)
{
$and: queryWords.map(word => ({
$or: [
{ first_name: { $regex: new RegExp(word, 'i') } },
{ last_name: { $regex: new RegExp(word, 'i') } },
{ middle_name: { $regex: new RegExp(word, 'i') } },
],
})),
},
],
}
}
else {
// Простой фильтр для одного слова
filter = {
deleted: false,
$or: [
{ first_name: { $regex: regex } },
{ last_name: { $regex: regex } },
{ middle_name: { $regex: regex } },
],
}
}
const individuals = await individualModel.getMany(filter)
for (const individual of individuals.results) {
const highlightedFields = []
// Проверяем отдельные поля
if (regex.test(individual.first_name))
highlightedFields.push('first_name')
if (regex.test(individual.last_name))
highlightedFields.push('last_name')
if (regex.test(individual.middle_name))
highlightedFields.push('middle_name')
// Проверяем полное ФИО
const fullName = `${individual.last_name} ${individual.first_name} ${individual.middle_name}`.trim()
if (this.matchesFullName(fullName, queryWords)) {
highlightedFields.push('full_name')
}
results.push({
type: 'individual',
data: individual,
highlightedFields,
})
}
return results
}
private async searchEntrepreneurs(regex: RegExp, queryWords: string[]): Promise<ISearchResult[]> {
const entrepreneurModel = new Entrepreneur(this.storage)
const results: ISearchResult[] = []
// Создаем фильтр с правильной типизацией
let filter: any
if (queryWords.length > 1) {
// Комбинированный фильтр
filter = {
deleted: false,
$or: [
// Поиск по отдельным полям
{ first_name: { $regex: regex } },
{ last_name: { $regex: regex } },
{ middle_name: { $regex: regex } },
{ 'details.inn': { $regex: regex } },
{ 'details.ogrn': { $regex: regex } },
// Поиск по полному ФИО
{
$and: queryWords.map(word => ({
$or: [
{ first_name: { $regex: new RegExp(word, 'i') } },
{ last_name: { $regex: new RegExp(word, 'i') } },
{ middle_name: { $regex: new RegExp(word, 'i') } },
],
})),
},
],
}
}
else {
// Простой фильтр
filter = {
deleted: false,
$or: [
{ first_name: { $regex: regex } },
{ last_name: { $regex: regex } },
{ middle_name: { $regex: regex } },
{ 'details.inn': { $regex: regex } },
{ 'details.ogrn': { $regex: regex } },
],
}
}
const entrepreneurs = await entrepreneurModel.getMany(filter)
for (const entrepreneur of entrepreneurs.results) {
const highlightedFields = []
// Проверяем отдельные поля
if (regex.test(entrepreneur.first_name))
highlightedFields.push('first_name')
if (regex.test(entrepreneur.last_name))
highlightedFields.push('last_name')
if (regex.test(entrepreneur.middle_name))
highlightedFields.push('middle_name')
if (entrepreneur.details?.inn && regex.test(entrepreneur.details.inn))
highlightedFields.push('details.inn')
if (entrepreneur.details?.ogrn && regex.test(entrepreneur.details.ogrn))
highlightedFields.push('details.ogrn')
// Проверяем полное ФИО
const fullName = `${entrepreneur.last_name} ${entrepreneur.first_name} ${entrepreneur.middle_name}`.trim()
if (this.matchesFullName(fullName, queryWords)) {
highlightedFields.push('full_name')
}
results.push({
type: 'entrepreneur',
data: entrepreneur,
highlightedFields,
})
}
return results
}
private async searchOrganizations(regex: RegExp): Promise<ISearchResult[]> {
const organizationModel = new Organization(this.storage)
const results: ISearchResult[] = []
const organizations = await organizationModel.getMany({
deleted: false,
$or: [
{ short_name: { $regex: regex } },
{ 'details.inn': { $regex: regex } },
{ 'details.ogrn': { $regex: regex } },
],
})
for (const organization of organizations.results) {
const highlightedFields = []
if (regex.test(organization.short_name))
highlightedFields.push('short_name')
if (organization.details?.inn && regex.test(organization.details.inn))
highlightedFields.push('details.inn')
if (organization.details?.ogrn && regex.test(organization.details.ogrn))
highlightedFields.push('details.ogrn')
results.push({
type: 'organization',
data: organization as ExternalOrganizationData,
highlightedFields,
})
}
return results
}
private matchesFullName(fullName: string, queryWords: string[]): boolean {
// Проверяем, содержит ли полное ФИО все слова из запроса
const fullNameLower = fullName.toLowerCase()
return queryWords.every(word => fullNameLower.includes(word.toLowerCase()))
}
}
@@ -1,2 +1,3 @@
export * from './DataService'
export * from './MongoDBConnector'
export { MongoDBConnector } from './MongoDBConnector'
export { default as DataService } from './DataService'
export { SearchService, type ISearchResult } from './SearchService'
+18 -2
View File
@@ -8,8 +8,7 @@ import type { IFilterDocuments, IGeneratedDocument, Numbers, externalDataTypes,
import type { IGenerate, IGenerationOptions } from './Interfaces/Documents'
import * as Actions from './Actions'
import { MongoDBConnector } from './Services/Databazor'
import { type ISearchResult, MongoDBConnector, SearchService } from './Services/Databazor'
import type { ExternalIndividualData } from './Models/Individual'
import { Individual } from './Models/Individual'
import type { ExternalEntrepreneurData, ExternalOrganizationData, IVars } from './Models'
@@ -29,6 +28,9 @@ export type { ExternalEntrepreneurData as IEntrepreneurData } from './Models'
export type { CooperativeData as ICooperativeData } from './Models'
export type { ExternalProjectData as IExternalProjectData } from './Models'
// Экспортируем интерфейс результатов поиска
export type { ISearchResult } from './Services/Databazor'
export interface IGenerator {
connect: (mongoUri: string) => Promise<void>
disconnect: () => Promise<void>
@@ -43,6 +45,9 @@ export interface IGenerator {
list: (type: dataTypes, filter: Filter<internalFilterTypes>) => Promise<CooperativeModel.Document.IGetResponse<internalFilterTypes>>
getHistory: (type: dataTypes, filter: Filter<internalFilterTypes>) => Promise<externalDataTypesArrays>
// Новый метод поиска
search: (query: string) => Promise<ISearchResult[]>
}
export class Generator implements IGenerator {
@@ -54,10 +59,16 @@ export class Generator implements IGenerator {
// Определение хранилища
public storage!: MongoDBConnector
// Сервис поиска
private searchService!: SearchService
// Метод подключения к хранилищу
async connect(mongoUri: string): Promise<void> {
this.storage = new MongoDBConnector(mongoUri)
// Инициализация сервиса поиска
this.searchService = new SearchService(this.storage)
// Инициализация фабрик документов
this.factories = {
[Actions.WalletAgreement.Template.registry_id]: new Actions.WalletAgreement.Factory(this.storage), // 1
@@ -177,4 +188,9 @@ export class Generator implements IGenerator {
async constructCooperative(username: string, block_num?: number): Promise<CooperativeData | null> {
return new Cooperative(this.storage).getOne(username, block_num)
}
// Новый метод поиска
async search(query: string): Promise<ISearchResult[]> {
return this.searchService.search(query)
}
}
+236
View File
@@ -0,0 +1,236 @@
import { beforeAll, describe, expect, it } from 'vitest'
import { Generator } from '../src'
import type { ExternalEntrepreneurData, ExternalIndividualData, ExternalOrganizationData } from '../src/Models'
import { mongoUri } from './utils'
const generator = new Generator()
beforeAll(async () => {
await generator.connect(mongoUri)
})
describe('тест поиска в фабрике документов', () => {
it('подготовка тестовых данных', async () => {
// Создаем тестовые данные для поиска
const testIndividual: ExternalIndividualData = {
username: 'search_test_individual',
first_name: 'Иван',
last_name: 'Иванов',
middle_name: 'Иванович',
birthdate: '01.01.1980',
full_address: 'г. Москва, ул. Тестовая, д. 1',
phone: '+7-900-123-45-67',
email: 'ivan.ivanov@test.com',
deleted: false,
}
const testEntrepreneur: ExternalEntrepreneurData = {
username: 'search_test_entrepreneur',
first_name: 'Петр',
last_name: 'Петров',
middle_name: 'Петрович',
birthdate: '01.01.1985',
full_address: 'г. Москва, ул. Предпринимательская, д. 2',
phone: '+7-900-987-65-43',
email: 'petr.petrov@test.com',
country: 'Россия',
city: 'Москва',
details: {
inn: '123456789012',
ogrn: '1234567890123',
},
deleted: false,
}
const testOrganization: ExternalOrganizationData = {
username: 'search_test_organization',
type: 'coop',
short_name: 'ООО "Тестовая Организация"',
full_name: 'Общество с ограниченной ответственностью "Тестовая Организация"',
represented_by: {
first_name: 'Сидор',
last_name: 'Сидоров',
middle_name: 'Сидорович',
position: 'Директор',
based_on: 'Устав',
},
country: 'Россия',
city: 'Москва',
full_address: 'г. Москва, ул. Организационная, д. 3',
fact_address: 'г. Москва, ул. Организационная, д. 3',
phone: '+7-900-555-55-55',
email: 'info@testorg.com',
details: {
inn: '9876543210',
ogrn: '1234567890987',
kpp: '123456789',
},
deleted: false,
}
// Сохраняем тестовые данные
await generator.save('individual', testIndividual)
await generator.save('entrepreneur', testEntrepreneur)
await generator.save('organization', testOrganization)
})
it('поиск по имени физического лица', async () => {
const results = await generator.search('Иван')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
const individualResults = results.filter(r => r.type === 'individual')
expect(individualResults.length).toBeGreaterThan(0)
const foundIndividual = individualResults.find(r =>
(r.data as ExternalIndividualData).username === 'search_test_individual',
)
expect(foundIndividual).toBeDefined()
expect(foundIndividual?.highlightedFields).toContain('first_name')
})
it('поиск по фамилии', async () => {
const results = await generator.search('Петров')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
const entrepreneurResults = results.filter(r => r.type === 'entrepreneur')
expect(entrepreneurResults.length).toBeGreaterThan(0)
const foundEntrepreneur = entrepreneurResults.find(r =>
(r.data as ExternalEntrepreneurData).username === 'search_test_entrepreneur',
)
expect(foundEntrepreneur).toBeDefined()
expect(foundEntrepreneur?.highlightedFields).toContain('last_name')
})
it('поиск по ИНН предпринимателя', async () => {
const results = await generator.search('123456789012')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
const entrepreneurResults = results.filter(r => r.type === 'entrepreneur')
expect(entrepreneurResults.length).toBeGreaterThan(0)
const foundEntrepreneur = entrepreneurResults.find(r =>
(r.data as ExternalEntrepreneurData).username === 'search_test_entrepreneur',
)
expect(foundEntrepreneur).toBeDefined()
expect(foundEntrepreneur?.highlightedFields).toContain('details.inn')
})
it('поиск по названию организации', async () => {
const results = await generator.search('Тестовая Организация')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
const organizationResults = results.filter(r => r.type === 'organization')
expect(organizationResults.length).toBeGreaterThan(0)
const foundOrganization = organizationResults.find(r =>
(r.data as ExternalOrganizationData).username === 'search_test_organization',
)
expect(foundOrganization).toBeDefined()
expect(foundOrganization?.highlightedFields).toContain('short_name')
})
it('поиск по ИНН организации', async () => {
const results = await generator.search('9876543210')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
const organizationResults = results.filter(r => r.type === 'organization')
expect(organizationResults.length).toBeGreaterThan(0)
const foundOrganization = organizationResults.find(r =>
(r.data as ExternalOrganizationData).username === 'search_test_organization',
)
expect(foundOrganization).toBeDefined()
expect(foundOrganization?.highlightedFields).toContain('details.inn')
})
it('поиск по ОГРН', async () => {
const results = await generator.search('1234567890123')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
const entrepreneurResults = results.filter(r => r.type === 'entrepreneur')
expect(entrepreneurResults.length).toBeGreaterThan(0)
const foundEntrepreneur = entrepreneurResults.find(r =>
(r.data as ExternalEntrepreneurData).username === 'search_test_entrepreneur',
)
expect(foundEntrepreneur).toBeDefined()
expect(foundEntrepreneur?.highlightedFields).toContain('details.ogrn')
})
it('поиск по полному ФИО', async () => {
// Теперь поиск по полному ФИО должен работать!
const results = await generator.search('Иванов Иван Иванович')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
const individualResults = results.filter(r => r.type === 'individual')
expect(individualResults.length).toBeGreaterThan(0)
const foundIndividual = individualResults.find(r =>
(r.data as ExternalIndividualData).username === 'search_test_individual',
)
expect(foundIndividual).toBeDefined()
expect(foundIndividual?.highlightedFields).toContain('full_name')
})
it('поиск по частичному ФИО (два слова)', async () => {
const results = await generator.search('Петров Петр')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
const entrepreneurResults = results.filter(r => r.type === 'entrepreneur')
expect(entrepreneurResults.length).toBeGreaterThan(0)
const foundEntrepreneur = entrepreneurResults.find(r =>
(r.data as ExternalEntrepreneurData).username === 'search_test_entrepreneur',
)
expect(foundEntrepreneur).toBeDefined()
expect(foundEntrepreneur?.highlightedFields).toContain('full_name')
})
it('поиск с пустым запросом', async () => {
const results = await generator.search('')
expect(results).toEqual([])
const results2 = await generator.search(' ')
expect(results2).toEqual([])
})
it('поиск несуществующих данных', async () => {
const results = await generator.search('НесуществующийЗапрос12345')
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
expect(results.length).toBe(0)
})
it('ограничение количества результатов', async () => {
const results = await generator.search('test') // Поиск общего термина
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
expect(results.length).toBeLessThanOrEqual(10) // Не больше 10 результатов
})
it('очистка тестовых данных', async () => {
// Удаляем тестовые данные
await generator.del('individual', { username: 'search_test_individual' })
await generator.del('entrepreneur', { username: 'search_test_entrepreneur' })
await generator.del('organization', { username: 'search_test_organization' })
})
})
+4 -1
View File
@@ -4,4 +4,7 @@
export * as GetAccount from './getAccount'
/** Получить сводную информацию о аккаунтах системы */
export * as GetAccounts from './getAccounts'
export * as GetAccounts from './getAccounts'
/** Поиск приватных данных аккаунтов по запросу */
export * as SearchPrivateAccounts from './searchPrivateAccounts'
@@ -0,0 +1,22 @@
import { privateAccountSearchResultSelector } from '../../selectors/accounts/searchPrivateAccountsResult'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'searchPrivateAccounts'
/**
* Поиск приватных данных аккаунтов по запросу
*/
export const query = Selector('Query')({
[name]: [{ data: $('data', 'SearchPrivateAccountsInput!') }, privateAccountSearchResultSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['SearchPrivateAccountsInput']
}
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
@@ -0,0 +1,23 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
import { rawEntrepreneurSelector, rawIndividualSelector, rawOrganizationSelector } from '../common'
import { rawAccountSelector } from './accountSelector'
export const rawSearchPrivateAccountsResultSelector = {
type: true,
score: true,
highlightedFields: true,
data: {
'...on Individual': rawIndividualSelector,
'...on Entrepreneur': rawEntrepreneurSelector,
'...on Organization': rawOrganizationSelector,
},
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['PrivateAccountSearchResult']> = rawSearchPrivateAccountsResultSelector
export const privateAccountSearchResultSelector = Selector('PrivateAccountSearchResult')(
rawSearchPrivateAccountsResultSelector,
)
+19 -1
View File
@@ -564,6 +564,9 @@ export const AllTypesProps: Record<string,any> = {
getPayments:{
data:"GetPaymentsInput",
options:"PaginationInput"
},
searchPrivateAccounts:{
data:"SearchPrivateAccountsInput"
}
},
ReceiveOnRequestInput:{
@@ -618,6 +621,9 @@ export const AllTypesProps: Record<string,any> = {
},
ReturnByAssetStatementSignedMetaDocumentInput:{
request:"CommonRequestInput"
},
SearchPrivateAccountsInput:{
},
SelectBranchGenerateDocumentInput:{
@@ -1356,6 +1362,17 @@ export const ReturnTypes: Record<string,any> = {
organization_data:"Organization",
type:"AccountType"
},
PrivateAccountSearchData:{
"...on Entrepreneur":"Entrepreneur",
"...on Individual":"Individual",
"...on Organization":"Organization"
},
PrivateAccountSearchResult:{
data:"PrivateAccountSearchData",
highlightedFields:"String",
score:"Float",
type:"String"
},
PublicChairman:{
first_name:"String",
last_name:"String",
@@ -1373,7 +1390,8 @@ export const ReturnTypes: Record<string,any> = {
getMeets:"MeetAggregate",
getPaymentMethods:"PaymentMethodPaginationResult",
getPayments:"PaymentPaginationResult",
getSystemInfo:"SystemInfo"
getSystemInfo:"SystemInfo",
searchPrivateAccounts:"PrivateAccountSearchResult"
},
Question:{
context:"String",
+87 -3
View File
@@ -920,7 +920,7 @@ export type ScalarCoders = {
JSON?: ScalarResolver;
JSONObject?: ScalarResolver;
}
type ZEUS_UNIONS = GraphQLTypes["PaymentMethodData"] | GraphQLTypes["UserCertificateUnion"]
type ZEUS_UNIONS = GraphQLTypes["PaymentMethodData"] | GraphQLTypes["PrivateAccountSearchData"] | GraphQLTypes["UserCertificateUnion"]
export type ValueTypes = {
["AcceptChildOrderInput"]: {
@@ -3175,6 +3175,22 @@ voteOnAnnualGeneralMeet?: [{ data: ValueTypes["VoteOnAnnualGeneralMeetInput"] |
/** Тип аккаунта */
type?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["PrivateAccountSearchData"]: AliasType<{ ["...on Entrepreneur"]?: ValueTypes["Entrepreneur"],
["...on Individual"]?: ValueTypes["Individual"],
["...on Organization"]?: ValueTypes["Organization"]
__typename?: boolean | `@${string}`
}>;
["PrivateAccountSearchResult"]: AliasType<{
/** Данные найденного аккаунта */
data?:ValueTypes["PrivateAccountSearchData"],
/** Поля, в которых найдены совпадения */
highlightedFields?:boolean | `@${string}`,
/** Оценка релевантности результата */
score?:boolean | `@${string}`,
/** Тип аккаунта */
type?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["ProhibitRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -3290,6 +3306,7 @@ getPaymentMethods?: [{ data?: ValueTypes["GetPaymentMethodsInput"] | undefined |
getPayments?: [{ data?: ValueTypes["GetPaymentsInput"] | undefined | null | Variable<any, string>, options?: ValueTypes["PaginationInput"] | undefined | null | Variable<any, string>},ValueTypes["PaymentPaginationResult"]],
/** Получить сводную публичную информацию о системе */
getSystemInfo?:ValueTypes["SystemInfo"],
searchPrivateAccounts?: [{ data: ValueTypes["SearchPrivateAccountsInput"] | Variable<any, string>},ValueTypes["PrivateAccountSearchResult"]],
__typename?: boolean | `@${string}`
}>;
/** Вопрос повестки собрания с результатами голосования */
@@ -3637,6 +3654,10 @@ getPayments?: [{ data?: ValueTypes["GetPaymentsInput"] | undefined | null | Vari
phone?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["SearchPrivateAccountsInput"]: {
/** Поисковый запрос для поиска приватных аккаунтов */
query: string | Variable<any, string>
};
["SelectBranchGenerateDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num?: number | undefined | null | Variable<any, string>,
@@ -6386,6 +6407,23 @@ voteOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["VoteOnAnnualGeneralMeetIn
/** Тип аккаунта */
type?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["PrivateAccountSearchData"]: AliasType<{
Entrepreneur?:ResolverInputTypes["Entrepreneur"],
Individual?:ResolverInputTypes["Individual"],
Organization?:ResolverInputTypes["Organization"],
__typename?: boolean | `@${string}`
}>;
["PrivateAccountSearchResult"]: AliasType<{
/** Данные найденного аккаунта */
data?:ResolverInputTypes["PrivateAccountSearchData"],
/** Поля, в которых найдены совпадения */
highlightedFields?:boolean | `@${string}`,
/** Оценка релевантности результата */
score?:boolean | `@${string}`,
/** Тип аккаунта */
type?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["ProhibitRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -6501,6 +6539,7 @@ getPaymentMethods?: [{ data?: ResolverInputTypes["GetPaymentMethodsInput"] | und
getPayments?: [{ data?: ResolverInputTypes["GetPaymentsInput"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaymentPaginationResult"]],
/** Получить сводную публичную информацию о системе */
getSystemInfo?:ResolverInputTypes["SystemInfo"],
searchPrivateAccounts?: [{ data: ResolverInputTypes["SearchPrivateAccountsInput"]},ResolverInputTypes["PrivateAccountSearchResult"]],
__typename?: boolean | `@${string}`
}>;
/** Вопрос повестки собрания с результатами голосования */
@@ -6848,6 +6887,10 @@ getPayments?: [{ data?: ResolverInputTypes["GetPaymentsInput"] | undefined | nul
phone?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["SearchPrivateAccountsInput"]: {
/** Поисковый запрос для поиска приватных аккаунтов */
query: string
};
["SelectBranchGenerateDocumentInput"]: {
/** Номер блока, на котором был создан документ */
block_num?: number | undefined | null,
@@ -9607,6 +9650,17 @@ export type ModelTypes = {
organization_data?: ModelTypes["Organization"] | undefined | null,
/** Тип аккаунта */
type: ModelTypes["AccountType"]
};
["PrivateAccountSearchData"]:ModelTypes["Entrepreneur"] | ModelTypes["Individual"] | ModelTypes["Organization"];
["PrivateAccountSearchResult"]: {
/** Данные найденного аккаунта */
data: ModelTypes["PrivateAccountSearchData"],
/** Поля, в которых найдены совпадения */
highlightedFields?: Array<string> | undefined | null,
/** Оценка релевантности результата */
score?: number | undefined | null,
/** Тип аккаунта */
type: string
};
["ProhibitRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -9728,7 +9782,9 @@ export type ModelTypes = {
/** Получить список платежей */
getPayments: ModelTypes["PaymentPaginationResult"],
/** Получить сводную публичную информацию о системе */
getSystemInfo: ModelTypes["SystemInfo"]
getSystemInfo: ModelTypes["SystemInfo"],
/** Поиск приватных данных аккаунтов по запросу. Поиск осуществляется по полям ФИО, ИНН, ОГРН, наименованию организации и другим приватным данным. */
searchPrivateAccounts: Array<ModelTypes["PrivateAccountSearchResult"]>
};
/** Вопрос повестки собрания с результатами голосования */
["Question"]: {
@@ -10066,6 +10122,10 @@ export type ModelTypes = {
["SbpAccount"]: {
/** Мобильный телефон получателя */
phone: string
};
["SearchPrivateAccountsInput"]: {
/** Поисковый запрос для поиска приватных аккаунтов */
query: string
};
["SelectBranchGenerateDocumentInput"]: {
/** Номер блока, на котором был создан документ */
@@ -12878,6 +12938,23 @@ export type GraphQLTypes = {
organization_data?: GraphQLTypes["Organization"] | undefined | null,
/** Тип аккаунта */
type: GraphQLTypes["AccountType"]
};
["PrivateAccountSearchData"]:{
__typename:"Entrepreneur" | "Individual" | "Organization"
['...on Entrepreneur']: '__union' & GraphQLTypes["Entrepreneur"];
['...on Individual']: '__union' & GraphQLTypes["Individual"];
['...on Organization']: '__union' & GraphQLTypes["Organization"];
};
["PrivateAccountSearchResult"]: {
__typename: "PrivateAccountSearchResult",
/** Данные найденного аккаунта */
data: GraphQLTypes["PrivateAccountSearchData"],
/** Поля, в которых найдены совпадения */
highlightedFields?: Array<string> | undefined | null,
/** Оценка релевантности результата */
score?: number | undefined | null,
/** Тип аккаунта */
type: string
};
["ProhibitRequestInput"]: {
/** Имя аккаунта кооператива */
@@ -13001,7 +13078,9 @@ export type GraphQLTypes = {
/** Получить список платежей */
getPayments: GraphQLTypes["PaymentPaginationResult"],
/** Получить сводную публичную информацию о системе */
getSystemInfo: GraphQLTypes["SystemInfo"]
getSystemInfo: GraphQLTypes["SystemInfo"],
/** Поиск приватных данных аккаунтов по запросу. Поиск осуществляется по полям ФИО, ИНН, ОГРН, наименованию организации и другим приватным данным. */
searchPrivateAccounts: Array<GraphQLTypes["PrivateAccountSearchResult"]>
};
/** Вопрос повестки собрания с результатами голосования */
["Question"]: {
@@ -13347,6 +13426,10 @@ export type GraphQLTypes = {
__typename: "SbpAccount",
/** Мобильный телефон получателя */
phone: string
};
["SearchPrivateAccountsInput"]: {
/** Поисковый запрос для поиска приватных аккаунтов */
query: string
};
["SelectBranchGenerateDocumentInput"]: {
/** Номер блока, на котором был создан документ */
@@ -14013,6 +14096,7 @@ type ZEUS_VARIABLES = {
["ReturnByAssetStatementGenerateDocumentInput"]: ValueTypes["ReturnByAssetStatementGenerateDocumentInput"];
["ReturnByAssetStatementSignedDocumentInput"]: ValueTypes["ReturnByAssetStatementSignedDocumentInput"];
["ReturnByAssetStatementSignedMetaDocumentInput"]: ValueTypes["ReturnByAssetStatementSignedMetaDocumentInput"];
["SearchPrivateAccountsInput"]: ValueTypes["SearchPrivateAccountsInput"];
["SelectBranchGenerateDocumentInput"]: ValueTypes["SelectBranchGenerateDocumentInput"];
["SelectBranchInput"]: ValueTypes["SelectBranchInput"];
["SelectBranchSignedDocumentInput"]: ValueTypes["SelectBranchSignedDocumentInput"];