import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { ROLE_TEMPLATES } from '@patrol/shared'; import { Organization, Role } from '../../entities'; import { parsePerson, PeopleService, PersonInput } from '../users/people.service'; import { bool, obj, reqStr, str } from '../../common/input'; import { isValidBin } from './bin'; import { CompanyRegistry } from './company-registry'; export type RegisterInput = { bin: string; name: string; legalName?: string; legalAddress?: string; directorName?: string; phone?: string; email?: string; admin: PersonInput; }; // allowExisting — выбрать администратора из людей паспорта (только владелец платформы). export function parseRegisterInput(body: unknown, opts: { allowExisting: boolean }): RegisterInput { const o = obj(body); const bin = reqStr(o, 'bin').replace(/\s/g, ''); if (!isValidBin(bin)) throw new BadRequestException('БИН должен состоять из 12 цифр с верным контрольным разрядом'); return { bin, name: str(o, 'name', { max: 300 }) ?? '', legalName: str(o, 'legalName', { max: 500 }), legalAddress: str(o, 'legalAddress', { max: 1000 }), directorName: str(o, 'directorName', { max: 300 }), phone: str(o, 'phone', { max: 50 }), email: str(o, 'email', { max: 200 }), admin: parsePerson(obj(o.admin, 'admin'), opts), }; } @Injectable() export class OrganizationsService { constructor( @InjectDataSource() private readonly db: DataSource, private readonly registry: CompanyRegistry, private readonly people: PeopleService, ) {} async lookup(bin: string) { if (!isValidBin(bin)) throw new BadRequestException('Неверный БИН'); const exists = await this.db.getRepository(Organization).exist({ where: { bin } }); const info = await this.registry.lookup(bin); return { bin, alreadyRegistered: exists, registryEnabled: this.registry.enabled, company: info && { name: info.name, legalName: info.legalName, legalAddress: info.legalAddress, directorName: info.directorName, phone: info.phone, email: info.email, }, }; } /** * Компания, её роли из шаблонов и первый администратор. Администратор — человек паспорта: * новый заводится там же, существующий привязывается к платформе patrol. * trusted — регистрирует владелец платформы (иначе — сама компания, см. PeopleService.provision). */ async register(input: RegisterInput, opts: { trusted: boolean }) { const info = await this.registry.lookup(input.bin); const name = input.name || info?.name; if (!name) throw new BadRequestException('Укажите название компании'); if (await this.db.getRepository(Organization).exist({ where: { bin: input.bin } })) { throw new ConflictException('Компания с таким БИН уже зарегистрирована'); } const person = await this.people.provision(input.admin, opts); return this.db.transaction(async (m) => { if (await m.getRepository(Organization).exist({ where: { bin: input.bin } })) { throw new ConflictException('Компания с таким БИН уже зарегистрирована'); } const org = await m.getRepository(Organization).save({ bin: input.bin, name, legalName: input.legalName ?? info?.legalName ?? null, legalAddress: input.legalAddress ?? info?.legalAddress ?? null, directorName: input.directorName ?? info?.directorName ?? null, phone: input.phone ?? info?.phone ?? null, email: input.email ?? info?.email ?? null, registryData: info?.raw ?? null, registrySyncedAt: info ? new Date() : null, }); const roles = await m.getRepository(Role).save( ROLE_TEMPLATES.map((t) => ({ organizationId: org.id, code: t.code, name: t.name, isSystem: true, allSites: t.allSites, permissions: [...t.permissions], })), ); const adminRole = roles.find((r) => r.code === 'admin')!; const adminId = await this.people.attach(m, person, org.id, adminRole.id); return { organization: org, adminId }; }); } list() { return this.db.query(` SELECT o.id, o.bin, o.name, o.legal_name AS "legalName", o.status, o.created_at AS "createdAt", (SELECT count(*)::int FROM users u WHERE u.organization_id = o.id AND u.removed_at IS NULL) AS "usersCount", (SELECT count(*)::int FROM sites s WHERE s.organization_id = o.id AND s.archived_at IS NULL) AS "sitesCount" FROM organizations o ORDER BY o.created_at DESC`); } async setStatus(id: string, body: unknown) { const blocked = bool(obj(body), 'blocked'); if (blocked === undefined) throw new BadRequestException('Поле «blocked» обязательно'); await this.db.getRepository(Organization).update({ id }, { status: blocked ? 'blocked' : 'active' }); return this.db.getRepository(Organization).findOneByOrFail({ id }); } }