mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
add server to monorepo.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
|
||||
exports.up = (knex) => knex.schema.createTable('password_resets', (table) => {
|
||||
table.increments();
|
||||
table.string('email').index();
|
||||
table.string('token').index();
|
||||
table.timestamp('created_at');
|
||||
});
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('password_resets');
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('tenants', (table) => {
|
||||
table.bigIncrements();
|
||||
table.string('organization_id').index();
|
||||
|
||||
table.dateTime('under_maintenance_since').nullable();
|
||||
table.dateTime('initialized_at').nullable();
|
||||
table.dateTime('seeded_at').nullable();
|
||||
table.dateTime('built_at').nullable();
|
||||
table.string('build_job_id');
|
||||
|
||||
table.integer('database_batch');
|
||||
table.string('upgrade_job_id');
|
||||
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('tenants');
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
exports.up = (knex) => {
|
||||
return knex.schema.createTable('users', (table) => {
|
||||
table.increments();
|
||||
table.string('first_name');
|
||||
table.string('last_name');
|
||||
table.string('email').index();
|
||||
table.string('phone_number').index();
|
||||
table.string('password');
|
||||
table.boolean('active').index();
|
||||
table.string('language');
|
||||
table
|
||||
.bigInteger('tenant_id')
|
||||
.unsigned()
|
||||
.index()
|
||||
.references('id')
|
||||
.inTable('tenants');
|
||||
table.dateTime('invite_accepted_at').index();
|
||||
table.dateTime('last_login_at').index();
|
||||
table.dateTime('deleted_at').index();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => {
|
||||
return knex.schema.dropTableIfExists('users');
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('user_invites', (table) => {
|
||||
table.increments();
|
||||
table.string('email').index();
|
||||
table.string('token').unique().index();
|
||||
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
|
||||
table.integer('user_id').unsigned().index().references('id').inTable('users');
|
||||
table.datetime('created_at');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('user_invites');
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscriptions_plans', table => {
|
||||
table.increments();
|
||||
|
||||
table.string('name');
|
||||
table.string('description');
|
||||
table.decimal('price');
|
||||
table.string('currency', 3);
|
||||
|
||||
table.integer('trial_period');
|
||||
table.string('trial_interval');
|
||||
|
||||
table.integer('invoice_period');
|
||||
table.string('invoice_interval');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscriptions_plans')
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscription_plans', table => {
|
||||
table.increments();
|
||||
table.string('slug');
|
||||
table.string('name');
|
||||
table.string('desc');
|
||||
table.boolean('active');
|
||||
|
||||
table.decimal('price').unsigned();
|
||||
table.string('currency', 3);
|
||||
|
||||
table.decimal('trial_period').nullable();
|
||||
table.string('trial_interval').nullable();
|
||||
|
||||
table.decimal('invoice_period').nullable();
|
||||
table.string('invoice_interval').nullable();
|
||||
|
||||
table.integer('index').unsigned();
|
||||
table.timestamps();
|
||||
}).then(() => {
|
||||
return knex.seed.run({
|
||||
specific: 'seed_subscriptions_plans.js',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscription_plans')
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscription_plan_features', table => {
|
||||
table.increments();
|
||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
||||
table.string('slug');
|
||||
table.string('name');
|
||||
table.string('description');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscription_plan_features');
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscription_plan_subscriptions', table => {
|
||||
table.increments('id');
|
||||
table.string('slug');
|
||||
|
||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
||||
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
|
||||
|
||||
table.dateTime('starts_at').nullable();
|
||||
table.dateTime('ends_at').nullable();
|
||||
|
||||
table.dateTime('cancels_at').nullable();
|
||||
table.dateTime('canceled_at').nullable();
|
||||
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscription_plan_subscriptions');
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscription_licenses', (table) => {
|
||||
table.increments();
|
||||
|
||||
table.string('license_code').unique().index();
|
||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
||||
|
||||
table.integer('license_period').unsigned();
|
||||
table.string('period_interval');
|
||||
|
||||
table.dateTime('sent_at').index();
|
||||
table.dateTime('disabled_at').index();
|
||||
table.dateTime('used_at').index();
|
||||
|
||||
table.timestamps();
|
||||
})
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscription_licenses');
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('tenants_metadata', (table) => {
|
||||
table.bigIncrements();
|
||||
table.integer('tenant_id').unsigned();
|
||||
|
||||
table.string('name');
|
||||
table.string('industry');
|
||||
table.string('location');
|
||||
|
||||
table.string('base_currency');
|
||||
table.string('language');
|
||||
|
||||
table.string('timezone');
|
||||
table.string('date_format');
|
||||
|
||||
table.string('fiscal_year');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists('tenants_metadata');
|
||||
};
|
||||
30
packages/server/src/system/models/Invite.ts
Normal file
30
packages/server/src/system/models/Invite.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class UserInvite extends SystemModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'user_invites';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
notExpired(query) {
|
||||
const comp = moment().subtract(24, 'hours').toMySqlDateTime();
|
||||
query.where('created_at', '>=', comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
packages/server/src/system/models/PasswordReset.ts
Normal file
17
packages/server/src/system/models/PasswordReset.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
|
||||
export default class PasswordResets extends SystemModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'password_resets';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt'];
|
||||
}
|
||||
}
|
||||
129
packages/server/src/system/models/Subscriptions/License.ts
Normal file
129
packages/server/src/system/models/Subscriptions/License.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import moment from 'moment';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
|
||||
export default class License extends SystemModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'subscription_licenses';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
// Filters active licenses.
|
||||
filterActiveLicense(query) {
|
||||
query.where('disabled_at', null);
|
||||
query.where('used_at', null);
|
||||
},
|
||||
|
||||
// Find license by its code or id.
|
||||
findByCodeOrId(query, id, code) {
|
||||
if (id) {
|
||||
query.where('id', id);
|
||||
}
|
||||
if (code) {
|
||||
query.where('license_code', code);
|
||||
}
|
||||
},
|
||||
|
||||
// Filters licenses list.
|
||||
filter(builder, licensesFilter) {
|
||||
if (licensesFilter.active) {
|
||||
builder.modify('filterActiveLicense');
|
||||
}
|
||||
if (licensesFilter.disabled) {
|
||||
builder.whereNot('disabled_at', null);
|
||||
}
|
||||
if (licensesFilter.used) {
|
||||
builder.whereNot('used_at', null);
|
||||
}
|
||||
if (licensesFilter.sent) {
|
||||
builder.whereNot('sent_at', null);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Plan = require('system/models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
plan: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Plan.default,
|
||||
join: {
|
||||
from: 'subscription_licenses.planId',
|
||||
to: 'subscriptions_plans.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given license code from the storage.
|
||||
* @param {string} licenseCode
|
||||
* @return {Promise}
|
||||
*/
|
||||
static deleteLicense(licenseCode, viaAttribute = 'license_code') {
|
||||
return this.query().where(viaAttribute, licenseCode).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given license code as disabled on the storage.
|
||||
* @param {string} licenseCode
|
||||
* @return {Promise}
|
||||
*/
|
||||
static markLicenseAsDisabled(licenseCode, viaAttribute = 'license_code') {
|
||||
return this.query().where(viaAttribute, licenseCode).patch({
|
||||
disabled_at: moment().toMySqlDateTime(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given license code as sent on the storage.
|
||||
* @param {string} licenseCode
|
||||
*/
|
||||
static markLicenseAsSent(licenseCode, viaAttribute = 'license_code') {
|
||||
return this.query().where(viaAttribute, licenseCode).patch({
|
||||
sent_at: moment().toMySqlDateTime(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given license code as used on the storage.
|
||||
* @param {string} licenseCode
|
||||
* @return {Promise}
|
||||
*/
|
||||
static markLicenseAsUsed(licenseCode, viaAttribute = 'license_code') {
|
||||
return this.query().where(viaAttribute, licenseCode).patch({
|
||||
used_at: moment().toMySqlDateTime(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IIPlan} plan
|
||||
* @return {boolean}
|
||||
*/
|
||||
isEqualPlanPeriod(plan) {
|
||||
return (
|
||||
this.invoicePeriod === plan.invoiceInterval &&
|
||||
license.licensePeriod === license.periodInterval
|
||||
);
|
||||
}
|
||||
}
|
||||
82
packages/server/src/system/models/Subscriptions/Plan.ts
Normal file
82
packages/server/src/system/models/Subscriptions/Plan.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import { PlanSubscription } from '..';
|
||||
|
||||
export default class Plan extends mixin(SystemModel) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'subscription_plans';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['isFree', 'hasTrial'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
getFeatureBySlug(builder, featureSlug) {
|
||||
builder.where('slug', featureSlug);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const PlanSubscription = require('system/models/Subscriptions/PlanSubscription');
|
||||
|
||||
return {
|
||||
/**
|
||||
* The plan may have many subscriptions.
|
||||
*/
|
||||
subscriptions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: PlanSubscription.default,
|
||||
join: {
|
||||
from: 'subscription_plans.id',
|
||||
to: 'subscription_plan_subscriptions.planId',
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plan is free.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isFree() {
|
||||
return this.price <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plan is paid.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isPaid() {
|
||||
return !this.isFree();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plan has trial.
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasTrial() {
|
||||
return this.trialPeriod && this.trialInterval;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
|
||||
export default class PlanFeature extends mixin(SystemModel) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'subscriptions.plan_features';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Plan = require('system/models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
plan: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Plan.default,
|
||||
join: {
|
||||
from: 'subscriptions.plan_features.planId',
|
||||
to: 'subscriptions.plans.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import moment from 'moment';
|
||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||
|
||||
export default class PlanSubscription extends mixin(SystemModel) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'subscription_plan_subscriptions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['active', 'inactive', 'ended', 'onTrial'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifiers queries.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
activeSubscriptions(builder) {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const now = moment().format(dateFormat);
|
||||
|
||||
builder.where('ends_at', '>', now);
|
||||
builder.where('trial_ends_at', '>', now);
|
||||
},
|
||||
|
||||
inactiveSubscriptions() {
|
||||
builder.modify('endedTrial');
|
||||
builder.modify('endedPeriod');
|
||||
},
|
||||
|
||||
subscriptionBySlug(builder, subscriptionSlug) {
|
||||
builder.where('slug', subscriptionSlug);
|
||||
},
|
||||
|
||||
endedTrial(builder) {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const endDate = moment().format(dateFormat);
|
||||
|
||||
builder.where('ends_at', '<=', endDate);
|
||||
},
|
||||
|
||||
endedPeriod(builder) {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const endDate = moment().format(dateFormat);
|
||||
|
||||
builder.where('trial_ends_at', '<=', endDate);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relations mappings.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Tenant = require('system/models/Tenant');
|
||||
const Plan = require('system/models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Plan subscription belongs to tenant.
|
||||
*/
|
||||
tenant: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Tenant.default,
|
||||
join: {
|
||||
from: 'subscription_plan_subscriptions.tenantId',
|
||||
to: 'tenants.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Plan description belongs to plan.
|
||||
*/
|
||||
plan: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Plan.default,
|
||||
join: {
|
||||
from: 'subscription_plan_subscriptions.planId',
|
||||
to: 'subscription_plans.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is active.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
active() {
|
||||
return !this.ended() || this.onTrial();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is inactive.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
inactive() {
|
||||
return !this.active();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription period has ended.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
ended() {
|
||||
return this.endsAt ? moment().isAfter(this.endsAt) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is currently on trial.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
onTrial() {
|
||||
return this.trailEndsAt ? moment().isAfter(this.trailEndsAt) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new period from the given details.
|
||||
* @param {string} invoiceInterval
|
||||
* @param {number} invoicePeriod
|
||||
* @param {string} start
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
static setNewPeriod(invoiceInterval, invoicePeriod, start) {
|
||||
const period = new SubscriptionPeriod(
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
start,
|
||||
);
|
||||
|
||||
const startsAt = period.getStartDate();
|
||||
const endsAt = period.getEndDate();
|
||||
|
||||
return { startsAt, endsAt };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renews subscription period.
|
||||
* @Promise
|
||||
*/
|
||||
renew(invoiceInterval, invoicePeriod) {
|
||||
const { startsAt, endsAt } = PlanSubscription.setNewPeriod(
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
);
|
||||
return this.$query().update({ startsAt, endsAt });
|
||||
}
|
||||
}
|
||||
19
packages/server/src/system/models/SystemModel.ts
Normal file
19
packages/server/src/system/models/SystemModel.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Container } from 'typedi';
|
||||
import BaseModel from 'models/Model';
|
||||
|
||||
export default class SystemModel extends BaseModel{
|
||||
/**
|
||||
* Loging all system database queries.
|
||||
* @param {...any} args
|
||||
*/
|
||||
static query(...args) {
|
||||
const Logger = Container.get('logger');
|
||||
return super.query(...args).onBuildKnex(knexQueryBuilder => {
|
||||
knexQueryBuilder.on('query', queryData => {
|
||||
Logger.info(`[query][system] ${queryData.sql}`, {
|
||||
bindings: queryData.bindings,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
85
packages/server/src/system/models/SystemUser.ts
Normal file
85
packages/server/src/system/models/SystemUser.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Model } from 'objection';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
||||
|
||||
export default class SystemUser extends SystemModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete query builder.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return SoftDeleteQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['fullName', 'isDeleted', 'isInviteAccepted'];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get isDeleted() {
|
||||
return !!this.deletedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get isInviteAccepted() {
|
||||
return !!this.inviteAcceptedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full name attribute.
|
||||
*/
|
||||
get fullName() {
|
||||
return (this.firstName + ' ' + this.lastName).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Tenant = require('system/models/Tenant');
|
||||
|
||||
return {
|
||||
/**
|
||||
* System user may belongs to tenant model.
|
||||
*/
|
||||
tenant: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Tenant.default,
|
||||
join: {
|
||||
from: 'users.tenantId',
|
||||
to: 'tenants.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the password of the user.
|
||||
* @param {String} password - The given password.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
verifyPassword(password) {
|
||||
return bcrypt.compareSync(password, this.password);
|
||||
}
|
||||
}
|
||||
226
packages/server/src/system/models/Tenant.ts
Normal file
226
packages/server/src/system/models/Tenant.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import moment from 'moment';
|
||||
import { Model } from 'objection';
|
||||
import uniqid from 'uniqid';
|
||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||
import BaseModel from 'models/Model';
|
||||
import TenantMetadata from './TenantMetadata';
|
||||
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||
|
||||
export default class Tenant extends BaseModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'tenants';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['isReady', 'isBuildRunning', 'isUpgradeRunning'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant is ready.
|
||||
*/
|
||||
get isReady() {
|
||||
return !!(this.initializedAt && this.seededAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarimes the tenant whether is build currently running.
|
||||
*/
|
||||
get isBuildRunning() {
|
||||
return !!this.buildJobId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the tenant whether is upgrade currently running.
|
||||
*/
|
||||
get isUpgradeRunning() {
|
||||
return !!this.upgradeJobId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query modifiers.
|
||||
*/
|
||||
static modifiers() {
|
||||
return {
|
||||
subscriptions(builder) {
|
||||
builder.withGraphFetched('subscriptions');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relations mappings.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const PlanSubscription = require('./Subscriptions/PlanSubscription');
|
||||
const TenantMetadata = require('./TenantMetadata');
|
||||
|
||||
return {
|
||||
subscriptions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: PlanSubscription.default,
|
||||
join: {
|
||||
from: 'tenants.id',
|
||||
to: 'subscription_plan_subscriptions.tenantId',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: TenantMetadata.default,
|
||||
join: {
|
||||
from: 'tenants.id',
|
||||
to: 'tenants_metadata.tenantId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the subscribed plans ids.
|
||||
* @return {number[]}
|
||||
*/
|
||||
async subscribedPlansIds() {
|
||||
const { subscriptions } = this;
|
||||
return chain(subscriptions).map('planId').unq();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} planId
|
||||
* @param {*} invoiceInterval
|
||||
* @param {*} invoicePeriod
|
||||
* @param {*} subscriptionSlug
|
||||
* @returns
|
||||
*/
|
||||
newSubscription(planId, invoiceInterval, invoicePeriod, subscriptionSlug) {
|
||||
return Tenant.newSubscription(
|
||||
this.id,
|
||||
planId,
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
subscriptionSlug,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a new subscription for the associated tenant.
|
||||
*/
|
||||
static newSubscription(
|
||||
tenantId,
|
||||
planId,
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
subscriptionSlug
|
||||
) {
|
||||
const period = new SubscriptionPeriod(invoiceInterval, invoicePeriod);
|
||||
|
||||
return PlanSubscription.query().insert({
|
||||
tenantId,
|
||||
slug: subscriptionSlug,
|
||||
planId,
|
||||
startsAt: period.getStartDate(),
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given tenant as upgrading.
|
||||
* @param {number} tenantId
|
||||
* @param {string} upgradeJobId
|
||||
* @returns
|
||||
*/
|
||||
static markAsUpgrading(tenantId, upgradeJobId) {
|
||||
return this.query().update({ upgradeJobId }).where({ id: tenantId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Markes the given tenant as upgraded.
|
||||
* @param {number} tenantId
|
||||
* @returns
|
||||
*/
|
||||
static markAsUpgraded(tenantId) {
|
||||
return this.query().update({ upgradeJobId: 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
packages/server/src/system/models/TenantMetadata.ts
Normal file
10
packages/server/src/system/models/TenantMetadata.ts
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';
|
||||
}
|
||||
}
|
||||
22
packages/server/src/system/models/index.ts
Normal file
22
packages/server/src/system/models/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
import Plan from './Subscriptions/Plan';
|
||||
import PlanFeature from './Subscriptions/PlanFeature';
|
||||
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||
import License from './Subscriptions/License';
|
||||
import Tenant from './Tenant';
|
||||
import TenantMetadata from './TenantMetadata';
|
||||
import SystemUser from './SystemUser';
|
||||
import PasswordReset from './PasswordReset';
|
||||
import Invite from './Invite';
|
||||
|
||||
export {
|
||||
Plan,
|
||||
PlanFeature,
|
||||
PlanSubscription,
|
||||
License,
|
||||
Tenant,
|
||||
TenantMetadata,
|
||||
SystemUser,
|
||||
PasswordReset,
|
||||
Invite,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import SystemRepository from '@/system/repositories/SystemRepository';
|
||||
import { PlanSubscription } from '@/system/models';
|
||||
|
||||
export default class SubscriptionRepository extends SystemRepository {
|
||||
/**
|
||||
* Gets the repository's model.
|
||||
*/
|
||||
get model() {
|
||||
return PlanSubscription.bindKnex(this.knex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve subscription from a given slug in specific tenant.
|
||||
* @param {string} slug
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
getBySlugInTenant(slug: string, tenantId: number) {
|
||||
const cacheKey = this.getCacheKey('getBySlugInTenant', slug, tenantId);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return PlanSubscription.query()
|
||||
.findOne('slug', slug)
|
||||
.where('tenant_id', tenantId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import CachableRepository from "repositories/CachableRepository";
|
||||
|
||||
export default class SystemRepository extends CachableRepository {
|
||||
|
||||
}
|
||||
101
packages/server/src/system/repositories/SystemUserRepository.ts
Normal file
101
packages/server/src/system/repositories/SystemUserRepository.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import moment from 'moment';
|
||||
import SystemRepository from '@/system/repositories/SystemRepository';
|
||||
import { SystemUser } from '@/system/models';
|
||||
import { ISystemUser } from '@/interfaces';
|
||||
|
||||
export default class SystemUserRepository extends SystemRepository {
|
||||
/**
|
||||
* Gets the repository's model.
|
||||
*/
|
||||
get model() {
|
||||
return SystemUser.bindKnex(this.knex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds system user by crediential.
|
||||
* @param {string} crediential - Phone number or email.
|
||||
* @return {ISystemUser}
|
||||
* @return {Promise<ISystemUser>}
|
||||
*/
|
||||
findByCrediential(crediential: string): Promise<ISystemUser> {
|
||||
const cacheKey = this.getCacheKey('findByCrediential', crediential);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return this.model.query()
|
||||
.findOne('email', crediential)
|
||||
.orWhere('phone_number', crediential);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user by id and tenant id.
|
||||
* @param {number} userId - User id.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @return {Promise<ISystemUser>}
|
||||
*/
|
||||
findOneByIdAndTenant(userId: number, tenantId: number): Promise<ISystemUser> {
|
||||
const cacheKey = this.getCacheKey('findOneByIdAndTenant', userId, tenantId);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return this.model.query()
|
||||
.findOne({ id: userId, tenant_id: tenantId });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve system user details by the given email.
|
||||
* @param {string} email - Email
|
||||
* @return {Promise<ISystemUser>}
|
||||
*/
|
||||
findOneByEmail(email: string): Promise<ISystemUser> {
|
||||
const cacheKey = this.getCacheKey('findOneByEmail', email);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return this.model.query().findOne('email', email);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user by phone number.
|
||||
* @param {string} phoneNumber - Phone number
|
||||
* @return {Promise<ISystemUser>}
|
||||
*/
|
||||
findOneByPhoneNumber(phoneNumber: string): Promise<ISystemUser> {
|
||||
const cacheKey = this.getCacheKey('findOneByPhoneNumber', phoneNumber);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return this.model.query()
|
||||
.findOne('phoneNumber', phoneNumber);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the last login date to the given system user.
|
||||
* @param {number} userId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
patchLastLoginAt(userId: number): Promise<void> {
|
||||
return super.update(
|
||||
{ last_login_at: moment().toMySqlDateTime() },
|
||||
{ id: userId }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate user by the given id.
|
||||
* @param {number} userId - User id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
activateById(userId: number): Promise<void> {
|
||||
return super.update({ active: 1 }, { id: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate user by the given id.
|
||||
* @param {number} userId - User id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
inactivateById(userId: number): Promise<void> {
|
||||
return super.update({ active: 0 }, { id: userId });
|
||||
}
|
||||
}
|
||||
43
packages/server/src/system/repositories/TenantRepository.ts
Normal file
43
packages/server/src/system/repositories/TenantRepository.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import moment from "moment";
|
||||
import uniqid from 'uniqid';
|
||||
import SystemRepository from "./SystemRepository";
|
||||
import { Tenant } from "@/system/models";
|
||||
import { ITenant } from '@/interfaces';
|
||||
|
||||
export default class TenantRepository extends SystemRepository {
|
||||
/**
|
||||
* Gets the repository's model.
|
||||
*/
|
||||
get model() {
|
||||
return Tenant.bindKnex(this.knex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tenant with random organization id.
|
||||
* @return {ITenant}
|
||||
*/
|
||||
createWithUniqueOrgId(uniqId?: string): Promise<ITenant>{
|
||||
const organizationId = uniqid() || uniqId;
|
||||
return super.create({ organizationId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as seeded.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
markAsSeeded(tenantId: number) {
|
||||
return super.update({
|
||||
seededAt: moment().toMySqlDateTime(),
|
||||
}, { id: tenantId })
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the the given organization as initialized.
|
||||
* @param {string} organizationId
|
||||
*/
|
||||
markAsInitialized(tenantId: number) {
|
||||
return super.update({
|
||||
initializedAt: moment().toMySqlDateTime(),
|
||||
}, { id: tenantId });
|
||||
}
|
||||
}
|
||||
9
packages/server/src/system/repositories/index.ts
Normal file
9
packages/server/src/system/repositories/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import SystemUserRepository from '@/system/repositories/SystemUserRepository';
|
||||
import SubscriptionRepository from '@/system/repositories/SubscriptionRepository';
|
||||
import TenantRepository from '@/system/repositories/TenantRepository';
|
||||
|
||||
export {
|
||||
SystemUserRepository,
|
||||
SubscriptionRepository,
|
||||
TenantRepository,
|
||||
};
|
||||
66
packages/server/src/system/seeds/seed_subscriptions_plans.js
Normal file
66
packages/server/src/system/seeds/seed_subscriptions_plans.js
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
exports.seed = (knex) => {
|
||||
// Deletes ALL existing entries
|
||||
return knex('subscription_plans').del()
|
||||
.then(() => {
|
||||
// Inserts seed entries
|
||||
return knex('subscription_plans').insert([
|
||||
{
|
||||
name: 'Essentials',
|
||||
slug: 'essentials-monthly',
|
||||
price: 100,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
trial_period: 7,
|
||||
trial_interval: 'days',
|
||||
},
|
||||
{
|
||||
name: 'Essentials',
|
||||
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,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
invoice_period: 12,
|
||||
invoice_interval: 'month',
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
name: 'Plus',
|
||||
slug: 'plus-monthly',
|
||||
price: 200,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
trial_period: 1,
|
||||
trial_interval: 'months',
|
||||
},
|
||||
{
|
||||
name: 'Plus',
|
||||
slug: 'plus-yearly',
|
||||
price: 500,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
invoice_period: 12,
|
||||
invoice_interval: 'month',
|
||||
index: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user