mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
refactor: organization service to nestjs
This commit is contained in:
@@ -65,6 +65,8 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mathjs": "^9.4.0",
|
"mathjs": "^9.4.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"moment-range": "^4.0.2",
|
||||||
|
"moment-timezone": "^0.5.43",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"mysql2": "^3.11.3",
|
"mysql2": "^3.11.3",
|
||||||
"nestjs-cls": "^5.2.0",
|
"nestjs-cls": "^5.2.0",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ import { DashboardModule } from '../Dashboard/Dashboard.module';
|
|||||||
import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
||||||
import { RolesModule } from '../Roles/Roles.module';
|
import { RolesModule } from '../Roles/Roles.module';
|
||||||
import { SubscriptionModule } from '../Subscription/Subscription.module';
|
import { SubscriptionModule } from '../Subscription/Subscription.module';
|
||||||
|
import { OrganizationModule } from '../Organization/Organization.module';
|
||||||
|
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -186,7 +188,9 @@ import { SubscriptionModule } from '../Subscription/Subscription.module';
|
|||||||
DashboardModule,
|
DashboardModule,
|
||||||
PaymentLinksModule,
|
PaymentLinksModule,
|
||||||
RolesModule,
|
RolesModule,
|
||||||
SubscriptionModule
|
SubscriptionModule,
|
||||||
|
OrganizationModule,
|
||||||
|
TenantDBManagerModule
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import currencies from 'js-money/lib/currency';
|
||||||
|
|
||||||
|
export const DATE_FORMATS = [
|
||||||
|
'MM.dd.yy',
|
||||||
|
'dd.MM.yy',
|
||||||
|
'yy.MM.dd',
|
||||||
|
'MM.dd.yyyy',
|
||||||
|
'dd.MM.yyyy',
|
||||||
|
'yyyy.MM.dd',
|
||||||
|
'MM/DD/YYYY',
|
||||||
|
'M/D/YYYY',
|
||||||
|
'dd MMM YYYY',
|
||||||
|
'dd MMMM YYYY',
|
||||||
|
'MMMM dd, YYYY',
|
||||||
|
'EEE, MMMM dd, YYYY',
|
||||||
|
];
|
||||||
|
export const MONTHS = [
|
||||||
|
'january',
|
||||||
|
'february',
|
||||||
|
'march',
|
||||||
|
'april',
|
||||||
|
'may',
|
||||||
|
'june',
|
||||||
|
'july',
|
||||||
|
'august',
|
||||||
|
'september',
|
||||||
|
'october',
|
||||||
|
'november',
|
||||||
|
'december',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ACCEPTED_LOCALES = ['en', 'ar'];
|
||||||
|
|
||||||
|
export const ERRORS = {
|
||||||
|
TENANT_DATABASE_UPGRADED: 'TENANT_DATABASE_UPGRADED',
|
||||||
|
TENANT_NOT_FOUND: 'tenant_not_found',
|
||||||
|
TENANT_ALREADY_BUILT: 'TENANT_ALREADY_BUILT',
|
||||||
|
TENANT_ALREADY_SEEDED: 'tenant_already_seeded',
|
||||||
|
TENANT_DB_NOT_BUILT: 'tenant_db_not_built',
|
||||||
|
TENANT_IS_BUILDING: 'TENANT_IS_BUILDING',
|
||||||
|
BASE_CURRENCY_MUTATE_LOCKED: 'BASE_CURRENCY_MUTATE_LOCKED',
|
||||||
|
TENANT_UPGRADE_IS_RUNNING: 'TENANT_UPGRADE_IS_RUNNING'
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Get,
|
||||||
|
Body,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
Next,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { BuildOrganizationService } from './commands/BuildOrganization.service';
|
||||||
|
import {
|
||||||
|
BuildOrganizationDto,
|
||||||
|
UpdateOrganizationDto,
|
||||||
|
} from './dtos/Organization.dto';
|
||||||
|
import { GetCurrentOrganizationService } from './queries/GetCurrentOrganization.service';
|
||||||
|
import { UpdateOrganizationService } from './commands/UpdateOrganization.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiTags('Organization')
|
||||||
|
@Controller('organization')
|
||||||
|
export class OrganizationController {
|
||||||
|
constructor(
|
||||||
|
private readonly buildOrganizationService: BuildOrganizationService,
|
||||||
|
private readonly getCurrentOrgService: GetCurrentOrganizationService,
|
||||||
|
private readonly updateOrganizationService: UpdateOrganizationService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('build')
|
||||||
|
@ApiOperation({ summary: 'Build organization database' })
|
||||||
|
@ApiBody({ type: BuildOrganizationDto })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The organization database has been initialized',
|
||||||
|
})
|
||||||
|
async build(
|
||||||
|
@Body() buildDTO: BuildOrganizationDto,
|
||||||
|
@Req() req: Request,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
const result = await this.buildOrganizationService.buildRunJob(buildDTO);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
type: 'success',
|
||||||
|
code: 'ORGANIZATION.DATABASE.INITIALIZED',
|
||||||
|
message: 'The organization database has been initialized.',
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'Get current organization' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Returns the current organization',
|
||||||
|
})
|
||||||
|
async currentOrganization(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Res() res: Response,
|
||||||
|
@Next() next: NextFunction,
|
||||||
|
) {
|
||||||
|
const organization =
|
||||||
|
await this.getCurrentOrgService.getCurrentOrganization();
|
||||||
|
|
||||||
|
return res.status(200).send({ organization });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
@ApiOperation({ summary: 'Update organization information' })
|
||||||
|
@ApiBody({ type: UpdateOrganizationDto })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Organization information has been updated successfully',
|
||||||
|
})
|
||||||
|
async updateOrganization(
|
||||||
|
@Body() updateDTO: UpdateOrganizationDto,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
await this.updateOrganizationService.execute(updateDTO);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Organization information has been updated successfully.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { GetCurrentOrganizationService } from './queries/GetCurrentOrganization.service';
|
||||||
|
import { BuildOrganizationService } from './commands/BuildOrganization.service';
|
||||||
|
import { UpdateOrganizationService } from './commands/UpdateOrganization.service';
|
||||||
|
import { OrganizationController } from './Organization.controller';
|
||||||
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
|
import { OrganizationBuildQueue } from './Organization.types';
|
||||||
|
import { OrganizationBuildProcessor } from './processors/OrganizationBuild.processor';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
GetCurrentOrganizationService,
|
||||||
|
BuildOrganizationService,
|
||||||
|
UpdateOrganizationService,
|
||||||
|
OrganizationBuildProcessor
|
||||||
|
],
|
||||||
|
imports: [BullModule.registerQueue({ name: OrganizationBuildQueue })],
|
||||||
|
controllers: [OrganizationController],
|
||||||
|
})
|
||||||
|
export class OrganizationModule {}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { SystemUser } from '../System/models/SystemUser';
|
||||||
|
|
||||||
|
export interface IOrganizationSetupDTO {
|
||||||
|
organizationName: string;
|
||||||
|
baseCurrency: string;
|
||||||
|
fiscalYear: string;
|
||||||
|
industry: string;
|
||||||
|
timeZone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrganizationBuildDTO {
|
||||||
|
name: string;
|
||||||
|
industry: string;
|
||||||
|
location: string;
|
||||||
|
baseCurrency: string;
|
||||||
|
timezone: string;
|
||||||
|
fiscalYear: string;
|
||||||
|
dateFormat?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationAddressDTO {
|
||||||
|
address1: string;
|
||||||
|
address2: string;
|
||||||
|
postalCode: string;
|
||||||
|
city: string;
|
||||||
|
stateProvince: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrganizationUpdateDTO {
|
||||||
|
name: string;
|
||||||
|
location?: string;
|
||||||
|
baseCurrency?: string;
|
||||||
|
timezone?: string;
|
||||||
|
fiscalYear?: string;
|
||||||
|
industry?: string;
|
||||||
|
taxNumber?: string;
|
||||||
|
primaryColor?: string;
|
||||||
|
logoKey?: string;
|
||||||
|
address?: OrganizationAddressDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrganizationBuildEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
buildDTO: IOrganizationBuildDTO;
|
||||||
|
systemUser: SystemUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrganizationBuiltEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrganizationBuildQueue = 'OrganizationBuildQueue';
|
||||||
|
export const OrganizationBuildQueueJob = 'OrganizationBuildQueueJob';
|
||||||
|
|
||||||
|
export interface OrganizationBuildQueueJobPayload {
|
||||||
|
buildDto: IOrganizationBuildDTO;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { defaultTo } from 'lodash';
|
||||||
|
import { IOrganizationBuildDTO } from './Organization.types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes build DTO object.
|
||||||
|
* @param {IOrganizationBuildDTO} buildDTO
|
||||||
|
* @returns {IOrganizationBuildDTO}
|
||||||
|
*/
|
||||||
|
export const transformBuildDto = (
|
||||||
|
buildDTO: IOrganizationBuildDTO,
|
||||||
|
): IOrganizationBuildDTO => {
|
||||||
|
return {
|
||||||
|
...buildDTO,
|
||||||
|
dateFormat: defaultTo(buildDTO.dateFormat, 'DD MMM yyyy'),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import { TimeoutSettings } from 'puppeteer';
|
|
||||||
|
|
||||||
interface MutateBaseCurrencyLockMeta {
|
interface MutateBaseCurrencyLockMeta {
|
||||||
modelName: string;
|
modelName: string;
|
||||||
pluralName?: string;
|
pluralName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service()
|
@Injectable()
|
||||||
export default class OrganizationBaseCurrencyLocking {
|
export class OrganizationBaseCurrencyLocking {
|
||||||
@Inject()
|
|
||||||
tenancy: HasTenancyService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the tenant models that have prevented mutation base currency.
|
* Retrieves the tenant models that have prevented mutation base currency.
|
||||||
*/
|
*/
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import { ObjectId } from 'mongodb';
|
|
||||||
import { defaultTo, pick } from 'lodash';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import {
|
|
||||||
IOrganizationBuildDTO,
|
|
||||||
IOrganizationBuildEventPayload,
|
|
||||||
IOrganizationBuiltEventPayload,
|
|
||||||
IOrganizationUpdateDTO,
|
|
||||||
ISystemUser,
|
|
||||||
ITenant,
|
|
||||||
} from '@/interfaces';
|
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
|
||||||
import events from '@/subscribers/events';
|
|
||||||
import config from '../../config';
|
|
||||||
import TenantsManager from '@/services/Tenancy/TenantsManager';
|
|
||||||
import { Tenant } from '@/system/models';
|
|
||||||
import OrganizationBaseCurrencyLocking from './OrganizationBaseCurrencyLocking';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import { ERRORS } from './constants';
|
|
||||||
import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware';
|
|
||||||
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class OrganizationService {
|
|
||||||
@Inject()
|
|
||||||
private eventPublisher: EventPublisher;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private tenantsManager: TenantsManager;
|
|
||||||
|
|
||||||
@Inject('agenda')
|
|
||||||
private agenda: any;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private baseCurrencyMutateLocking: OrganizationBaseCurrencyLocking;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the database schema and seed data of the given organization id.
|
|
||||||
* @param {srting} organizationId
|
|
||||||
* @return {Promise<void>}
|
|
||||||
*/
|
|
||||||
public async build(
|
|
||||||
tenantId: number,
|
|
||||||
buildDTO: IOrganizationBuildDTO,
|
|
||||||
systemUser: ISystemUser
|
|
||||||
): Promise<void> {
|
|
||||||
const tenant = await this.getTenantOrThrowError(tenantId);
|
|
||||||
|
|
||||||
// Throw error if the tenant is already initialized.
|
|
||||||
this.throwIfTenantInitizalized(tenant);
|
|
||||||
|
|
||||||
// Drop the database if is already exists.
|
|
||||||
await this.tenantsManager.dropDatabaseIfExists(tenant);
|
|
||||||
|
|
||||||
// Creates a new database.
|
|
||||||
await this.tenantsManager.createDatabase(tenant);
|
|
||||||
|
|
||||||
// Migrate the tenant.
|
|
||||||
await this.tenantsManager.migrateTenant(tenant);
|
|
||||||
|
|
||||||
// Migrated tenant.
|
|
||||||
const migratedTenant = await tenant.$query().withGraphFetched('metadata');
|
|
||||||
|
|
||||||
// Injects the given tenant IoC services.
|
|
||||||
await initalizeTenantServices(tenantId);
|
|
||||||
await initializeTenantSettings(tenantId);
|
|
||||||
|
|
||||||
// Creates a tenancy object from given tenant model.
|
|
||||||
const tenancyContext =
|
|
||||||
this.tenantsManager.getSeedMigrationContext(migratedTenant);
|
|
||||||
|
|
||||||
// Seed tenant.
|
|
||||||
await this.tenantsManager.seedTenant(migratedTenant, tenancyContext);
|
|
||||||
|
|
||||||
// Throws `onOrganizationBuild` event.
|
|
||||||
await this.eventPublisher.emitAsync(events.organization.build, {
|
|
||||||
tenantId: tenant.id,
|
|
||||||
buildDTO,
|
|
||||||
systemUser,
|
|
||||||
} as IOrganizationBuildEventPayload);
|
|
||||||
|
|
||||||
// Markes the tenant as completed builing.
|
|
||||||
await Tenant.markAsBuilt(tenantId);
|
|
||||||
await Tenant.markAsBuildCompleted(tenantId);
|
|
||||||
|
|
||||||
//
|
|
||||||
await this.flagTenantDBBatch(tenantId);
|
|
||||||
|
|
||||||
// Triggers the organization built event.
|
|
||||||
await this.eventPublisher.emitAsync(events.organization.built, {
|
|
||||||
tenantId: tenant.id,
|
|
||||||
} as IOrganizationBuiltEventPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {IOrganizationBuildDTO} buildDTO
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async buildRunJob(
|
|
||||||
tenantId: number,
|
|
||||||
buildDTO: IOrganizationBuildDTO,
|
|
||||||
authorizedUser: ISystemUser
|
|
||||||
) {
|
|
||||||
const tenant = await this.getTenantOrThrowError(tenantId);
|
|
||||||
|
|
||||||
// Throw error if the tenant is already initialized.
|
|
||||||
this.throwIfTenantInitizalized(tenant);
|
|
||||||
|
|
||||||
// Throw error if tenant is currently building.
|
|
||||||
this.throwIfTenantIsBuilding(tenant);
|
|
||||||
|
|
||||||
// Transformes build DTO object.
|
|
||||||
const transformedBuildDTO = this.transformBuildDTO(buildDTO);
|
|
||||||
|
|
||||||
// Saves the tenant metadata.
|
|
||||||
await tenant.saveMetadata(transformedBuildDTO);
|
|
||||||
|
|
||||||
// Send welcome mail to the user.
|
|
||||||
const jobMeta = await this.agenda.now('organization-setup', {
|
|
||||||
tenantId,
|
|
||||||
buildDTO,
|
|
||||||
authorizedUser,
|
|
||||||
});
|
|
||||||
// Transformes the mangodb id to string.
|
|
||||||
const jobId = new ObjectId(jobMeta.attrs._id).toString();
|
|
||||||
|
|
||||||
// Markes the tenant as currently building.
|
|
||||||
await Tenant.markAsBuilding(tenantId, jobId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
nextRunAt: jobMeta.attrs.nextRunAt,
|
|
||||||
jobId: jobMeta.attrs._id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unlocks tenant build run job.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} jobId
|
|
||||||
*/
|
|
||||||
public async revertBuildRunJob(tenantId: number, jobId: string) {
|
|
||||||
await Tenant.markAsBuildCompleted(tenantId, jobId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the current organization metadata.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @returns {Promise<ITenant[]>}
|
|
||||||
*/
|
|
||||||
public async currentOrganization(tenantId: number): Promise<ITenant> {
|
|
||||||
const tenant = await Tenant.query()
|
|
||||||
.findById(tenantId)
|
|
||||||
.withGraphFetched('subscriptions')
|
|
||||||
.withGraphFetched('metadata');
|
|
||||||
|
|
||||||
this.throwIfTenantNotExists(tenant);
|
|
||||||
|
|
||||||
return tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve organization ability of mutate base currency
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
public mutateBaseCurrencyAbility(tenantId: number) {
|
|
||||||
return this.baseCurrencyMutateLocking.baseCurrencyMutateLocks(tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates organization information.
|
|
||||||
* @param {ITenant} tenantId
|
|
||||||
* @param {IOrganizationUpdateDTO} organizationDTO
|
|
||||||
*/
|
|
||||||
public async updateOrganization(
|
|
||||||
tenantId: number,
|
|
||||||
organizationDTO: IOrganizationUpdateDTO
|
|
||||||
): Promise<void> {
|
|
||||||
const tenant = await Tenant.query()
|
|
||||||
.findById(tenantId)
|
|
||||||
.withGraphFetched('metadata');
|
|
||||||
|
|
||||||
// Throw error if the tenant not exists.
|
|
||||||
this.throwIfTenantNotExists(tenant);
|
|
||||||
|
|
||||||
// Validate organization transactions before mutate base currency.
|
|
||||||
if (organizationDTO.baseCurrency) {
|
|
||||||
await this.validateMutateBaseCurrency(
|
|
||||||
tenant,
|
|
||||||
organizationDTO.baseCurrency,
|
|
||||||
tenant.metadata?.baseCurrency
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await tenant.saveMetadata(organizationDTO);
|
|
||||||
|
|
||||||
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {
|
|
||||||
// Triggers `onOrganizationBaseCurrencyUpdated` event.
|
|
||||||
await this.eventPublisher.emitAsync(
|
|
||||||
events.organization.baseCurrencyUpdated,
|
|
||||||
{
|
|
||||||
tenantId,
|
|
||||||
organizationDTO,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transformes build DTO object.
|
|
||||||
* @param {IOrganizationBuildDTO} buildDTO
|
|
||||||
* @returns {IOrganizationBuildDTO}
|
|
||||||
*/
|
|
||||||
private transformBuildDTO(
|
|
||||||
buildDTO: IOrganizationBuildDTO
|
|
||||||
): IOrganizationBuildDTO {
|
|
||||||
return {
|
|
||||||
...buildDTO,
|
|
||||||
dateFormat: defaultTo(buildDTO.dateFormat, 'DD MMM yyyy'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throw base currency mutate locked error.
|
|
||||||
*/
|
|
||||||
private throwBaseCurrencyMutateLocked() {
|
|
||||||
throw new ServiceError(ERRORS.BASE_CURRENCY_MUTATE_LOCKED);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate mutate base currency ability.
|
|
||||||
* @param {Tenant} tenant -
|
|
||||||
* @param {string} newBaseCurrency -
|
|
||||||
* @param {string} oldBaseCurrency -
|
|
||||||
*/
|
|
||||||
private async validateMutateBaseCurrency(
|
|
||||||
tenant: Tenant,
|
|
||||||
newBaseCurrency: string,
|
|
||||||
oldBaseCurrency: string
|
|
||||||
) {
|
|
||||||
if (tenant.isReady && newBaseCurrency !== oldBaseCurrency) {
|
|
||||||
const isLocked =
|
|
||||||
await this.baseCurrencyMutateLocking.isBaseCurrencyMutateLocked(
|
|
||||||
tenant.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLocked) {
|
|
||||||
this.throwBaseCurrencyMutateLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throws error in case the given tenant is undefined.
|
|
||||||
* @param {ITenant} tenant
|
|
||||||
*/
|
|
||||||
private throwIfTenantNotExists(tenant: ITenant) {
|
|
||||||
if (!tenant) {
|
|
||||||
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throws error in case the given tenant is already initialized.
|
|
||||||
* @param {ITenant} tenant
|
|
||||||
*/
|
|
||||||
private throwIfTenantInitizalized(tenant: ITenant) {
|
|
||||||
if (tenant.builtAt) {
|
|
||||||
throw new ServiceError(ERRORS.TENANT_ALREADY_BUILT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throw error if the tenant is building.
|
|
||||||
* @param {ITenant} tenant
|
|
||||||
*/
|
|
||||||
private throwIfTenantIsBuilding(tenant) {
|
|
||||||
if (tenant.buildJobId) {
|
|
||||||
throw new ServiceError(ERRORS.TENANT_IS_BUILDING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve tenant of throw not found error.
|
|
||||||
* @param {number} tenantId -
|
|
||||||
*/
|
|
||||||
async getTenantOrThrowError(tenantId: number): Promise<ITenant> {
|
|
||||||
const tenant = await Tenant.query().findById(tenantId);
|
|
||||||
|
|
||||||
if (!tenant) {
|
|
||||||
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
return tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds organization database latest batch number.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} version
|
|
||||||
*/
|
|
||||||
public async flagTenantDBBatch(tenantId: number) {
|
|
||||||
await Tenant.query()
|
|
||||||
.update({
|
|
||||||
databaseBatch: config.databaseBatch,
|
|
||||||
})
|
|
||||||
.where({ id: tenantId });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Syncs system user to tenant user.
|
|
||||||
*/
|
|
||||||
public async syncSystemUserToTenant(
|
|
||||||
tenantId: number,
|
|
||||||
systemUser: ISystemUser
|
|
||||||
) {
|
|
||||||
const { User, Role } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const adminRole = await Role.query().findOne('slug', 'admin');
|
|
||||||
|
|
||||||
await User.query().insert({
|
|
||||||
...pick(systemUser, [
|
|
||||||
'firstName',
|
|
||||||
'lastName',
|
|
||||||
'phoneNumber',
|
|
||||||
'email',
|
|
||||||
'active',
|
|
||||||
'inviteAcceptedAt',
|
|
||||||
]),
|
|
||||||
systemUserId: systemUser.id,
|
|
||||||
roleId: adminRole.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { ERRORS } from './constants';
|
||||||
|
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw base currency mutate locked error.
|
||||||
|
*/
|
||||||
|
export function throwBaseCurrencyMutateLocked() {
|
||||||
|
throw new ServiceError(ERRORS.BASE_CURRENCY_MUTATE_LOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws error in case the given tenant is undefined.
|
||||||
|
* @param {TenantModel} tenant
|
||||||
|
*/
|
||||||
|
export function throwIfTenantNotExists(tenant: TenantModel) {
|
||||||
|
if (!tenant) {
|
||||||
|
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws error in case the given tenant is already initialized.
|
||||||
|
* @param {TenantModel} tenant
|
||||||
|
*/
|
||||||
|
export function throwIfTenantInitizalized(tenant: TenantModel) {
|
||||||
|
if (tenant.builtAt) {
|
||||||
|
throw new ServiceError(ERRORS.TENANT_ALREADY_BUILT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw error if the tenant is building.
|
||||||
|
* @param {TenantModel} tenant
|
||||||
|
*/
|
||||||
|
export function throwIfTenantIsBuilding(tenant: TenantModel) {
|
||||||
|
if (tenant.buildJobId) {
|
||||||
|
throw new ServiceError(ERRORS.TENANT_IS_BUILDING);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
IOrganizationBuildDTO,
|
||||||
|
IOrganizationBuildEventPayload,
|
||||||
|
IOrganizationBuiltEventPayload,
|
||||||
|
} from '../Organization.types';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
import { throwIfTenantInitizalized, throwIfTenantIsBuilding } from '../Organization/_utils';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { TenantsManagerService } from '@/modules/TenantDBManager/TenantsManager';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { transformBuildDto } from '../Organization.utils';
|
||||||
|
import { BuildOrganizationDto } from '../dtos/Organization.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BuildOrganizationService {
|
||||||
|
constructor(
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
private readonly tenantsManager: TenantsManagerService,
|
||||||
|
private readonly tenancyContext: TenancyContext
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the database schema and seed data of the given organization id.
|
||||||
|
* @param {srting} organizationId
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async build(
|
||||||
|
buildDTO: BuildOrganizationDto,
|
||||||
|
): Promise<void> {
|
||||||
|
const tenant = await this.tenancyContext.getTenant();
|
||||||
|
const systemUser = await this.tenancyContext.getSystemUser();
|
||||||
|
|
||||||
|
// Throw error if the tenant is already initialized.
|
||||||
|
throwIfTenantInitizalized(tenant);
|
||||||
|
|
||||||
|
// Drop the database if is already exists.
|
||||||
|
await this.tenantsManager.dropDatabaseIfExists(tenant);
|
||||||
|
|
||||||
|
// Creates a new database.
|
||||||
|
await this.tenantsManager.createDatabase(tenant);
|
||||||
|
|
||||||
|
// Migrate the tenant.
|
||||||
|
await this.tenantsManager.migrateTenant(tenant);
|
||||||
|
|
||||||
|
// Migrated tenant.
|
||||||
|
const migratedTenant = await tenant.$query().withGraphFetched('metadata');
|
||||||
|
|
||||||
|
// Creates a tenancy object from given tenant model.
|
||||||
|
const tenancyContext =
|
||||||
|
this.tenantsManager.getSeedMigrationContext(migratedTenant);
|
||||||
|
|
||||||
|
// Seed tenant.
|
||||||
|
await this.tenantsManager.seedTenant(migratedTenant, tenancyContext);
|
||||||
|
|
||||||
|
// Throws `onOrganizationBuild` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.organization.build, {
|
||||||
|
tenantId: tenant.id,
|
||||||
|
buildDTO,
|
||||||
|
systemUser,
|
||||||
|
} as IOrganizationBuildEventPayload);
|
||||||
|
|
||||||
|
// Markes the tenant as completed builing.
|
||||||
|
await Tenant.markAsBuilt(tenantId);
|
||||||
|
await Tenant.markAsBuildCompleted(tenantId);
|
||||||
|
|
||||||
|
//
|
||||||
|
await this.flagTenantDBBatch(tenantId);
|
||||||
|
|
||||||
|
// Triggers the organization built event.
|
||||||
|
await this.eventPublisher.emitAsync(events.organization.built, {
|
||||||
|
tenantId: tenant.id,
|
||||||
|
} as IOrganizationBuiltEventPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IOrganizationBuildDTO} buildDTO
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async buildRunJob(
|
||||||
|
buildDTO: BuildOrganizationDto,
|
||||||
|
) {
|
||||||
|
const tenant = await this.tenancyContext.getTenant();
|
||||||
|
const systemUser = await this.tenancyContext.getSystemUser();
|
||||||
|
|
||||||
|
// Throw error if the tenant is already initialized.
|
||||||
|
throwIfTenantInitizalized(tenant);
|
||||||
|
|
||||||
|
// Throw error if tenant is currently building.
|
||||||
|
throwIfTenantIsBuilding(tenant);
|
||||||
|
|
||||||
|
// Transformes build DTO object.
|
||||||
|
const transformedBuildDTO = transformBuildDto(buildDTO);
|
||||||
|
|
||||||
|
// Saves the tenant metadata.
|
||||||
|
await tenant.saveMetadata(transformedBuildDTO);
|
||||||
|
|
||||||
|
// Send welcome mail to the user.
|
||||||
|
const jobMeta = await this.agenda.now('organization-setup', {
|
||||||
|
tenantId,
|
||||||
|
buildDTO,
|
||||||
|
authorizedUser,
|
||||||
|
});
|
||||||
|
// Transformes the mangodb id to string.
|
||||||
|
const jobId = new ObjectId(jobMeta.attrs._id).toString();
|
||||||
|
|
||||||
|
// Markes the tenant as currently building.
|
||||||
|
await Tenant.markAsBuilding(tenantId, jobId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nextRunAt: jobMeta.attrs.nextRunAt,
|
||||||
|
jobId: jobMeta.attrs._id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlocks tenant build run job.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} jobId
|
||||||
|
*/
|
||||||
|
public async revertBuildRunJob() {
|
||||||
|
// await Tenant.markAsBuildCompleted(tenantId, jobId);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds organization database latest batch number.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} version
|
||||||
|
*/
|
||||||
|
public async flagTenantDBBatch(tenantId: number) {
|
||||||
|
await Tenant.query()
|
||||||
|
.update({
|
||||||
|
databaseBatch: config.databaseBatch,
|
||||||
|
})
|
||||||
|
.where({ id: tenantId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { TenancyContext } from "@/modules/Tenancy/TenancyContext.service";
|
||||||
|
import { UpdateOrganizationDto } from "../dtos/Organization.dto";
|
||||||
|
import { throwIfTenantNotExists } from "../Organization/_utils";
|
||||||
|
|
||||||
|
|
||||||
|
export class UpdateOrganizationService {
|
||||||
|
constructor(
|
||||||
|
private readonly tenancyContext: TenancyContext
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates organization information.
|
||||||
|
* @param {ITenant} tenantId
|
||||||
|
* @param {IOrganizationUpdateDTO} organizationDTO
|
||||||
|
*/
|
||||||
|
public async execute(
|
||||||
|
organizationDTO: UpdateOrganizationDto,
|
||||||
|
): Promise<void> {
|
||||||
|
const tenant = await this.tenancyContext.getTenant(true);
|
||||||
|
|
||||||
|
// Throw error if the tenant not exists.
|
||||||
|
throwIfTenantNotExists(tenant);
|
||||||
|
|
||||||
|
// Validate organization transactions before mutate base currency.
|
||||||
|
if (organizationDTO.baseCurrency) {
|
||||||
|
await this.validateMutateBaseCurrency(
|
||||||
|
tenant,
|
||||||
|
organizationDTO.baseCurrency,
|
||||||
|
tenant.metadata?.baseCurrency,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await tenant.saveMetadata(organizationDTO);
|
||||||
|
|
||||||
|
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {
|
||||||
|
// Triggers `onOrganizationBaseCurrencyUpdated` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.organization.baseCurrencyUpdated,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
organizationDTO,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import {IsHexColor,
|
||||||
|
IsIn,
|
||||||
|
IsISO31661Alpha2,
|
||||||
|
IsISO4217CurrencyCode,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { MONTHS } from '../Organization/constants';
|
||||||
|
import { ACCEPTED_LOCALES, DATE_FORMATS } from '../Organization.constants';
|
||||||
|
|
||||||
|
export class BuildOrganizationDto {
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
industry?: string;
|
||||||
|
|
||||||
|
@IsISO31661Alpha2()
|
||||||
|
location: string;
|
||||||
|
|
||||||
|
@IsISO4217CurrencyCode()
|
||||||
|
baseCurrency: string;
|
||||||
|
|
||||||
|
@IsIn(moment.tz.names())
|
||||||
|
timezone: string;
|
||||||
|
|
||||||
|
@IsIn(MONTHS)
|
||||||
|
fiscalYear: string;
|
||||||
|
|
||||||
|
@IsIn(ACCEPTED_LOCALES)
|
||||||
|
language: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(DATE_FORMATS)
|
||||||
|
dateFormat?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateOrganizationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
industry?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsISO31661Alpha2()
|
||||||
|
location?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsISO4217CurrencyCode()
|
||||||
|
baseCurrency?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(moment.tz.names())
|
||||||
|
timezone?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(MONTHS)
|
||||||
|
fiscalYear?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(ACCEPTED_LOCALES)
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(DATE_FORMATS)
|
||||||
|
dateFormat?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
address?: {
|
||||||
|
address_1?: string;
|
||||||
|
address_2?: string;
|
||||||
|
postal_code?: string;
|
||||||
|
city?: string;
|
||||||
|
stateProvince?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsHexColor()
|
||||||
|
primaryColor?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
logoKey?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
taxNumber?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||||
|
import { Scope } from '@nestjs/common';
|
||||||
|
import { Process } from '@nestjs/bull';
|
||||||
|
import { Job } from 'bullmq';
|
||||||
|
import {
|
||||||
|
OrganizationBuildQueue,
|
||||||
|
OrganizationBuildQueueJob,
|
||||||
|
OrganizationBuildQueueJobPayload,
|
||||||
|
} from '../Organization.types';
|
||||||
|
import { BuildOrganizationService } from '../commands/BuildOrganization.service';
|
||||||
|
|
||||||
|
@Processor({
|
||||||
|
name: OrganizationBuildQueue,
|
||||||
|
scope: Scope.REQUEST,
|
||||||
|
})
|
||||||
|
export class OrganizationBuildProcessor extends WorkerHost {
|
||||||
|
constructor(
|
||||||
|
private readonly organizationBuildService: BuildOrganizationService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Process(OrganizationBuildQueueJob)
|
||||||
|
async process(job: Job<OrganizationBuildQueueJobPayload>) {
|
||||||
|
try {
|
||||||
|
await this.organizationBuildService.build(job.data.buildDto);
|
||||||
|
} catch (e) {
|
||||||
|
// Unlock build status of the tenant.
|
||||||
|
await this.organizationBuildService.revertBuildRunJob();
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
import { throwIfTenantNotExists } from '../Organization/_utils';
|
||||||
|
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetCurrentOrganizationService {
|
||||||
|
constructor(private readonly tenancyContext: TenancyContext) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the current organization metadata.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns {Promise<ITenant[]>}
|
||||||
|
*/
|
||||||
|
async getCurrentOrganization(): Promise<TenantModel> {
|
||||||
|
const tenant = await this.tenancyContext
|
||||||
|
.getTenant()
|
||||||
|
.withGraphFetched('subscriptions')
|
||||||
|
.withGraphFetched('metadata');
|
||||||
|
|
||||||
|
throwIfTenantNotExists(tenant);
|
||||||
|
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { PaymentLinksApplication } from './PaymentLinksApplication';
|
|||||||
import { PaymentLinksController } from './PaymentLinks.controller';
|
import { PaymentLinksController } from './PaymentLinks.controller';
|
||||||
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
||||||
import { PaymentLink } from './models/PaymentLink';
|
import { PaymentLink } from './models/PaymentLink';
|
||||||
|
import { StripePaymentModule } from '../StripePayment/StripePayment.module';
|
||||||
|
|
||||||
const models = [InjectSystemModel(PaymentLink)];
|
const models = [InjectSystemModel(PaymentLink)];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [StripePaymentModule],
|
||||||
providers: [
|
providers: [
|
||||||
...models,
|
...models,
|
||||||
CreateInvoiceCheckoutSession,
|
CreateInvoiceCheckoutSession,
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import { SaleInvoiceAction } from "../SaleInvoices/SaleInvoice.types";
|
|||||||
import { CreditNoteAction } from "../CreditNotes/types/CreditNotes.types";
|
import { CreditNoteAction } from "../CreditNotes/types/CreditNotes.types";
|
||||||
import { SaleReceiptAction } from "../SaleReceipts/types/SaleReceipts.types";
|
import { SaleReceiptAction } from "../SaleReceipts/types/SaleReceipts.types";
|
||||||
import { BillAction } from "../Bills/Bills.types";
|
import { BillAction } from "../Bills/Bills.types";
|
||||||
import { AbilitySubject, ISubjectAbilitiesSchema } from "./Roles.types";
|
import { AbilitySubject, ISubjectAbilitiesSchema, ISubjectAbilitySchema } from "./Roles.types";
|
||||||
|
import { PaymentReceiveAction } from "../PaymentReceived/types/PaymentReceived.types";
|
||||||
|
import { PreferencesAction } from "../Settings/Settings.types";
|
||||||
|
|
||||||
export const AbilitySchema: ISubjectAbilitiesSchema[] = [
|
export const AbilitySchema: ISubjectAbilitiesSchema[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,4 +10,8 @@ export interface ISettingsDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const SETTINGS_PROVIDER = 'SETTINGS';
|
export const SETTINGS_PROVIDER = 'SETTINGS';
|
||||||
|
|
||||||
|
export enum PreferencesAction {
|
||||||
|
Mutate = 'Mutate'
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ const models = [InjectSystemModel(PaymentIntegration)];
|
|||||||
StripeWebhooksSubscriber,
|
StripeWebhooksSubscriber,
|
||||||
TenancyContext,
|
TenancyContext,
|
||||||
],
|
],
|
||||||
|
exports: [StripePaymentService],
|
||||||
controllers: [StripeIntegrationController],
|
controllers: [StripeIntegrationController],
|
||||||
})
|
})
|
||||||
export class StripePaymentModule {}
|
export class StripePaymentModule {}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class SubscriptionApplication {
|
|||||||
* @param variantId
|
* @param variantId
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getLemonSqueezyCheckaoutUri(variantId: number) {
|
getLemonSqueezyCheckoutUri(variantId: number) {
|
||||||
return this.getLemonSqueezyCheckoutService.getCheckout(variantId);
|
return this.getLemonSqueezyCheckoutService.getCheckout(variantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,52 +6,28 @@ import {
|
|||||||
Req,
|
Req,
|
||||||
Res,
|
Res,
|
||||||
Next,
|
Next,
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { ApiOperation, ApiTags, ApiResponse, ApiBody } from '@nestjs/swagger';
|
||||||
import { SubscriptionApplication } from './SubscriptionApplication';
|
import { SubscriptionApplication } from './SubscriptionApplication';
|
||||||
import {
|
|
||||||
ApiOperation,
|
|
||||||
ApiTags,
|
|
||||||
ApiResponse,
|
|
||||||
ApiBody,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { Subscription } from './Subscription';
|
|
||||||
import { LemonSqueezy } from './LemonSqueezy';
|
|
||||||
|
|
||||||
@Controller('subscription')
|
@Controller('subscription')
|
||||||
@ApiTags('subscriptions')
|
@ApiTags('subscriptions')
|
||||||
export class SubscriptionsController {
|
export class SubscriptionsController {
|
||||||
constructor(
|
constructor(private readonly subscriptionApp: SubscriptionApplication) {}
|
||||||
private readonly subscriptionService: Subscription,
|
|
||||||
private readonly lemonSqueezyService: LemonSqueezy,
|
|
||||||
private readonly subscriptionApp: SubscriptionApplication,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve all subscriptions of the authenticated user's tenant.
|
|
||||||
*/
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Get all subscriptions for the current tenant' })
|
@ApiOperation({ summary: 'Get all subscriptions for the current tenant' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'List of subscriptions retrieved successfully',
|
description: 'List of subscriptions retrieved successfully',
|
||||||
})
|
})
|
||||||
async getSubscriptions(
|
async getSubscriptions(@Res() res: Response) {
|
||||||
@Req() req: Request,
|
const subscriptions = await this.subscriptionApp.getSubscriptions();
|
||||||
@Res() res: Response,
|
|
||||||
@Next() next: NextFunction,
|
|
||||||
) {
|
|
||||||
const tenantId = req.headers['organization-id'] as string;
|
|
||||||
const subscriptions =
|
|
||||||
await this.subscriptionService.getSubscriptions(tenantId);
|
|
||||||
|
|
||||||
return res.status(200).send({ subscriptions });
|
return res.status(200).send({ subscriptions });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the LemonSqueezy checkout url.
|
|
||||||
*/
|
|
||||||
@Post('lemon/checkout_url')
|
@Post('lemon/checkout_url')
|
||||||
@ApiOperation({ summary: 'Get LemonSqueezy checkout URL' })
|
@ApiOperation({ summary: 'Get LemonSqueezy checkout URL' })
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
@@ -71,22 +47,15 @@ export class SubscriptionsController {
|
|||||||
description: 'Checkout URL retrieved successfully',
|
description: 'Checkout URL retrieved successfully',
|
||||||
})
|
})
|
||||||
async getCheckoutUrl(
|
async getCheckoutUrl(
|
||||||
@Body('variantId') variantId: string,
|
@Body('variantId') variantId: number,
|
||||||
@Req() req: Request,
|
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
@Next() next: NextFunction,
|
|
||||||
) {
|
) {
|
||||||
const user = req.user;
|
const checkout =
|
||||||
const checkout = await this.lemonSqueezyService.getCheckout(
|
await this.subscriptionApp.getLemonSqueezyCheckoutUri(variantId);
|
||||||
variantId,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
return res.status(200).send(checkout);
|
return res.status(200).send(checkout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the subscription of the current organization.
|
|
||||||
*/
|
|
||||||
@Post('cancel')
|
@Post('cancel')
|
||||||
@ApiOperation({ summary: 'Cancel the current organization subscription' })
|
@ApiOperation({ summary: 'Cancel the current organization subscription' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
@@ -107,9 +76,6 @@ export class SubscriptionsController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resumes the subscription of the current organization.
|
|
||||||
*/
|
|
||||||
@Post('resume')
|
@Post('resume')
|
||||||
@ApiOperation({ summary: 'Resume the current organization subscription' })
|
@ApiOperation({ summary: 'Resume the current organization subscription' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
@@ -130,9 +96,6 @@ export class SubscriptionsController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the main subscription plan of the current organization.
|
|
||||||
*/
|
|
||||||
@Post('change')
|
@Post('change')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Change the subscription plan of the current organization',
|
summary: 'Change the subscription plan of the current organization',
|
||||||
@@ -155,12 +118,9 @@ export class SubscriptionsController {
|
|||||||
})
|
})
|
||||||
async changeSubscriptionPlan(
|
async changeSubscriptionPlan(
|
||||||
@Body('variant_id') variantId: number,
|
@Body('variant_id') variantId: number,
|
||||||
@Req() req: Request,
|
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
@Next() next: NextFunction,
|
|
||||||
) {
|
) {
|
||||||
const tenantId = req.headers['organization-id'] as string;
|
await this.subscriptionApp.changeSubscriptionPlan(variantId);
|
||||||
await this.subscriptionApp.changeSubscriptionPlan(tenantId, variantId);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'The subscription plan has been changed.',
|
message: 'The subscription plan has been changed.',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { SystemModel } from '@/modules/System/models/SystemModel';
|
|||||||
import { Model, mixin } from 'objection';
|
import { Model, mixin } from 'objection';
|
||||||
|
|
||||||
export class Plan extends mixin(SystemModel) {
|
export class Plan extends mixin(SystemModel) {
|
||||||
|
public readonly slug: string;
|
||||||
public readonly price: number;
|
public readonly price: number;
|
||||||
public readonly invoiceInternal: number;
|
public readonly invoiceInternal: number;
|
||||||
public readonly invoicePeriod: string;
|
public readonly invoicePeriod: string;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Subscription } from '../Subscription';
|
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { SubscriptionApplication } from '../SubscriptionApplication';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubscribeFreeOnSignupCommunity {
|
export class SubscribeFreeOnSignupCommunity {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly subscriptionService: Subscription,
|
private readonly subscriptionApp: SubscriptionApplication,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ export class SubscribeFreeOnSignupCommunity {
|
|||||||
signupDTO,
|
signupDTO,
|
||||||
tenant,
|
tenant,
|
||||||
user,
|
user,
|
||||||
}: IAuthSignedUpEventPayload) {
|
}) {
|
||||||
if (this.configService.get('hostedOnBigcapitalCloud')) return null;
|
if (this.configService.get('hostedOnBigcapitalCloud')) return null;
|
||||||
|
|
||||||
await this.subscriptionService.newSubscribtion(tenant.id, 'free');
|
await this.subscriptionApp.createNewSubscription('free');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export class TriggerInvalidateCacheOnSubscriptionChange {
|
|||||||
@OnEvent(events.subscription.onSubscriptionResumed)
|
@OnEvent(events.subscription.onSubscriptionResumed)
|
||||||
@OnEvent(events.subscription.onSubscriptionPlanChanged)
|
@OnEvent(events.subscription.onSubscriptionPlanChanged)
|
||||||
triggerInvalidateCache() {
|
triggerInvalidateCache() {
|
||||||
const io = Container.get('socket');
|
// const io = Container.get('socket');
|
||||||
|
|
||||||
// Notify the frontend to reflect the new transactions changes.
|
// // Notify the frontend to reflect the new transactions changes.
|
||||||
io.emit('SUBSCRIPTION_CHANGED', { subscriptionSlug: 'main' });
|
// io.emit('SUBSCRIPTION_CHANGED', { subscriptionSlug: 'main' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
compareSignatures,
|
compareSignatures,
|
||||||
configureLemonSqueezy,
|
configureLemonSqueezy,
|
||||||
@@ -6,14 +7,17 @@ import {
|
|||||||
webhookHasData,
|
webhookHasData,
|
||||||
webhookHasMeta,
|
webhookHasMeta,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { Subscription } from '../Subscription';
|
import { Plan } from '../models/Plan';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { SubscriptionApplication } from '../SubscriptionApplication';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LemonSqueezyWebhooks {
|
export class LemonSqueezyWebhooks {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly subscriptionService: Subscription,
|
private readonly subscriptionApp: SubscriptionApplication,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
|
||||||
|
@Inject(Plan.name)
|
||||||
|
private readonly planModel: typeof Plan,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,14 +72,12 @@ export class LemonSqueezyWebhooks {
|
|||||||
if (webhookEvent.startsWith('subscription_payment_')) {
|
if (webhookEvent.startsWith('subscription_payment_')) {
|
||||||
// Marks the main subscription payment as succeed.
|
// Marks the main subscription payment as succeed.
|
||||||
if (webhookEvent === 'subscription_payment_success') {
|
if (webhookEvent === 'subscription_payment_success') {
|
||||||
await this.subscriptionService.markSubscriptionPaymentSucceed(
|
await this.subscriptionApp.markSubscriptionPaymentSuccessed(
|
||||||
tenantId,
|
|
||||||
subscriptionSlug,
|
subscriptionSlug,
|
||||||
);
|
);
|
||||||
// Marks the main subscription payment as failed.
|
// Marks the main subscription payment as failed.
|
||||||
} else if (webhookEvent === 'subscription_payment_failed') {
|
} else if (webhookEvent === 'subscription_payment_failed') {
|
||||||
await this.subscriptionService.markSubscriptionPaymentFailed(
|
await this.subscriptionApp.markSubscriptionPaymentFailed(
|
||||||
tenantId,
|
|
||||||
subscriptionSlug,
|
subscriptionSlug,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,9 @@ export class LemonSqueezyWebhooks {
|
|||||||
const variantId = attributes.variant_id as string;
|
const variantId = attributes.variant_id as string;
|
||||||
|
|
||||||
// We assume that the Plan table is up to date.
|
// We assume that the Plan table is up to date.
|
||||||
const plan = await Plan.query().findOne('lemonVariantId', variantId);
|
const plan = await this.planModel
|
||||||
|
.query()
|
||||||
|
.findOne('lemonVariantId', variantId);
|
||||||
|
|
||||||
// Update the subscription in the database.
|
// Update the subscription in the database.
|
||||||
const priceId = attributes.first_subscription_item.price_id;
|
const priceId = attributes.first_subscription_item.price_id;
|
||||||
@@ -99,27 +103,23 @@ export class LemonSqueezyWebhooks {
|
|||||||
}
|
}
|
||||||
// Create a new subscription of the tenant.
|
// Create a new subscription of the tenant.
|
||||||
if (webhookEvent === 'subscription_created') {
|
if (webhookEvent === 'subscription_created') {
|
||||||
await this.subscriptionService.newSubscribtion(
|
await this.subscriptionApp.createNewSubscription(
|
||||||
tenantId,
|
|
||||||
plan.slug,
|
plan.slug,
|
||||||
subscriptionSlug,
|
subscriptionSlug,
|
||||||
{ lemonSqueezyId: subscriptionId },
|
{ lemonSqueezyId: subscriptionId },
|
||||||
);
|
);
|
||||||
// Cancel the given subscription of the organization.
|
// Cancel the given subscription of the organization.
|
||||||
} else if (webhookEvent === 'subscription_cancelled') {
|
} else if (webhookEvent === 'subscription_cancelled') {
|
||||||
await this.subscriptionService.cancelSubscription(
|
await this.subscriptionApp.cancelSubscription(
|
||||||
tenantId,
|
|
||||||
subscriptionSlug,
|
subscriptionSlug,
|
||||||
);
|
);
|
||||||
} else if (webhookEvent === 'subscription_plan_changed') {
|
} else if (webhookEvent === 'subscription_plan_changed') {
|
||||||
await this.subscriptionService.subscriptionPlanChanged(
|
await this.subscriptionApp.markSubscriptionPlanChanged(
|
||||||
tenantId,
|
|
||||||
plan.slug,
|
plan.slug,
|
||||||
subscriptionSlug,
|
subscriptionSlug,
|
||||||
);
|
);
|
||||||
} else if (webhookEvent === 'subscription_resumed') {
|
} else if (webhookEvent === 'subscription_resumed') {
|
||||||
await this.subscriptionService.resumeSubscription(
|
await this.subscriptionApp.resumeSubscription(
|
||||||
tenantId,
|
|
||||||
subscriptionSlug,
|
subscriptionSlug,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export class TenantModel extends BaseModel {
|
|||||||
public readonly seededAt: boolean;
|
public readonly seededAt: boolean;
|
||||||
public readonly builtAt: string;
|
public readonly builtAt: string;
|
||||||
public readonly metadata: TenantMetadata;
|
public readonly metadata: TenantMetadata;
|
||||||
|
public readonly buildJobId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class TenantDBManagerModule {}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { Knex, knex } from 'knex';
|
||||||
|
import { knexSnakeCaseMappers } from 'objection';
|
||||||
|
import { TenantDBAlreadyExists } from './exceptions/TenantDBAlreadyExists';
|
||||||
|
import { sanitizeDatabaseName } from '@/utils/sanitize-database-name';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { SystemKnexConnection } from '../System/SystemDB/SystemDB.constants';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class TenantDBManager {
|
||||||
|
static knexCache: { [key: string]: Knex } = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
|
||||||
|
@Inject(SystemKnexConnection)
|
||||||
|
private readonly systemKnex: Knex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the tenant database name.
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
private getDatabaseName(tenant: ITenant) {
|
||||||
|
return sanitizeDatabaseName(
|
||||||
|
`${this.configService.get('tenant.db_name_prefix')}${tenant.organizationId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the tenant database weather exists.
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
public async databaseExists(tenant: ITenant) {
|
||||||
|
const databaseName = this.getDatabaseName(tenant);
|
||||||
|
const results = await this.systemKnex.raw(
|
||||||
|
'SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "' +
|
||||||
|
databaseName +
|
||||||
|
'"',
|
||||||
|
);
|
||||||
|
return results[0].length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a tenant database.
|
||||||
|
* @throws {TenantAlreadyInitialized}
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async createDatabase(tenant: ITenant): Promise<void> {
|
||||||
|
await this.throwErrorIfTenantDBExists(tenant);
|
||||||
|
|
||||||
|
const databaseName = this.getDatabaseName(tenant);
|
||||||
|
await this.systemKnex.raw(
|
||||||
|
`CREATE DATABASE ${databaseName} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dropdowns the tenant database if it was exist.
|
||||||
|
* @param {ITenant} tenant -
|
||||||
|
*/
|
||||||
|
public async dropDatabaseIfExists(tenant: ITenant) {
|
||||||
|
const isExists = await this.databaseExists(tenant);
|
||||||
|
|
||||||
|
if (!isExists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.dropDatabase(tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dropdowns the tenant's database.
|
||||||
|
* @param {ITenant} tenant
|
||||||
|
*/
|
||||||
|
public async dropDatabase(tenant: ITenant) {
|
||||||
|
const databaseName = this.getDatabaseName(tenant);
|
||||||
|
|
||||||
|
await this.systemKnex.raw(`DROP DATABASE IF EXISTS ${databaseName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate tenant database schema to the latest version.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async migrate(tenant: ITenant): Promise<void> {
|
||||||
|
const knex = this.setupKnexInstance(tenant);
|
||||||
|
await knex.migrate.latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds initial data to the tenant database.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async seed(tenant: ITenant): Promise<void> {
|
||||||
|
const knex = this.setupKnexInstance(tenant);
|
||||||
|
|
||||||
|
await knex.migrate.latest({
|
||||||
|
...tenantSeedConfig(tenant),
|
||||||
|
disableMigrationsListValidation: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the knex instance of tenant.
|
||||||
|
* @return {Knex}
|
||||||
|
*/
|
||||||
|
private setupKnexInstance(tenant: ITenant) {
|
||||||
|
const key: string = `${tenant.id}`;
|
||||||
|
let knexInstance = TenantDBManager.knexCache[key];
|
||||||
|
|
||||||
|
if (!knexInstance) {
|
||||||
|
knexInstance = knex({
|
||||||
|
...tenantKnexConfig(tenant),
|
||||||
|
...knexSnakeCaseMappers({ upperCase: true }),
|
||||||
|
});
|
||||||
|
TenantDBManager.knexCache[key] = knexInstance;
|
||||||
|
}
|
||||||
|
return knexInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve knex instance from the givne tenant.
|
||||||
|
*/
|
||||||
|
public getKnexInstance(tenantId: number) {
|
||||||
|
const key: string = `${tenantId}`;
|
||||||
|
let knexInstance = TenantDBManager.knexCache[key];
|
||||||
|
|
||||||
|
if (!knexInstance) {
|
||||||
|
throw new Error('Knex instance is not initialized yut.');
|
||||||
|
}
|
||||||
|
return knexInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws error if the tenant database already exists.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async throwErrorIfTenantDBExists(tenant: ITenant) {
|
||||||
|
const isExists = await this.databaseExists(tenant);
|
||||||
|
if (isExists) {
|
||||||
|
throw new TenantDBAlreadyExists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { TenantDBManager } from "./TenantDBManager";
|
||||||
|
import { EventEmitter2 } from "@nestjs/event-emitter";
|
||||||
|
import { events } from "@/common/events/events";
|
||||||
|
|
||||||
|
// import { Container, Inject, Service } from 'typedi';
|
||||||
|
// import { ITenantManager, ITenant, ITenantDBManager } from '@/interfaces';
|
||||||
|
// import {
|
||||||
|
// EventDispatcherInterface,
|
||||||
|
// EventDispatcher,
|
||||||
|
// } from 'decorators/eventDispatcher';
|
||||||
|
// import {
|
||||||
|
// TenantAlreadyInitialized,
|
||||||
|
// TenantAlreadySeeded,
|
||||||
|
// TenantDatabaseNotBuilt,
|
||||||
|
// } from '@/exceptions';
|
||||||
|
// import TenantDBManager from '@/services/Tenancy/TenantDBManager';
|
||||||
|
// import events from '@/subscribers/events';
|
||||||
|
// import { Tenant } from '@/system/models';
|
||||||
|
// import { SeedMigration } from '@/lib/Seeder/SeedMigration';
|
||||||
|
// import i18n from '../../loaders/i18n';
|
||||||
|
|
||||||
|
// const ERRORS = {
|
||||||
|
// TENANT_ALREADY_CREATED: 'TENANT_ALREADY_CREATED',
|
||||||
|
// TENANT_NOT_EXISTS: 'TENANT_NOT_EXISTS',
|
||||||
|
// };
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TenantsManagerService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly tenantDbManager: TenantDBManager,
|
||||||
|
private readonly eventEmitter: EventEmitter2
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new teant with unique organization id.
|
||||||
|
* @param {ITenant} tenant
|
||||||
|
* @return {Promise<ITenant>}
|
||||||
|
*/
|
||||||
|
public async createTenant(): Promise<ITenant> {
|
||||||
|
const { tenantRepository } = this.sysRepositories;
|
||||||
|
const tenant = await tenantRepository.createWithUniqueOrgId();
|
||||||
|
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new tenant database.
|
||||||
|
* @param {ITenant} tenant -
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async createDatabase(tenant: ITenant): Promise<void> {
|
||||||
|
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||||
|
|
||||||
|
await this.tenantDbManager.createDatabase(tenant);
|
||||||
|
|
||||||
|
await this.eventEmitter.emitAsync(events.tenantManager.databaseCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the database if the given tenant.
|
||||||
|
* @param {number} tenantId
|
||||||
|
*/
|
||||||
|
async dropDatabaseIfExists(tenant: ITenant) {
|
||||||
|
// Drop the database if exists.
|
||||||
|
await this.tenantDbManager.dropDatabaseIfExists(tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the tenant has database.
|
||||||
|
* @param {ITenant} tenant
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
public async hasDatabase(tenant: ITenant): Promise<boolean> {
|
||||||
|
return this.tenantDbManager.databaseExists(tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the tenant database.
|
||||||
|
* @param {ITenant} tenant
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async migrateTenant(tenant: ITenant): Promise<void> {
|
||||||
|
// Throw error if the tenant already initialized.
|
||||||
|
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||||
|
|
||||||
|
// Migrate the database tenant.
|
||||||
|
await this.tenantDbManager.migrate(tenant);
|
||||||
|
|
||||||
|
// Mark the tenant as initialized.
|
||||||
|
await Tenant.markAsInitialized(tenant.id);
|
||||||
|
|
||||||
|
// Triggers `onTenantMigrated` event.
|
||||||
|
this.eventDispatcher.dispatch(events.tenantManager.tenantMigrated, {
|
||||||
|
tenantId: tenant.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds the tenant database.
|
||||||
|
* @param {ITenant} tenant
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async seedTenant(tenant: ITenant, tenancyContext): Promise<void> {
|
||||||
|
// Throw error if the tenant is not built yet.
|
||||||
|
this.throwErrorIfTenantNotBuilt(tenant);
|
||||||
|
|
||||||
|
// Throw error if the tenant is not seeded yet.
|
||||||
|
this.throwErrorIfTenantAlreadySeeded(tenant);
|
||||||
|
|
||||||
|
// Seeds the organization database data.
|
||||||
|
await new SeedMigration(tenancyContext.knex, tenancyContext).latest();
|
||||||
|
|
||||||
|
// Mark the tenant as seeded in specific date.
|
||||||
|
await Tenant.markAsSeeded(tenant.id);
|
||||||
|
|
||||||
|
// Triggers `onTenantSeeded` event.
|
||||||
|
this.eventDispatcher.dispatch(events.tenantManager.tenantSeeded, {
|
||||||
|
tenantId: tenant.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize knex instance or retrieve the instance of cache map.
|
||||||
|
* @param {ITenant} tenant
|
||||||
|
* @returns {Knex}
|
||||||
|
*/
|
||||||
|
public setupKnexInstance(tenant: ITenant) {
|
||||||
|
return this.tenantDBManager.setupKnexInstance(tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve tenant knex instance or throw error in case was not initialized.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns {Knex}
|
||||||
|
*/
|
||||||
|
public getKnexInstance(tenantId: number) {
|
||||||
|
return this.tenantDBManager.getKnexInstance(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws error if the tenant already seeded.
|
||||||
|
* @throws {TenantAlreadySeeded}
|
||||||
|
*/
|
||||||
|
private throwErrorIfTenantAlreadySeeded(tenant: ITenant) {
|
||||||
|
if (tenant.seededAt) {
|
||||||
|
throw new TenantAlreadySeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws error if the tenant database is not built yut.
|
||||||
|
* @param {ITenant} tenant
|
||||||
|
*/
|
||||||
|
private throwErrorIfTenantNotBuilt(tenant: ITenant) {
|
||||||
|
if (!tenant.initializedAt) {
|
||||||
|
throw new TenantDatabaseNotBuilt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws error if the tenant already migrated.
|
||||||
|
* @throws {TenantAlreadyInitialized}
|
||||||
|
*/
|
||||||
|
private throwErrorIfTenantAlreadyInitialized(tenant: ITenant) {
|
||||||
|
if (tenant.initializedAt) {
|
||||||
|
throw new TenantAlreadyInitialized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize seed migration contxt.
|
||||||
|
* @param {ITenant} tenant
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public getSeedMigrationContext(tenant: ITenant) {
|
||||||
|
// Initialize the knex instance.
|
||||||
|
const knex = this.setupKnexInstance(tenant);
|
||||||
|
const i18nInstance = i18n();
|
||||||
|
|
||||||
|
i18nInstance.setLocale(tenant.metadata.language);
|
||||||
|
|
||||||
|
return {
|
||||||
|
knex,
|
||||||
|
i18n: i18nInstance,
|
||||||
|
tenant,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export class TenantDBAlreadyExists {
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/server-nest/src/utils/sanitize-database-name.ts
Normal file
4
packages/server-nest/src/utils/sanitize-database-name.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export function sanitizeDatabaseName(dbName: string) {
|
||||||
|
// Replace any character that is not alphanumeric or an underscore with an underscore
|
||||||
|
return dbName.replace(/[^a-zA-Z0-9_]/g, '');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user