@sourceloop/ctrl-plane-tenant-management-service
Version:
Tenant Management microservice for SaaS control plane
220 lines (213 loc) • 7.69 kB
text/typescript
import {BindingScope, inject, injectable, service} from '@loopback/core';
import {repository} from '@loopback/repository';
import {HttpErrors} from '@loopback/rest';
import {ILogger, LOGGER} from '@sourceloop/core';
import {TenantStatus} from '../enums';
import {Address, Contact, Lead, Tenant, TenantOnboardDTO} from '../models';
import {CreateLeadDTO} from '../models/dtos/create-lead-dto.model';
import {
AddressRepository,
ContactRepository,
LeadRepository,
TenantRepository,
} from '../repositories';
import {LeadUser} from '../types';
import {hasAnyOf, weakEqual} from '../utils';
import {LeadAuthenticator} from './lead-authenticator.service';
import {omit} from 'lodash';
/**
* Helper service for onboarding tenants.
*/
@injectable({scope: BindingScope.REQUEST})
export class OnboardingService {
/**
* Constructs a new instance of the OnboardingService.
* @param {LeadRepository} leadRepository - Repository for managing leads.
* @param {TenantRepository} tenantRepository - Repository for managing tenants.
* @param {ContactRepository} contactRepository - Repository for managing contacts.
* @param {AddressRepository} addressRepository - Repository for managing addresses.
* @param {LeadAuthenticator} leadAuthenticator - Service for authenticating leads.
* @param {ILogger} logger - Logger service for logging messages.
*/
constructor(
@repository(LeadRepository)
private leadRepository: LeadRepository,
@repository(TenantRepository)
private tenantRepository: TenantRepository,
@repository(ContactRepository)
private contactRepository: ContactRepository,
@repository(AddressRepository)
private addressRepository: AddressRepository,
@service(LeadAuthenticator)
private leadAuthenticator: LeadAuthenticator,
@inject(LOGGER.LOGGER_INJECT)
private logger: ILogger,
) {}
/**
* The addLead function creates a new lead, triggers a validation email, and
* returns the new lead.
* @param lead - The `lead` parameter is an object of type `Lead` with the `id`
* property omitted.
* @returns The `addLead` function is returning the newly created lead object.
*/
async addLead(lead: Omit<CreateLeadDTO, 'isValidated' | 'addressId' | 'id'>) {
const existing = await this.leadRepository.findOne({
where: {
email: lead.email,
isValidated: true,
},
});
if (existing) {
this.logger.error(`Lead with email ${lead.email} already exists`);
throw new HttpErrors.Conflict();
}
let addressId: string | undefined = undefined;
if (lead.address?.country) {
const address = await this.addressRepository.create(
new Address({
country: lead.address?.country,
address: lead.address?.address,
city: lead.address?.city,
state: lead.address?.state,
zip: lead.address?.zip,
}),
);
addressId = address.id;
}
const newLead = await this.leadRepository.create(
new Lead({
isValidated: false,
addressId,
...omit(lead, 'address'),
}),
);
const key = await this.leadAuthenticator.triggerValidationMail(newLead); // triggered notification sunny
return {key, id: newLead.id};
}
/**
* The startOnboarding function checks if a lead user exists and is validated, and
* if not, updates the lead user's validation status and triggers tenant creation.
* @param {LeadUser} lead - The `lead` parameter is an object of type `LeadUser`.
* It represents a lead user who is going through the onboarding process.
*/
async onboardForLead(dto: Omit<TenantOnboardDTO, 'contact'>, lead: LeadUser) {
const existing = await this.leadRepository.findOne({
where: {
id: lead.id,
isValidated: true,
},
include: ['tenant'],
});
if (!existing) {
this.logger.error(`Valid Lead with id ${lead.id} does not exist`);
throw new HttpErrors.Unauthorized();
}
if (existing.tenant) {
this.logger.error(`Lead with id ${lead.id} has a tenant`);
throw new HttpErrors.Unauthorized();
}
await this.leadRepository.updateById(lead.id, {
isValidated: true,
});
return this.onboard(
{
...dto,
contact: new Contact({
email: existing.email,
type: 'admin',
firstName: existing.firstName,
lastName: existing.lastName,
isPrimary: true,
}),
},
existing,
);
}
/**
* The `setupTenant` function creates a new tenant with the provided information and an optional lead,
* and returns the created tenant.
* @param {TenantOnboardDTO} dto - The `dto` parameter is an object of type `TenantOnboardDTO` which
* contains the necessary information to onboard a new tenant. It includes properties such as `key`,
* `country`, `address`, `city`, `state`, `zip`, and `name`.
* @param {Lead} [lead] - The `lead` parameter is an optional parameter of type `Lead`. It represents
* the lead associated with the tenant being onboarded. If a lead is provided, their information will
* be used to create a contact for the tenant. If no lead is provided, the contact will not be created.
* @returns a Promise that resolves to a Tenant object.
*/
async onboard(dto: TenantOnboardDTO, lead?: Lead): Promise<Tenant> {
const transaction = await this.tenantRepository.beginTransaction();
try {
let address: Address | null = null;
if (lead?.addressId) {
address = await this.addressRepository.findById(lead.addressId);
if (
hasAnyOf(dto, ['address', 'city', 'state', 'country', 'zip']) &&
(!weakEqual(address.address, dto.address) ||
!weakEqual(address.city, dto.city) ||
!weakEqual(address.state, dto.state) ||
!weakEqual(address.country, dto.country) ||
!weakEqual(address.zip, dto.zip))
) {
throw new HttpErrors.BadRequest('Address mismatch with Lead');
}
} else if (dto.country) {
address = await this.addressRepository.create(
{
country: dto.country,
address: dto.address,
city: dto.city,
state: dto.state,
zip: dto.zip,
},
{transaction},
);
} else {
// Do Nothing
}
const tenant = await this.tenantRepository.create(
{
key: dto.key,
name: dto.name ?? lead?.companyName,
leadId: lead?.id,
domains: dto.domains,
status: TenantStatus.PENDINGPROVISION,
addressId: address?.id,
},
{transaction},
);
const contactHostName = dto.contact.email.split('@')[1];
if (!dto.domains.includes(contactHostName)) {
throw new HttpErrors.BadRequest(
'Contact email domain does not match tenant domains',
);
}
await this.contactRepository.create(
{
email: dto.contact.email,
firstName: dto.contact.firstName,
lastName: dto.contact.lastName,
tenantId: tenant.id,
isPrimary: dto.contact.isPrimary,
},
{transaction},
);
const res = await this.tenantRepository.findById(
tenant.id,
{
include: [
{relation: 'contacts'},
{relation: 'resources'},
{relation: 'lead'},
{relation: 'address'},
],
},
{transaction},
);
await transaction.commit();
return res;
} catch (error) {
await transaction.rollback();
throw error;
}
}
}