mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
feat: organization metadata redesigned.
This commit is contained in:
60
server/src/api/controllers/Jobs.ts
Normal file
60
server/src/api/controllers/Jobs.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import BaseController from 'api/controllers/BaseController';
|
||||||
|
import { ServiceError } from 'exceptions';
|
||||||
|
import JobsService from 'services/Jobs/JobsService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class ItemsController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
jobsService: JobsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
public router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/:id', this.getJob, this.handlerServiceErrors);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve job details.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private getJob = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const job = await this.jobsService.getJob(id);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
job: this.transfromToResponse(job),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles service errors.
|
||||||
|
* @param {Error} error
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private handlerServiceErrors = (
|
||||||
|
error: Error,
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
if (error instanceof ServiceError) {
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { check, ValidationChain } from 'express-validator';
|
||||||
|
|
||||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||||
import JWTAuth from 'api/middleware/jwtAuth';
|
import JWTAuth from 'api/middleware/jwtAuth';
|
||||||
import TenancyMiddleware from 'api/middleware/TenancyMiddleware';
|
import TenancyMiddleware from 'api/middleware/TenancyMiddleware';
|
||||||
@@ -8,9 +10,9 @@ import AttachCurrentTenantUser from 'api/middleware/AttachCurrentTenantUser';
|
|||||||
import OrganizationService from 'services/Organization';
|
import OrganizationService from 'services/Organization';
|
||||||
import { ServiceError } from 'exceptions';
|
import { ServiceError } from 'exceptions';
|
||||||
import BaseController from 'api/controllers/BaseController';
|
import BaseController from 'api/controllers/BaseController';
|
||||||
import EnsureConfiguredMiddleware from 'api/middleware/EnsureConfiguredMiddleware';
|
|
||||||
import SettingsMiddleware from 'api/middleware/SettingsMiddleware';
|
|
||||||
|
|
||||||
|
const DATE_FORMATS = ['MM/DD/YYYY', 'M/D/YYYY'];
|
||||||
|
const BASE_CURRENCY = ['USD', 'LYD'];
|
||||||
@Service()
|
@Service()
|
||||||
export default class OrganizationController extends BaseController {
|
export default class OrganizationController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -28,111 +30,63 @@ export default class OrganizationController extends BaseController {
|
|||||||
router.use(AttachCurrentTenantUser);
|
router.use(AttachCurrentTenantUser);
|
||||||
router.use(TenancyMiddleware);
|
router.use(TenancyMiddleware);
|
||||||
|
|
||||||
// Should to seed organization tenant be configured.
|
|
||||||
router.use('/seed', SubscriptionMiddleware('main'));
|
|
||||||
router.use('/seed', SettingsMiddleware);
|
|
||||||
router.use('/seed', EnsureConfiguredMiddleware);
|
|
||||||
|
|
||||||
router.use('/build', SubscriptionMiddleware('main'));
|
router.use('/build', SubscriptionMiddleware('main'));
|
||||||
|
router.post(
|
||||||
router.post('/build', asyncMiddleware(this.build.bind(this)));
|
'/build',
|
||||||
router.post('/seed', asyncMiddleware(this.seed.bind(this)));
|
this.buildValidationSchema,
|
||||||
router.get('/all', asyncMiddleware(this.allOrganizations.bind(this)));
|
this.validationResult,
|
||||||
|
asyncMiddleware(this.build.bind(this)),
|
||||||
|
this.handleServiceErrors.bind(this)
|
||||||
|
);
|
||||||
|
router.put(
|
||||||
|
'/',
|
||||||
|
this.asyncMiddleware(this.updateOrganization.bind(this)),
|
||||||
|
this.handleServiceErrors.bind(this)
|
||||||
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/current',
|
'/',
|
||||||
asyncMiddleware(this.currentOrganization.bind(this))
|
asyncMiddleware(this.currentOrganization.bind(this)),
|
||||||
|
this.handleServiceErrors.bind(this)
|
||||||
);
|
);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization setup schema.
|
||||||
|
*/
|
||||||
|
private get buildValidationSchema(): ValidationChain[] {
|
||||||
|
return [
|
||||||
|
check('organization_name').exists().trim(),
|
||||||
|
check('base_currency').exists().isIn(BASE_CURRENCY),
|
||||||
|
check('timezone').exists(),
|
||||||
|
check('fiscal_year').exists().isISO8601(),
|
||||||
|
check('industry').optional().isString(),
|
||||||
|
check('date_format').optional().isIn(DATE_FORMATS),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds tenant database and migrate database schema.
|
* Builds tenant database and migrate database schema.
|
||||||
* @param {Request} req - Express request.
|
* @param {Request} req - Express request.
|
||||||
* @param {Response} res - Express response.
|
* @param {Response} res - Express response.
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
async build(req: Request, res: Response, next: Function) {
|
private async build(req: Request, res: Response, next: Function) {
|
||||||
const { organizationId } = req.tenant;
|
const { tenantId } = req;
|
||||||
const { user } = req;
|
const buildDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.organizationService.build(organizationId, user);
|
const result = await this.organizationService.buildRunJob(
|
||||||
|
tenantId,
|
||||||
|
buildDTO
|
||||||
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
code: 'ORGANIZATION.DATABASE.INITIALIZED',
|
code: 'ORGANIZATION.DATABASE.INITIALIZED',
|
||||||
message: 'The organization database has been initialized.',
|
message: 'The organization database has been initialized.',
|
||||||
|
data: result,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ServiceError) {
|
|
||||||
if (error.errorType === 'tenant_not_found') {
|
|
||||||
return res.status(400).send({
|
|
||||||
// errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (error.errorType === 'tenant_already_initialized') {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{ type: 'TENANT.DATABASE.ALREADY.BUILT', code: 200 }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seeds initial data to tenant database.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
|
||||||
async seed(req: Request, res: Response, next: Function) {
|
|
||||||
const { organizationId } = req.tenant;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.organizationService.seed(organizationId);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
type: 'success',
|
|
||||||
code: 'ORGANIZATION.DATABASE.SEED',
|
|
||||||
message: 'The organization database has been seeded.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ServiceError) {
|
|
||||||
if (error.errorType === 'tenant_not_found') {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (error.errorType === 'tenant_already_seeded') {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{ type: 'TENANT.DATABASE.ALREADY.SEEDED', code: 200 }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (error.errorType === 'tenant_db_not_built') {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{ type: 'TENANT.DATABASE.NOT.BUILT', code: 300 }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listing all organizations that assocaited to the authorized user.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
|
||||||
async allOrganizations(req: Request, res: Response, next: NextFunction) {
|
|
||||||
const { user } = req;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const organizations = await this.organizationService.listOrganizations(
|
|
||||||
user
|
|
||||||
);
|
|
||||||
return res.status(200).send({ organizations });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -144,7 +98,11 @@ export default class OrganizationController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
async currentOrganization(req: Request, res: Response, next: NextFunction) {
|
private async currentOrganization(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -156,4 +114,68 @@ export default class OrganizationController extends BaseController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the organization information.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private async updateOrganization(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const tenantDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const organization = await this.organizationService.updateOrganization(
|
||||||
|
tenantId,
|
||||||
|
tenantDTO
|
||||||
|
);
|
||||||
|
return res.status(200).send(
|
||||||
|
this.transfromToResponse({
|
||||||
|
tenantId,
|
||||||
|
message: 'Organization information has been updated successfully.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles service errors.
|
||||||
|
* @param {Error} error
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private handleServiceErrors(
|
||||||
|
error: Error,
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
if (error instanceof ServiceError) {
|
||||||
|
if (error.errorType === 'tenant_not_found') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'TENANT_ALREADY_BUILT') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'TENANT_ALREADY_BUILT', code: 200 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'TENANT_IS_BUILDING') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'TENANT_IS_BUILDING', code: 300 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import EnsureTenantIsInitialized from 'api/middleware/EnsureTenantIsInitialized'
|
|||||||
import SettingsMiddleware from 'api/middleware/SettingsMiddleware';
|
import SettingsMiddleware from 'api/middleware/SettingsMiddleware';
|
||||||
import I18nMiddleware from 'api/middleware/I18nMiddleware';
|
import I18nMiddleware from 'api/middleware/I18nMiddleware';
|
||||||
import I18nAuthenticatedMiddlware from 'api/middleware/I18nAuthenticatedMiddlware';
|
import I18nAuthenticatedMiddlware from 'api/middleware/I18nAuthenticatedMiddlware';
|
||||||
import EnsureConfiguredMiddleware from 'api/middleware/EnsureConfiguredMiddleware';
|
|
||||||
import EnsureTenantIsSeeded from 'api/middleware/EnsureTenantIsSeeded';
|
import EnsureTenantIsSeeded from 'api/middleware/EnsureTenantIsSeeded';
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
@@ -41,8 +40,8 @@ import Ping from 'api/controllers/Ping';
|
|||||||
import Subscription from 'api/controllers/Subscription';
|
import Subscription from 'api/controllers/Subscription';
|
||||||
import Licenses from 'api/controllers/Subscription/Licenses';
|
import Licenses from 'api/controllers/Subscription/Licenses';
|
||||||
import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments';
|
import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments';
|
||||||
import Setup from 'api/controllers/Setup';
|
|
||||||
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
|
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
|
||||||
|
import Jobs from './controllers/Jobs';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
@@ -59,7 +58,7 @@ export default () => {
|
|||||||
app.use('/subscription', Container.get(Subscription).router());
|
app.use('/subscription', Container.get(Subscription).router());
|
||||||
app.use('/organization', Container.get(Organization).router());
|
app.use('/organization', Container.get(Organization).router());
|
||||||
app.use('/ping', Container.get(Ping).router());
|
app.use('/ping', Container.get(Ping).router());
|
||||||
app.use('/setup', Container.get(Setup).router());
|
app.use('/jobs', Container.get(Jobs).router());
|
||||||
|
|
||||||
// - Dashboard routes.
|
// - Dashboard routes.
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -72,7 +71,6 @@ export default () => {
|
|||||||
dashboard.use(EnsureTenantIsInitialized);
|
dashboard.use(EnsureTenantIsInitialized);
|
||||||
dashboard.use(SettingsMiddleware);
|
dashboard.use(SettingsMiddleware);
|
||||||
dashboard.use(I18nAuthenticatedMiddlware);
|
dashboard.use(I18nAuthenticatedMiddlware);
|
||||||
dashboard.use(EnsureConfiguredMiddleware);
|
|
||||||
dashboard.use(EnsureTenantIsSeeded);
|
dashboard.use(EnsureTenantIsSeeded);
|
||||||
|
|
||||||
dashboard.use('/users', Container.get(Users).router());
|
dashboard.use('/users', Container.get(Users).router());
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
|
|
||||||
export default (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const { settings } = req;
|
|
||||||
|
|
||||||
if (!settings.get('app_configured', false)) {
|
|
||||||
return res.boom.badRequest(null, {
|
|
||||||
errors: [{ type: 'APP.NOT.CONFIGURED', code: 100 }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
14
server/src/interfaces/Jobs.ts
Normal file
14
server/src/interfaces/Jobs.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export interface IJobMeta {
|
||||||
|
id: string;
|
||||||
|
nextRunAt: Date;
|
||||||
|
lastModifiedBy: null | Date;
|
||||||
|
lockedAt: null | Date;
|
||||||
|
lastRunAt: null | Date;
|
||||||
|
failCount: number;
|
||||||
|
failedAt: null | Date;
|
||||||
|
lastFinishedAt: Date | null;
|
||||||
|
running: boolean;
|
||||||
|
queued: boolean;
|
||||||
|
completed: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
}
|
||||||
@@ -8,3 +8,19 @@ export interface IOrganizationSetupDTO{
|
|||||||
industry: string,
|
industry: string,
|
||||||
timeZone: string,
|
timeZone: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IOrganizationBuildDTO {
|
||||||
|
organizationName: string;
|
||||||
|
baseCurrency: string,
|
||||||
|
timezone: string;
|
||||||
|
fiscalYear: string;
|
||||||
|
industry: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrganizationUpdateDTO {
|
||||||
|
organizationName: string;
|
||||||
|
baseCurrency: string,
|
||||||
|
timezone: string;
|
||||||
|
fiscalYear: string;
|
||||||
|
industry: string;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ export interface ITenant {
|
|||||||
|
|
||||||
initializedAt: Date|null,
|
initializedAt: Date|null,
|
||||||
seededAt: Date|null,
|
seededAt: Date|null,
|
||||||
|
builtAt: Date|null,
|
||||||
createdAt: Date|null,
|
createdAt: Date|null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export * from './InventoryDetails';
|
|||||||
export * from './LandedCost';
|
export * from './LandedCost';
|
||||||
export * from './Entry';
|
export * from './Entry';
|
||||||
export * from './TransactionsByReference';
|
export * from './TransactionsByReference';
|
||||||
|
export * from './Jobs';
|
||||||
|
|
||||||
export interface I18nService {
|
export interface I18nService {
|
||||||
__: (input: string) => string;
|
__: (input: string) => string;
|
||||||
|
|||||||
32
server/src/jobs/OrganizationSetup.ts
Normal file
32
server/src/jobs/OrganizationSetup.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import OrganizationService from 'services/Organization';
|
||||||
|
|
||||||
|
export default class OrganizationSetupJob {
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
constructor(agenda) {
|
||||||
|
agenda.define(
|
||||||
|
'organization-setup',
|
||||||
|
{ priority: 'high', concurrency: 1 },
|
||||||
|
this.handler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle job action.
|
||||||
|
*/
|
||||||
|
async handler(job, done: Function): Promise<void> {
|
||||||
|
const { tenantId, _id } = job.attrs.data;
|
||||||
|
const licenseService = Container.get(OrganizationService);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await licenseService.build(tenantId);
|
||||||
|
done();
|
||||||
|
} catch (e) {
|
||||||
|
// Unlock build status of the tenant.
|
||||||
|
await licenseService.revertBuildRunJob(tenantId, _id);
|
||||||
|
done(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { isEmpty, isObject, isUndefined } from 'lodash';
|
|||||||
|
|
||||||
export class Transformer {
|
export class Transformer {
|
||||||
/**
|
/**
|
||||||
*
|
* Includeded attributes.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
protected includeAttributes = (): string[] => {
|
protected includeAttributes = (): string[] => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import SendSMSNotificationTrialEnd from 'jobs/SMSNotificationTrialEnd';
|
|||||||
import SendMailNotificationSubscribeEnd from 'jobs/MailNotificationSubscribeEnd';
|
import SendMailNotificationSubscribeEnd from 'jobs/MailNotificationSubscribeEnd';
|
||||||
import SendMailNotificationTrialEnd from 'jobs/MailNotificationTrialEnd';
|
import SendMailNotificationTrialEnd from 'jobs/MailNotificationTrialEnd';
|
||||||
import UserInviteMailJob from 'jobs/UserInviteMail';
|
import UserInviteMailJob from 'jobs/UserInviteMail';
|
||||||
|
import OrganizationSetupJob from 'jobs/OrganizationSetup';
|
||||||
|
|
||||||
export default ({ agenda }: { agenda: Agenda }) => {
|
export default ({ agenda }: { agenda: Agenda }) => {
|
||||||
new WelcomeEmailJob(agenda);
|
new WelcomeEmailJob(agenda);
|
||||||
@@ -21,6 +22,7 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
|||||||
new SendLicenseViaPhoneJob(agenda);
|
new SendLicenseViaPhoneJob(agenda);
|
||||||
new ComputeItemCost(agenda);
|
new ComputeItemCost(agenda);
|
||||||
new RewriteInvoicesJournalEntries(agenda);
|
new RewriteInvoicesJournalEntries(agenda);
|
||||||
|
new OrganizationSetupJob(agenda);
|
||||||
|
|
||||||
agenda.define(
|
agenda.define(
|
||||||
'send-sms-notification-subscribe-end',
|
'send-sms-notification-subscribe-end',
|
||||||
|
|||||||
47
server/src/services/Jobs/JobTransformer.ts
Normal file
47
server/src/services/Jobs/JobTransformer.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { ISaleInvoice } from 'interfaces';
|
||||||
|
import { Transformer } from 'lib/Transformer/Transformer';
|
||||||
|
import { formatNumber } from 'utils';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class JobTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale invoice object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
protected includeAttributes = (): string[] => {
|
||||||
|
return ['queued', 'completed', 'failed'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the queued state.
|
||||||
|
* @param {IJob} job
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
protected queued = (job): boolean => {
|
||||||
|
return !!job.nextRunAt && moment().isSameOrAfter(job.nextRunAt, 'seconds');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the completed state.
|
||||||
|
* @param job
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected completed = (job): boolean => {
|
||||||
|
return !!job.lastFinishedAt;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the failed state.
|
||||||
|
* @param job
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected failed = (job): boolean => {
|
||||||
|
return (
|
||||||
|
job.lastFinishedAt &&
|
||||||
|
job.failedAt &&
|
||||||
|
moment(job.failedAt).isSame(job.lastFinishedAt)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
50
server/src/services/Jobs/JobsService.ts
Normal file
50
server/src/services/Jobs/JobsService.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { pick, first } from 'lodash';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import JobTransformer from './JobTransformer';
|
||||||
|
import { IJobMeta } from 'interfaces';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class JobsService {
|
||||||
|
@Inject('agenda')
|
||||||
|
agenda: any;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
jobsTransformer: JobTransformer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve job details of the given job id.
|
||||||
|
* @param {string} jobId
|
||||||
|
* @returns {Promise<IJobMeta>}
|
||||||
|
*/
|
||||||
|
async getJob(jobId: string): Promise<IJobMeta> {
|
||||||
|
const jobs = await this.agenda.jobs({ _id: new ObjectId(jobId) });
|
||||||
|
|
||||||
|
// Transformes job to json.
|
||||||
|
const jobJson = this.transformJobToJson(first(jobs));
|
||||||
|
|
||||||
|
return this.jobsTransformer.transform(jobJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the job to json.
|
||||||
|
* @param job
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private transformJobToJson(job) {
|
||||||
|
return {
|
||||||
|
id: job.attrs._id,
|
||||||
|
...pick(job.attrs, [
|
||||||
|
'nextRunAt',
|
||||||
|
'lastModifiedBy',
|
||||||
|
'lockedAt',
|
||||||
|
'lastRunAt',
|
||||||
|
'failCount',
|
||||||
|
'failReason',
|
||||||
|
'failedAt',
|
||||||
|
'lastFinishedAt',
|
||||||
|
]),
|
||||||
|
running: job.isRunning(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
// import { ObjectId } from 'mongoose';
|
||||||
import { ServiceError } from 'exceptions';
|
import { ServiceError } from 'exceptions';
|
||||||
import { ISystemService, ISystemUser, ITenant } from 'interfaces';
|
import { IOrganizationBuildDTO, ISystemUser, ITenant } from 'interfaces';
|
||||||
import {
|
import {
|
||||||
EventDispatcher,
|
EventDispatcher,
|
||||||
EventDispatcherInterface,
|
EventDispatcherInterface,
|
||||||
@@ -12,12 +14,15 @@ import {
|
|||||||
TenantDatabaseNotBuilt,
|
TenantDatabaseNotBuilt,
|
||||||
} from 'exceptions';
|
} from 'exceptions';
|
||||||
import TenantsManager from 'services/Tenancy/TenantsManager';
|
import TenantsManager from 'services/Tenancy/TenantsManager';
|
||||||
|
import { Tenant, TenantMetadata } from 'system/models';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
TENANT_NOT_FOUND: 'tenant_not_found',
|
TENANT_NOT_FOUND: 'tenant_not_found',
|
||||||
TENANT_ALREADY_INITIALIZED: 'tenant_already_initialized',
|
TENANT_ALREADY_BUILT: 'TENANT_ALREADY_BUILT',
|
||||||
TENANT_ALREADY_SEEDED: 'tenant_already_seeded',
|
TENANT_ALREADY_SEEDED: 'tenant_already_seeded',
|
||||||
TENANT_DB_NOT_BUILT: 'tenant_db_not_built',
|
TENANT_DB_NOT_BUILT: 'tenant_db_not_built',
|
||||||
|
TENANT_IS_BUILDING: 'TENANT_IS_BUILDING',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -34,82 +39,84 @@ export default class OrganizationService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
tenantsManager: TenantsManager;
|
tenantsManager: TenantsManager;
|
||||||
|
|
||||||
|
@Inject('agenda')
|
||||||
|
agenda: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the database schema and seed data of the given organization id.
|
* Builds the database schema and seed data of the given organization id.
|
||||||
* @param {srting} organizationId
|
* @param {srting} organizationId
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async build(organizationId: string, user: ISystemUser): Promise<void> {
|
public async build(tenantId: number): Promise<void> {
|
||||||
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
|
const tenant = await this.getTenantOrThrowError(tenantId);
|
||||||
|
|
||||||
|
// Throw error if the tenant is already initialized.
|
||||||
this.throwIfTenantInitizalized(tenant);
|
this.throwIfTenantInitizalized(tenant);
|
||||||
|
|
||||||
const tenantHasDB = await this.tenantsManager.hasDatabase(tenant);
|
// Drop the database if is already exists.
|
||||||
|
await this.tenantsManager.dropDatabaseIfExists(tenant);
|
||||||
|
|
||||||
try {
|
// Creates a new database.
|
||||||
if (!tenantHasDB) {
|
await this.tenantsManager.createDatabase(tenant);
|
||||||
this.logger.info('[organization] trying to create tenant database.', {
|
|
||||||
organizationId, userId: user.id,
|
|
||||||
});
|
|
||||||
await this.tenantsManager.createDatabase(tenant);
|
|
||||||
}
|
|
||||||
this.logger.info('[organization] trying to migrate tenant database.', {
|
|
||||||
organizationId, userId: user.id,
|
|
||||||
});
|
|
||||||
await this.tenantsManager.migrateTenant(tenant);
|
|
||||||
|
|
||||||
// Throws `onOrganizationBuild` event.
|
// Migrate the tenant.
|
||||||
this.eventDispatcher.dispatch(events.organization.build, { tenant, user });
|
const migratedTenant = await this.tenantsManager.migrateTenant(tenant);
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof TenantAlreadyInitialized) {
|
|
||||||
throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Seed tenant.
|
||||||
* Seeds initial core data to the given organization tenant.
|
const seededTenant = await this.tenantsManager.seedTenant(migratedTenant);
|
||||||
* @param {number} organizationId
|
|
||||||
* @return {Promise<void>}
|
|
||||||
*/
|
|
||||||
public async seed(organizationId: string): Promise<void> {
|
|
||||||
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
|
|
||||||
this.throwIfTenantSeeded(tenant);
|
|
||||||
|
|
||||||
try {
|
// Markes the tenant as completed builing.
|
||||||
this.logger.info('[organization] trying to seed tenant database.', {
|
await Tenant.markAsBuilt(tenantId);
|
||||||
organizationId,
|
await Tenant.markAsBuildCompleted(tenantId);
|
||||||
});
|
|
||||||
await this.tenantsManager.seedTenant(tenant);
|
|
||||||
|
|
||||||
// Throws `onOrganizationBuild` event.
|
// Throws `onOrganizationBuild` event.
|
||||||
this.eventDispatcher.dispatch(events.organization.seeded, { tenant });
|
this.eventDispatcher.dispatch(events.organization.build, {
|
||||||
} catch (error) {
|
tenant: seededTenant,
|
||||||
if (error instanceof TenantAlreadySeeded) {
|
|
||||||
throw new ServiceError(ERRORS.TENANT_ALREADY_SEEDED);
|
|
||||||
} else if (error instanceof TenantDatabaseNotBuilt) {
|
|
||||||
throw new ServiceError(ERRORS.TENANT_DB_NOT_BUILT);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listing all associated organizations to the given user.
|
|
||||||
* @param {ISystemUser} user -
|
|
||||||
* @return {Promise<void>}
|
|
||||||
*/
|
|
||||||
public async listOrganizations(user: ISystemUser): Promise<ITenant[]> {
|
|
||||||
this.logger.info('[organization] trying to list all organizations.', {
|
|
||||||
user,
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { tenantRepository } = this.sysRepositories;
|
/**
|
||||||
const tenant = await tenantRepository.findOneById(user.tenantId);
|
*
|
||||||
|
* @param tenantId
|
||||||
|
* @param buildDTO
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async buildRunJob(tenantId: number, buildDTO: IOrganizationBuildDTO) {
|
||||||
|
const tenant = await this.getTenantOrThrowError(tenantId);
|
||||||
|
|
||||||
return [tenant];
|
// Throw error if the tenant is already initialized.
|
||||||
|
this.throwIfTenantInitizalized(tenant);
|
||||||
|
|
||||||
|
// Throw error if tenant is currently building.
|
||||||
|
this.throwIfTenantIsBuilding(tenant);
|
||||||
|
|
||||||
|
// Saves the tenant metadata.
|
||||||
|
await this.saveTenantMetadata(tenant, buildDTO);
|
||||||
|
|
||||||
|
// Send welcome mail to the user.
|
||||||
|
const jobMeta = await this.agenda.now('organization-setup', {
|
||||||
|
tenantId,
|
||||||
|
buildDTO,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throwIfTenantIsBuilding(tenant) {
|
||||||
|
if (tenant.buildJobId) {
|
||||||
|
throw new ServiceError(ERRORS.TENANT_IS_BUILDING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async revertBuildRunJob(tenantId: number, jobId: string) {
|
||||||
|
await Tenant.markAsBuildCompleted(tenantId, jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,9 +124,11 @@ export default class OrganizationService {
|
|||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @returns {Promise<ITenant[]>}
|
* @returns {Promise<ITenant[]>}
|
||||||
*/
|
*/
|
||||||
public async currentOrganization(tenantId: number): Promise<ITenant[]> {
|
public async currentOrganization(tenantId: number): Promise<ITenant> {
|
||||||
const { tenantRepository } = this.sysRepositories;
|
const tenant = await Tenant.query()
|
||||||
const tenant = await tenantRepository.findOneById(tenantId, ['subscriptions']);
|
.findById(tenantId)
|
||||||
|
.withGraphFetched('subscriptions')
|
||||||
|
.withGraphFetched('metadata');
|
||||||
|
|
||||||
this.throwIfTenantNotExists(tenant);
|
this.throwIfTenantNotExists(tenant);
|
||||||
|
|
||||||
@@ -132,7 +141,6 @@ export default class OrganizationService {
|
|||||||
*/
|
*/
|
||||||
private throwIfTenantNotExists(tenant: ITenant) {
|
private throwIfTenantNotExists(tenant: ITenant) {
|
||||||
if (!tenant) {
|
if (!tenant) {
|
||||||
this.logger.info('[tenant_db_build] organization id not found.');
|
|
||||||
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
|
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,33 +150,34 @@ export default class OrganizationService {
|
|||||||
* @param {ITenant} tenant
|
* @param {ITenant} tenant
|
||||||
*/
|
*/
|
||||||
private throwIfTenantInitizalized(tenant: ITenant) {
|
private throwIfTenantInitizalized(tenant: ITenant) {
|
||||||
if (tenant.initializedAt) {
|
if (tenant.builtAt) {
|
||||||
throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED);
|
throw new ServiceError(ERRORS.TENANT_ALREADY_BUILT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throws service if the tenant already seeded.
|
* Saves the organization metadata.
|
||||||
* @param {ITenant} tenant
|
* @param tenant
|
||||||
|
* @param buildDTO
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
private throwIfTenantSeeded(tenant: ITenant) {
|
private saveTenantMetadata(tenant: ITenant, buildDTO) {
|
||||||
if (tenant.seededAt) {
|
return TenantMetadata.query().insert({
|
||||||
throw new ServiceError(ERRORS.TENANT_ALREADY_SEEDED);
|
tenantId: tenant.id,
|
||||||
}
|
...buildDTO,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve tenant model by the given organization id or throw not found
|
* Retrieve tenant of throw not found error.
|
||||||
* error if the tenant not exists on the storage.
|
* @param {number} tenantId -
|
||||||
* @param {string} organizationId
|
|
||||||
* @return {ITenant}
|
|
||||||
*/
|
*/
|
||||||
private async getTenantByOrgIdOrThrowError(organizationId: string) {
|
async getTenantOrThrowError(tenantId: number): Promise<ITenant> {
|
||||||
const { tenantRepository } = this.sysRepositories;
|
const tenant = await Tenant.query().findById(tenantId);
|
||||||
const tenant = await tenantRepository.findOne({ organizationId });
|
|
||||||
|
|
||||||
this.throwIfTenantNotExists(tenant);
|
|
||||||
|
|
||||||
|
if (!tenant) {
|
||||||
|
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
|
||||||
|
}
|
||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import SystemService from 'services/Tenancy/SystemService';
|
|||||||
import { TenantDBAlreadyExists } from 'exceptions';
|
import { TenantDBAlreadyExists } from 'exceptions';
|
||||||
import { tenantKnexConfig, tenantSeedConfig } from 'config/knexConfig';
|
import { tenantKnexConfig, tenantSeedConfig } from 'config/knexConfig';
|
||||||
|
|
||||||
export default class TenantDBManager implements ITenantDBManager{
|
export default class TenantDBManager implements ITenantDBManager {
|
||||||
static knexCache: { [key: string]: Knex; } = {};
|
static knexCache: { [key: string]: Knex } = {};
|
||||||
|
|
||||||
// System database manager.
|
// System database manager.
|
||||||
dbManager: any;
|
dbManager: any;
|
||||||
@@ -41,8 +41,12 @@ export default class TenantDBManager implements ITenantDBManager{
|
|||||||
*/
|
*/
|
||||||
public async databaseExists(tenant: ITenant) {
|
public async databaseExists(tenant: ITenant) {
|
||||||
const databaseName = this.getDatabaseName(tenant);
|
const databaseName = this.getDatabaseName(tenant);
|
||||||
const results = await this.sysKnex
|
|
||||||
.raw('SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "?"', databaseName);
|
const results = await this.sysKnex.raw(
|
||||||
|
'SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "' +
|
||||||
|
databaseName +
|
||||||
|
'"'
|
||||||
|
);
|
||||||
|
|
||||||
return results[0].length > 0;
|
return results[0].length > 0;
|
||||||
}
|
}
|
||||||
@@ -59,6 +63,30 @@ export default class TenantDBManager implements ITenantDBManager{
|
|||||||
await this.dbManager.createDb(databaseName);
|
await this.dbManager.createDb(databaseName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.dbManager.dropDb(databaseName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate tenant database schema to the latest version.
|
* Migrate tenant database schema to the latest version.
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
@@ -100,6 +128,9 @@ export default class TenantDBManager implements ITenantDBManager{
|
|||||||
return knexInstance;
|
return knexInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve knex instance from the givne tenant.
|
||||||
|
*/
|
||||||
public getKnexInstance(tenantId: number) {
|
public getKnexInstance(tenantId: number) {
|
||||||
const key: string = `${tenantId}`;
|
const key: string = `${tenantId}`;
|
||||||
let knexInstance = TenantDBManager.knexCache[key];
|
let knexInstance = TenantDBManager.knexCache[key];
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import { Container, Inject, Service } from 'typedi';
|
import { Container, Inject, Service } from 'typedi';
|
||||||
import { ServiceError } from 'exceptions';
|
import { ServiceError } from 'exceptions';
|
||||||
import {
|
import { ITenantManager, ITenant, ITenantDBManager } from 'interfaces';
|
||||||
ITenantManager,
|
|
||||||
ITenant,
|
|
||||||
ITenantDBManager,
|
|
||||||
} from 'interfaces';
|
|
||||||
import {
|
import {
|
||||||
EventDispatcherInterface,
|
EventDispatcherInterface,
|
||||||
EventDispatcher,
|
EventDispatcher,
|
||||||
} from 'decorators/eventDispatcher';
|
} from 'decorators/eventDispatcher';
|
||||||
import { TenantAlreadyInitialized, TenantAlreadySeeded, TenantDatabaseNotBuilt } from 'exceptions';
|
import {
|
||||||
|
TenantAlreadyInitialized,
|
||||||
|
TenantAlreadySeeded,
|
||||||
|
TenantDatabaseNotBuilt,
|
||||||
|
} from 'exceptions';
|
||||||
import TenantDBManager from 'services/Tenancy/TenantDBManager';
|
import TenantDBManager from 'services/Tenancy/TenantDBManager';
|
||||||
import events from 'subscribers/events';
|
import events from 'subscribers/events';
|
||||||
|
import { Tenant } from 'system/models';
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
TENANT_ALREADY_CREATED: 'TENANT_ALREADY_CREATED',
|
TENANT_ALREADY_CREATED: 'TENANT_ALREADY_CREATED',
|
||||||
TENANT_NOT_EXISTS: 'TENANT_NOT_EXISTS'
|
TENANT_NOT_EXISTS: 'TENANT_NOT_EXISTS',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tenants manager service.
|
// Tenants manager service.
|
||||||
@Service()
|
@Service()
|
||||||
export default class TenantsManagerService implements ITenantManager{
|
export default class TenantsManagerService implements ITenantManager {
|
||||||
static instances: { [key: number]: ITenantManager } = {};
|
static instances: { [key: number]: ITenantManager } = {};
|
||||||
|
|
||||||
@EventDispatcher()
|
@EventDispatcher()
|
||||||
@@ -63,6 +64,15 @@ export default class TenantsManagerService implements ITenantManager{
|
|||||||
this.eventDispatcher.dispatch(events.tenantManager.databaseCreated);
|
this.eventDispatcher.dispatch(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.
|
* Detarmines the tenant has database.
|
||||||
* @param {ITenant} tenant
|
* @param {ITenant} tenant
|
||||||
@@ -77,15 +87,20 @@ export default class TenantsManagerService implements ITenantManager{
|
|||||||
* @param {ITenant} tenant
|
* @param {ITenant} tenant
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async migrateTenant(tenant: ITenant) {
|
public async migrateTenant(tenant: ITenant): Promise<void> {
|
||||||
|
// Throw error if the tenant already initialized.
|
||||||
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||||
|
|
||||||
const { tenantRepository } = this.sysRepositories;
|
// Migrate the database tenant.
|
||||||
|
|
||||||
await this.tenantDBManager.migrate(tenant);
|
await this.tenantDBManager.migrate(tenant);
|
||||||
await tenantRepository.markAsInitialized(tenant.id);
|
|
||||||
|
|
||||||
this.eventDispatcher.dispatch(events.tenantManager.tenantMigrated, { tenant });
|
// Mark the tenant as initialized.
|
||||||
|
await Tenant.markAsInitialized(tenant.id);
|
||||||
|
|
||||||
|
// Triggers `onTenantMigrated` event.
|
||||||
|
this.eventDispatcher.dispatch(events.tenantManager.tenantMigrated, {
|
||||||
|
tenantId: tenant.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,19 +108,23 @@ export default class TenantsManagerService implements ITenantManager{
|
|||||||
* @param {ITenant} tenant
|
* @param {ITenant} tenant
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async seedTenant(tenant: ITenant) {
|
public async seedTenant(tenant: ITenant): Promise<void> {
|
||||||
|
// Throw error if the tenant is not built yet.
|
||||||
this.throwErrorIfTenantNotBuilt(tenant);
|
this.throwErrorIfTenantNotBuilt(tenant);
|
||||||
this.throwErrorIfTenantAlreadySeeded(tenant);
|
|
||||||
|
|
||||||
const { tenantRepository } = this.sysRepositories;
|
// Throw error if the tenant is not seeded yet.
|
||||||
|
this.throwErrorIfTenantAlreadySeeded(tenant);
|
||||||
|
|
||||||
// Seed the tenant database.
|
// Seed the tenant database.
|
||||||
await this.tenantDBManager.seed(tenant);
|
await this.tenantDBManager.seed(tenant);
|
||||||
|
|
||||||
// Mark the tenant as seeded in specific date.
|
// Mark the tenant as seeded in specific date.
|
||||||
await tenantRepository.markAsSeeded(tenant.id);
|
await Tenant.markAsSeeded(tenant.id);
|
||||||
|
|
||||||
this.eventDispatcher.dispatch(events.tenantManager.tenantSeeded);
|
// Triggers `onTenantSeeded` event.
|
||||||
|
this.eventDispatcher.dispatch(events.tenantManager.tenantSeeded, {
|
||||||
|
tenantId: tenant.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ exports.up = function(knex) {
|
|||||||
table.dateTime('under_maintenance_since').nullable();
|
table.dateTime('under_maintenance_since').nullable();
|
||||||
table.dateTime('initialized_at').nullable();
|
table.dateTime('initialized_at').nullable();
|
||||||
table.dateTime('seeded_at').nullable();
|
table.dateTime('seeded_at').nullable();
|
||||||
|
table.dateTime('built_at').nullable();
|
||||||
|
table.string('build_job_id');
|
||||||
|
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ exports.up = function(knex) {
|
|||||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
||||||
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
|
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
|
||||||
|
|
||||||
table.dateTime('trial_started_at').nullable();
|
|
||||||
table.dateTime('trial_ends_at').nullable();
|
|
||||||
|
|
||||||
table.dateTime('starts_at').nullable();
|
table.dateTime('starts_at').nullable();
|
||||||
table.dateTime('ends_at').nullable();
|
table.dateTime('ends_at').nullable();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
exports.up = function(knex) {
|
exports.up = function(knex) {
|
||||||
return knex.schema.createTable('subscription_licenses', table => {
|
return knex.schema.createTable('subscription_licenses', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
|
|
||||||
table.string('license_code').unique().index();
|
table.string('license_code').unique().index();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.createTable('tenants_metadata', (table) => {
|
||||||
|
table.bigIncrements();
|
||||||
|
table.integer('tenant_id').unsigned();
|
||||||
|
|
||||||
|
table.string('organization_name');
|
||||||
|
table.string('industry');
|
||||||
|
|
||||||
|
table.string('base_currency');
|
||||||
|
|
||||||
|
table.string('timezone');
|
||||||
|
table.string('date_format');
|
||||||
|
|
||||||
|
table.string('fiscal_year');
|
||||||
|
table.string('financial_start_date');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.dropTableIfExists('tenants_metadata');
|
||||||
|
};
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import BaseModel from 'models/Model';
|
import moment from 'moment';
|
||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
|
import uniqid from 'uniqid';
|
||||||
import SubscriptionPeriod from 'services/Subscription/SubscriptionPeriod';
|
import SubscriptionPeriod from 'services/Subscription/SubscriptionPeriod';
|
||||||
|
import BaseModel from 'models/Model';
|
||||||
|
import TenantMetadata from './TenantMetadata';
|
||||||
|
|
||||||
export default class Tenant extends BaseModel {
|
export default class Tenant extends BaseModel {
|
||||||
/**
|
/**
|
||||||
@@ -21,7 +24,7 @@ export default class Tenant extends BaseModel {
|
|||||||
* Virtual attributes.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['isReady'];
|
return ['isReady', 'isBuildRunning'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +34,13 @@ export default class Tenant extends BaseModel {
|
|||||||
return !!(this.initializedAt && this.seededAt);
|
return !!(this.initializedAt && this.seededAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarimes the tenant whether is build currently running.
|
||||||
|
*/
|
||||||
|
get isBuildRunning() {
|
||||||
|
return !!this.buildJobId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query modifiers.
|
* Query modifiers.
|
||||||
*/
|
*/
|
||||||
@@ -47,6 +57,7 @@ export default class Tenant extends BaseModel {
|
|||||||
*/
|
*/
|
||||||
static get relationMappings() {
|
static get relationMappings() {
|
||||||
const PlanSubscription = require('./Subscriptions/PlanSubscription');
|
const PlanSubscription = require('./Subscriptions/PlanSubscription');
|
||||||
|
const TenantMetadata = require('./TenantMetadata');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
@@ -55,9 +66,17 @@ export default class Tenant extends BaseModel {
|
|||||||
join: {
|
join: {
|
||||||
from: 'tenants.id',
|
from: 'tenants.id',
|
||||||
to: 'subscription_plan_subscriptions.tenantId',
|
to: 'subscription_plan_subscriptions.tenantId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
metadata: {
|
||||||
|
relation: Model.HasOneRelation,
|
||||||
|
modelClass: TenantMetadata.default,
|
||||||
|
join: {
|
||||||
|
from: 'tenants.id',
|
||||||
|
to: 'tenants_metadata.tenantId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,22 +90,90 @@ export default class Tenant extends BaseModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a new subscription for the associated tenant.
|
* Records a new subscription for the associated tenant.
|
||||||
* @param {string} subscriptionSlug
|
|
||||||
* @param {IPlan} plan
|
|
||||||
*/
|
*/
|
||||||
newSubscription(subscriptionSlug, plan) {
|
newSubscription(subscriptionSlug, plan) {
|
||||||
const trial = new SubscriptionPeriod(plan.trialInterval, plan.trialPeriod)
|
const trial = new SubscriptionPeriod(plan.trialInterval, plan.trialPeriod);
|
||||||
const period = new SubscriptionPeriod(plan.invoiceInterval, plan.invoicePeriod, trial.getEndDate());
|
const period = new SubscriptionPeriod(
|
||||||
|
plan.invoiceInterval,
|
||||||
|
plan.invoicePeriod,
|
||||||
|
trial.getEndDate()
|
||||||
|
);
|
||||||
|
|
||||||
return this.$relatedQuery('subscriptions').insert({
|
return this.$relatedQuery('subscriptions').insert({
|
||||||
slug: subscriptionSlug,
|
slug: subscriptionSlug,
|
||||||
planId: plan.id,
|
planId: plan.id,
|
||||||
|
|
||||||
trialStartedAt: trial.getStartDate(),
|
|
||||||
trialEndsAt: trial.getEndDate(),
|
|
||||||
|
|
||||||
startsAt: period.getStartDate(),
|
startsAt: period.getStartDate(),
|
||||||
endsAt: period.getEndDate(),
|
endsAt: period.getEndDate(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new tenant with random organization id.
|
||||||
|
*/
|
||||||
|
static createWithUniqueOrgId(uniqId) {
|
||||||
|
const organizationId = uniqid() || uniqId;
|
||||||
|
return this.query().insert({ organizationId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark as seeded.
|
||||||
|
* @param {number} tenantId
|
||||||
|
*/
|
||||||
|
static markAsSeeded(tenantId) {
|
||||||
|
const seededAt = moment().toMySqlDateTime();
|
||||||
|
return this.query().update({ seededAt }).where({ id: tenantId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the the given organization as initialized.
|
||||||
|
* @param {string} organizationId
|
||||||
|
*/
|
||||||
|
static markAsInitialized(tenantId) {
|
||||||
|
const initializedAt = moment().toMySqlDateTime();
|
||||||
|
return this.query().update({ initializedAt }).where({ id: tenantId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the given tenant as built.
|
||||||
|
*/
|
||||||
|
static markAsBuilt(tenantId) {
|
||||||
|
const builtAt = moment().toMySqlDateTime();
|
||||||
|
return this.query().update({ builtAt }).where({ id: tenantId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the given tenant as built.
|
||||||
|
*/
|
||||||
|
static markAsBuilding(tenantId, buildJobId) {
|
||||||
|
return this.query().update({ buildJobId }).where({ id: tenantId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the given tenant as built.
|
||||||
|
*/
|
||||||
|
static markAsBuildCompleted(tenantId) {
|
||||||
|
return this.query().update({ buildJobId: null }).where({ id: tenantId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the metadata of the given tenant.
|
||||||
|
*/
|
||||||
|
static async saveMetadata(tenantId, metadata) {
|
||||||
|
const foundMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||||
|
const updateOrInsert = foundMetadata ? 'update' : 'insert';
|
||||||
|
|
||||||
|
return TenantMetadata.query()
|
||||||
|
[updateOrInsert]({
|
||||||
|
tenantId,
|
||||||
|
...metadata,
|
||||||
|
})
|
||||||
|
.where({ tenantId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the metadata of the tenant.
|
||||||
|
*/
|
||||||
|
saveMetadata(metadata) {
|
||||||
|
return Tenant.saveMetadata(this.id, metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
server/src/system/models/TenantMetadata.js
Normal file
10
server/src/system/models/TenantMetadata.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import BaseModel from 'models/Model';
|
||||||
|
|
||||||
|
export default class TenantMetadata extends BaseModel {
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'tenants_metadata';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import PlanFeature from './Subscriptions/PlanFeature';
|
|||||||
import PlanSubscription from './Subscriptions/PlanSubscription';
|
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||||
import License from './Subscriptions/License';
|
import License from './Subscriptions/License';
|
||||||
import Tenant from './Tenant';
|
import Tenant from './Tenant';
|
||||||
|
import TenantMetadata from './TenantMetadata';
|
||||||
import SystemUser from './SystemUser';
|
import SystemUser from './SystemUser';
|
||||||
import PasswordReset from './PasswordReset';
|
import PasswordReset from './PasswordReset';
|
||||||
import Invite from './Invite';
|
import Invite from './Invite';
|
||||||
@@ -14,6 +15,7 @@ export {
|
|||||||
PlanSubscription,
|
PlanSubscription,
|
||||||
License,
|
License,
|
||||||
Tenant,
|
Tenant,
|
||||||
|
TenantMetadata,
|
||||||
SystemUser,
|
SystemUser,
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
Invite,
|
Invite,
|
||||||
|
|||||||
@@ -6,18 +6,35 @@ exports.seed = (knex) => {
|
|||||||
// Inserts seed entries
|
// Inserts seed entries
|
||||||
return knex('subscription_plans').insert([
|
return knex('subscription_plans').insert([
|
||||||
{
|
{
|
||||||
name: 'Free',
|
name: 'Essentials',
|
||||||
slug: 'free',
|
slug: 'essentials-monthly',
|
||||||
price: 0,
|
price: 100,
|
||||||
active: true,
|
active: true,
|
||||||
currency: 'LYD',
|
currency: 'LYD',
|
||||||
trial_period: 7,
|
trial_period: 7,
|
||||||
trial_interval: 'days',
|
trial_interval: 'days',
|
||||||
index: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Starter',
|
name: 'Essentials',
|
||||||
slug: 'starter',
|
slug: 'essentials-yearly',
|
||||||
|
price: 1200,
|
||||||
|
active: true,
|
||||||
|
currency: 'LYD',
|
||||||
|
trial_period: 12,
|
||||||
|
trial_interval: 'months',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pro',
|
||||||
|
slug: 'pro-monthly',
|
||||||
|
price: 200,
|
||||||
|
active: true,
|
||||||
|
currency: 'LYD',
|
||||||
|
trial_period: 1,
|
||||||
|
trial_interval: 'months',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pro',
|
||||||
|
slug: 'pro-yearly',
|
||||||
price: 500,
|
price: 500,
|
||||||
active: true,
|
active: true,
|
||||||
currency: 'LYD',
|
currency: 'LYD',
|
||||||
@@ -26,14 +43,23 @@ exports.seed = (knex) => {
|
|||||||
index: 2,
|
index: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Growth',
|
name: 'Plus',
|
||||||
slug: 'growth',
|
slug: 'plus-monthly',
|
||||||
price: 1000,
|
price: 200,
|
||||||
|
active: true,
|
||||||
|
currency: 'LYD',
|
||||||
|
trial_period: 1,
|
||||||
|
trial_interval: 'months',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Plus',
|
||||||
|
slug: 'plus-yearly',
|
||||||
|
price: 500,
|
||||||
active: true,
|
active: true,
|
||||||
currency: 'LYD',
|
currency: 'LYD',
|
||||||
invoice_period: 12,
|
invoice_period: 12,
|
||||||
invoice_interval: 'month',
|
invoice_interval: 'month',
|
||||||
index: 3,
|
index: 2,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user