All files / libs/tenants/users/core/src/lib/usecases register-users.use-case.ts

96.26% Statements 103/107
63.63% Branches 14/22
100% Functions 2/2
96.26% Lines 103/107

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 1081x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x     3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 1x 1x 3x 3x 4x     3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 1x  
import { BadRequestException, Injectable, UnprocessableEntityException } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
import { InjectRepository } from '@nestjs/typeorm';
import { keyBy, merge, uniqBy } from 'lodash-es';
import { SetRequired } from 'type-fest';
import { Repository } from 'typeorm';
 
import { KEYV_NAMESPACE, KeyvRepository, User, type Company } from '@amalia/core/models';
import { assert } from '@amalia/ext/typescript';
import { AuthenticatedContextFactory } from '@amalia/kernel/auth/core';
import { RecordType } from '@amalia/tenants/monitoring/audit/types';
import { AvatarsService } from '@amalia/tenants/users/profile/core';
import { SyncUserRequest, UserExternalIdSource, UserHrisIdSource } from '@amalia/tenants/users/types';
 
import { AppUsersRepository } from '../appUsers.repository';
import { UsersCreatedEvent } from '../events/UsersCreatedEvent';
 
@Injectable()
export class RegisterUsersUseCase {
  public constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly appUsersRepository: AppUsersRepository,
    private readonly avatarsService: AvatarsService,
    private readonly eventBus: EventBus,
    private readonly authenticatedContextFactory: AuthenticatedContextFactory,
    private readonly keyvRepository: KeyvRepository,
  ) {}
 
  public async execute({
    company,
    users,
    currentUser,
  }: {
    company: Company;
    users: SyncUserRequest[];
    currentUser: User;
  }) {
    if (users.length === 0) {
      return;
    }
 
    // WARNING: if you change the way users are retrieved, be sure to check that you also
    // retrieve the settings of the users, else the settings will be lost and replaced by the default ones.
 
    assert(
      users.every((u): u is SetRequired<SyncUserRequest, 'email'> => !!u.email),
      'All users must have an email',
    );
 
    const existingUsers = await this.appUsersRepository.findUsersByEmail(users.map((u) => u.email.toLowerCase()));
 
    const existingUsersEmails = keyBy(existingUsers, 'email') as Record<string, User | undefined>;
    const usersToSave = users.map((u) => {
      const existingUser = existingUsersEmails[u.email.toLowerCase()];
 
      assert(
        !existingUser || existingUser.companyId === company.id,
        new UnprocessableEntityException(`User with email ${existingUser?.email} already exists.`),
      );
 
      const userToSave: User = merge(existingUser || new User(), u);
      userToSave.company = company;
      userToSave.email = u.email.toLowerCase();
      userToSave.pictureURL ??= this.avatarsService.randomDefaultAvatarUrl();
 
      // If externalId is set without externalIdSource, it means that the user comes from another source than SALESFORCE or connector source.
      // So it's OTHER.
      if (userToSave.externalId) {
        userToSave.externalIdSource = u.externalIdSource ?? UserExternalIdSource.OTHERSOURCE;
      }
 
      // Same rule as above but for hrisId
      if (userToSave.hrisId) {
        userToSave.hrisIdSource = u.hrisIdSource ?? UserHrisIdSource.OTHERSOURCE;
      }
 
      return userToSave;
    });
 
    // We check that there is no users to save with the same email
    const uniqUsersPerEmail = uniqBy(usersToSave, 'email');
 
    if (uniqUsersPerEmail.length !== usersToSave.length) {
      throw new BadRequestException('Duplicate emails found in the list');
    }
 
    const savedUsers = await this.userRepository.save(usersToSave);
 
    await this.keyvRepository.clearByCompany(KEYV_NAMESPACE.USERS_WHO_CAN_SEE_STATEMENT, company.id);
 
    const newUsers = savedUsers.filter((u) => !existingUsersEmails[u.email]);
 
    if (newUsers.length > 0) {
      await this.eventBus.publish(new UsersCreatedEvent(company, currentUser, newUsers));
 
      const authenticatedContext = await this.authenticatedContextFactory.makeAuthenticatedContext(currentUser);
 
      // Audit each created user.
      await Promise.all(
        newUsers.map(async (u) => {
          await this.appUsersRepository.auditRecordForUser(authenticatedContext, RecordType.CREATE, u);
        }),
      );
    }
  }
}