mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -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,91 @@
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
|
||||
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 { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
|
||||
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
|
||||
|
||||
@ApiTags('Organization')
|
||||
@Controller('organization')
|
||||
@IgnoreTenantInitializedRoute()
|
||||
@IgnoreTenantSeededRoute()
|
||||
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('current')
|
||||
@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({
|
||||
code: 200,
|
||||
message: 'Organization information has been updated successfully.',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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';
|
||||
import { CommandOrganizationValidators } from './commands/CommandOrganizationValidators.service';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
|
||||
import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBaseCurrencyLocking.service';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
TenancyContext,
|
||||
GetCurrentOrganizationService,
|
||||
BuildOrganizationService,
|
||||
UpdateOrganizationService,
|
||||
OrganizationBuildProcessor,
|
||||
CommandOrganizationValidators,
|
||||
OrganizationBaseCurrencyLocking,
|
||||
],
|
||||
imports: [
|
||||
BullModule.registerQueue({ name: OrganizationBuildQueue }),
|
||||
TenantDBManagerModule,
|
||||
],
|
||||
controllers: [OrganizationController],
|
||||
})
|
||||
export class OrganizationModule {}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { TenantJobPayload } from '@/interfaces/Tenant';
|
||||
import { SystemUser } from '../System/models/SystemUser';
|
||||
import { BuildOrganizationDto } from './dtos/Organization.dto';
|
||||
|
||||
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 extends TenantJobPayload {
|
||||
buildDto: BuildOrganizationDto;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defaultTo } from 'lodash';
|
||||
import { IOrganizationBuildDTO } from './Organization.types';
|
||||
import { BuildOrganizationDto } from './dtos/Organization.dto';
|
||||
|
||||
/**
|
||||
* Transformes build DTO object.
|
||||
* @param {IOrganizationBuildDTO} buildDTO
|
||||
* @returns {IOrganizationBuildDTO}
|
||||
*/
|
||||
export const transformBuildDto = (
|
||||
buildDTO: BuildOrganizationDto,
|
||||
): BuildOrganizationDto => {
|
||||
return {
|
||||
...buildDTO,
|
||||
dateFormat: defaultTo(buildDTO.dateFormat, 'DD MMM yyyy'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
// @ts-nocheck
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
interface MutateBaseCurrencyLockMeta {
|
||||
modelName: string;
|
||||
pluralName?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationBaseCurrencyLocking {
|
||||
/**
|
||||
* Retrieves the tenant models that have prevented mutation base currency.
|
||||
*/
|
||||
private getModelsPreventsMutate = (tenantId: number) => {
|
||||
const Models = this.tenancy.models(tenantId);
|
||||
|
||||
const filteredEntries = Object.entries(Models).filter(
|
||||
([key, Model]) => !!Model.preventMutateBaseCurrency,
|
||||
);
|
||||
return Object.fromEntries(filteredEntries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the mutation base currency model is locked.
|
||||
* @param {Model} Model
|
||||
* @returns {Promise<MutateBaseCurrencyLockMeta | false>}
|
||||
*/
|
||||
private isModelMutateLocked = async (
|
||||
Model,
|
||||
): Promise<MutateBaseCurrencyLockMeta | false> => {
|
||||
const validateQuery = Model.query();
|
||||
|
||||
if (typeof Model?.modifiers?.preventMutateBaseCurrency !== 'undefined') {
|
||||
validateQuery.modify('preventMutateBaseCurrency');
|
||||
} else {
|
||||
validateQuery.select(['id']).first();
|
||||
}
|
||||
const validateResult = await validateQuery;
|
||||
const isValid = !isEmpty(validateResult);
|
||||
|
||||
return isValid
|
||||
? {
|
||||
modelName: Model.name,
|
||||
pluralName: Model.pluralName,
|
||||
}
|
||||
: false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the base currency mutation locks of the tenant models.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<MutateBaseCurrencyLockMeta[]>}
|
||||
*/
|
||||
public async baseCurrencyMutateLocks(
|
||||
tenantId: number,
|
||||
): Promise<MutateBaseCurrencyLockMeta[]> {
|
||||
const PreventedModels = this.getModelsPreventsMutate(tenantId);
|
||||
|
||||
const opers = Object.entries(PreventedModels).map(([ModelName, Model]) =>
|
||||
this.isModelMutateLocked(Model),
|
||||
);
|
||||
const results = await Promise.all(opers);
|
||||
|
||||
return results.filter(
|
||||
(result) => result !== false,
|
||||
) as MutateBaseCurrencyLockMeta[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the base currency mutation locked.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public isBaseCurrencyMutateLocked = async (tenantId: number) => {
|
||||
const locks = await this.baseCurrencyMutateLocks(tenantId);
|
||||
|
||||
return !isEmpty(locks);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { SeedMigration } from '@/lib/Seeder/SeedMigration';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import TenantDBManager from '@/services/Tenancy/TenantDBManager';
|
||||
import config from '../../config';
|
||||
import { ERRORS } from './constants';
|
||||
import OrganizationService from './OrganizationService';
|
||||
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
|
||||
|
||||
@Service()
|
||||
export default class OrganizationUpgrade {
|
||||
@Inject()
|
||||
private organizationService: OrganizationService;
|
||||
|
||||
@Inject()
|
||||
private tenantsManager: TenantsManagerService;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Upgrades the given organization database.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public upgradeJob = async (tenantId: number): Promise<void> => {
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
// Validate tenant version.
|
||||
this.validateTenantVersion(tenant);
|
||||
|
||||
// Initialize the tenant.
|
||||
const seedContext = this.tenantsManager.getSeedMigrationContext(tenant);
|
||||
|
||||
// Database manager.
|
||||
const dbManager = new TenantDBManager();
|
||||
|
||||
// Migrate the organization database schema.
|
||||
await dbManager.migrate(tenant);
|
||||
|
||||
// Seeds the organization database data.
|
||||
await new SeedMigration(seedContext.knex, seedContext).latest();
|
||||
|
||||
// Update the organization database version.
|
||||
await this.organizationService.flagTenantDBBatch(tenantId);
|
||||
|
||||
// Remove the tenant job id.
|
||||
await Tenant.markAsUpgraded(tenantId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Running organization upgrade job.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public upgrade = async (tenantId: number): Promise<{ jobId: string }> => {
|
||||
const tenant = await Tenant.query().findById(tenantId);
|
||||
|
||||
// Validate tenant version.
|
||||
this.validateTenantVersion(tenant);
|
||||
|
||||
// Validate tenant upgrade is not running.
|
||||
this.validateTenantUpgradeNotRunning(tenant);
|
||||
|
||||
// Send welcome mail to the user.
|
||||
const jobMeta = await this.agenda.now('organization-upgrade', {
|
||||
tenantId,
|
||||
});
|
||||
// Transformes the mangodb id to string.
|
||||
const jobId = new ObjectId(jobMeta.attrs._id).toString();
|
||||
|
||||
// Markes the tenant as currently building.
|
||||
await Tenant.markAsUpgrading(tenantId, jobId);
|
||||
|
||||
return { jobId };
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the given tenant version.
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
private validateTenantVersion(tenant) {
|
||||
if (tenant.databaseBatch >= config.databaseBatch) {
|
||||
throw new ServiceError(ERRORS.TENANT_DATABASE_UPGRADED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given tenant upgrade is not running.
|
||||
* @param tenant
|
||||
*/
|
||||
private validateTenantUpgradeNotRunning(tenant) {
|
||||
if (tenant.isUpgradeRunning) {
|
||||
throw new ServiceError(ERRORS.TENANT_UPGRADE_IS_RUNNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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,122 @@
|
||||
import { Queue } from 'bullmq';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import {
|
||||
IOrganizationBuildEventPayload,
|
||||
IOrganizationBuiltEventPayload,
|
||||
OrganizationBuildQueue,
|
||||
OrganizationBuildQueueJob,
|
||||
OrganizationBuildQueueJobPayload,
|
||||
} 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';
|
||||
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
|
||||
|
||||
@Injectable()
|
||||
export class BuildOrganizationService {
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly tenantsManager: TenantsManagerService,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly tenantRepository: TenantRepository,
|
||||
|
||||
@InjectQueue(OrganizationBuildQueue)
|
||||
private readonly organizationBuildQueue: Queue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Builds the database schema and seed data of the given organization id.
|
||||
* @param {string} 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);
|
||||
|
||||
await this.tenantsManager.dropDatabaseIfExists();
|
||||
await this.tenantsManager.createDatabase();
|
||||
|
||||
await this.tenantsManager.migrateTenant();
|
||||
await this.tenantsManager.seedTenant()
|
||||
|
||||
// Throws `onOrganizationBuild` event.
|
||||
await this.eventPublisher.emitAsync(events.organization.build, {
|
||||
tenantId: tenant.id,
|
||||
buildDTO,
|
||||
systemUser,
|
||||
} as IOrganizationBuildEventPayload);
|
||||
|
||||
// Marks the tenant as completed builing.
|
||||
await this.tenantRepository.markAsBuilt().findById(tenant.id);
|
||||
await this.tenantRepository.markAsBuildCompleted().findById(tenant.id);
|
||||
|
||||
// Flags the tenant database batch.
|
||||
await this.tenantRepository.flagTenantDBBatch().findById(tenant.id);
|
||||
|
||||
// Triggers the organization built event.
|
||||
await this.eventPublisher.emitAsync(events.organization.built, {
|
||||
tenantId: tenant.id,
|
||||
} as IOrganizationBuiltEventPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the tenant database build process.
|
||||
* @param {BuildOrganizationDto} buildDTO - Organization build dto.
|
||||
* @returns {Promise<{ nextRunAt: Date; jobId: string }>} - Returns the next run date and job id.
|
||||
*/
|
||||
async buildRunJob(
|
||||
buildDTO: BuildOrganizationDto,
|
||||
): Promise<{ nextRunAt: Date; jobId: string }> {
|
||||
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);
|
||||
|
||||
// Transforms build DTO object.
|
||||
const transformedBuildDTO = transformBuildDto(buildDTO);
|
||||
|
||||
// Saves the tenant metadata.
|
||||
await this.tenantRepository.saveMetadata(tenant.id, transformedBuildDTO);
|
||||
|
||||
// Run the organization database build job.
|
||||
const jobMeta = await this.organizationBuildQueue.add(
|
||||
OrganizationBuildQueueJob,
|
||||
{
|
||||
organizationId: tenant.organizationId,
|
||||
userId: systemUser.id,
|
||||
buildDto: transformedBuildDTO,
|
||||
} as OrganizationBuildQueueJobPayload,
|
||||
);
|
||||
// Marks the tenant as currently building.
|
||||
await this.tenantRepository.markAsBuilding(jobMeta.id).findById(tenant.id);
|
||||
|
||||
return {
|
||||
nextRunAt: jobMeta.data.nextRunAt,
|
||||
jobId: jobMeta.data.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks tenant build run job.
|
||||
*/
|
||||
public async revertBuildRunJob() {
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
|
||||
await this.tenantRepository.markAsBuildCompleted().findById(tenant.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { OrganizationBaseCurrencyLocking } from '../Organization/OrganizationBaseCurrencyLocking.service';
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ERRORS } from '../Organization.constants';
|
||||
|
||||
@Injectable()
|
||||
export class CommandOrganizationValidators {
|
||||
constructor(
|
||||
private readonly baseCurrencyMutateLocking: OrganizationBaseCurrencyLocking,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate mutate base currency ability.
|
||||
* @param {Tenant} tenant -
|
||||
* @param {string} newBaseCurrency -
|
||||
* @param {string} oldBaseCurrency -
|
||||
*/
|
||||
async validateMutateBaseCurrency(
|
||||
tenant: TenantModel,
|
||||
newBaseCurrency: string,
|
||||
oldBaseCurrency: string,
|
||||
) {
|
||||
if (tenant.isReady && newBaseCurrency !== oldBaseCurrency) {
|
||||
const isLocked =
|
||||
await this.baseCurrencyMutateLocking.isBaseCurrencyMutateLocked(
|
||||
tenant.id,
|
||||
);
|
||||
|
||||
if (isLocked) {
|
||||
throw new ServiceError(ERRORS.BASE_CURRENCY_MUTATE_LOCKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { UpdateOrganizationDto } from '../dtos/Organization.dto';
|
||||
import { throwIfTenantNotExists } from '../Organization/_utils';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { CommandOrganizationValidators } from './CommandOrganizationValidators.service';
|
||||
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateOrganizationService {
|
||||
constructor(
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly commandOrganizationValidators: CommandOrganizationValidators,
|
||||
|
||||
private readonly tenantRepository: TenantRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Updates organization information.
|
||||
* @param {UpdateOrganizationDto} 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.commandOrganizationValidators.validateMutateBaseCurrency(
|
||||
tenant,
|
||||
organizationDTO.baseCurrency,
|
||||
tenant.metadata?.baseCurrency,
|
||||
);
|
||||
}
|
||||
await this.tenantRepository.saveMetadata(tenant.id, organizationDTO);
|
||||
|
||||
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {
|
||||
// Triggers `onOrganizationBaseCurrencyUpdated` event.
|
||||
await this.eventEmitter.emitAsync(
|
||||
events.organization.baseCurrencyUpdated,
|
||||
{
|
||||
organizationDTO,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import * as momentTz from 'moment-timezone';
|
||||
import {
|
||||
IsHexColor,
|
||||
IsIn,
|
||||
IsISO31661Alpha2,
|
||||
IsISO4217CurrencyCode,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
import { MONTHS } from '../Organization/constants';
|
||||
import { ACCEPTED_LOCALES, DATE_FORMATS } from '../Organization.constants';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class BuildOrganizationDto {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Organization name',
|
||||
example: 'Acme Inc.',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Industry of the organization',
|
||||
example: 'Technology',
|
||||
})
|
||||
industry?: string;
|
||||
|
||||
@IsISO31661Alpha2()
|
||||
@ApiProperty({
|
||||
description: 'Country location in ISO 3166-1 alpha-2 format',
|
||||
example: 'US',
|
||||
})
|
||||
location: string;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@ApiProperty({
|
||||
description: 'Base currency in ISO 4217 format',
|
||||
example: 'USD',
|
||||
})
|
||||
baseCurrency: string;
|
||||
|
||||
@IsIn(momentTz.tz.names())
|
||||
@ApiProperty({
|
||||
description: 'Timezone of the organization',
|
||||
example: 'America/New_York',
|
||||
})
|
||||
timezone: string;
|
||||
|
||||
@IsIn(MONTHS)
|
||||
@ApiProperty({
|
||||
description: 'Starting month of fiscal year',
|
||||
example: 'January',
|
||||
})
|
||||
fiscalYear: string;
|
||||
|
||||
@IsIn(ACCEPTED_LOCALES)
|
||||
@ApiProperty({
|
||||
description: 'Language/locale of the organization',
|
||||
example: 'en-US',
|
||||
})
|
||||
language: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(DATE_FORMATS)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Date format used by the organization',
|
||||
example: 'MM/DD/YYYY',
|
||||
})
|
||||
dateFormat?: string;
|
||||
}
|
||||
|
||||
export class UpdateOrganizationDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Organization name',
|
||||
example: 'Acme Inc.',
|
||||
})
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Industry of the organization',
|
||||
example: 'Technology',
|
||||
})
|
||||
industry?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO31661Alpha2()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Country location in ISO 3166-1 alpha-2 format',
|
||||
example: 'US',
|
||||
})
|
||||
location?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO4217CurrencyCode()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Base currency in ISO 4217 format',
|
||||
example: 'USD',
|
||||
})
|
||||
baseCurrency?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(momentTz.tz.names())
|
||||
@ApiPropertyOptional({
|
||||
description: 'Timezone of the organization',
|
||||
example: 'America/New_York',
|
||||
})
|
||||
timezone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(MONTHS)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Starting month of fiscal year',
|
||||
example: 'January',
|
||||
})
|
||||
fiscalYear?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(ACCEPTED_LOCALES)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Language/locale of the organization',
|
||||
example: 'en-US',
|
||||
})
|
||||
language?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(DATE_FORMATS)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Date format used by the organization',
|
||||
example: 'MM/DD/YYYY',
|
||||
})
|
||||
dateFormat?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Organization address details',
|
||||
example: {
|
||||
address_1: '123 Main St',
|
||||
address_2: 'Suite 100',
|
||||
postal_code: '10001',
|
||||
city: 'New York',
|
||||
stateProvince: 'NY',
|
||||
phone: '+1-555-123-4567',
|
||||
},
|
||||
})
|
||||
address?: {
|
||||
address_1?: string;
|
||||
address_2?: string;
|
||||
postal_code?: string;
|
||||
city?: string;
|
||||
stateProvince?: string;
|
||||
phone?: string;
|
||||
};
|
||||
|
||||
@IsOptional()
|
||||
@IsHexColor()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Primary brand color in hex format',
|
||||
example: '#4285F4',
|
||||
})
|
||||
primaryColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Logo file key reference',
|
||||
example: 'organizations/acme-logo-123456.png',
|
||||
})
|
||||
logoKey?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Organization tax identification number',
|
||||
example: '12-3456789',
|
||||
})
|
||||
taxNumber?: string;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
import { Process } from '@nestjs/bull';
|
||||
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,
|
||||
private readonly clsService: ClsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(OrganizationBuildQueueJob)
|
||||
@UseCls()
|
||||
async process(job: Job<OrganizationBuildQueueJobPayload>) {
|
||||
console.log('Processing organization build job:', job.id);
|
||||
|
||||
this.clsService.set('organizationId', job.data.organizationId);
|
||||
this.clsService.set('userId', job.data.userId);
|
||||
|
||||
try {
|
||||
await this.organizationBuildService.build(job.data.buildDto);
|
||||
} catch (e) {
|
||||
// Unlock build status of the tenant.
|
||||
await this.organizationBuildService.revertBuildRunJob();
|
||||
console.error('Error processing organization build job:', e);
|
||||
throw e; // Re-throw to mark job as failed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user