aspans_patrol/apps/api/src/modules/organizations/organizations.service.ts
2026-10-22 10:53:02 +05:00

127 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 });
}
}