127 lines
5.4 KiB
TypeScript
127 lines
5.4 KiB
TypeScript
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 });
|
||
}
|
||
}
|