mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: remove path alias.
feat: remove Webpack and depend on nodemon. feat: refactoring expenses. feat: optimize system users with caching. feat: architecture tenant optimize.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { sumBy, chain } from 'lodash';
|
||||
import JournalPoster from "./JournalPoster";
|
||||
import JournalEntry from "./JournalEntry";
|
||||
import { AccountTransaction } from '@/models';
|
||||
import { IInventoryTransaction } from '@/interfaces';
|
||||
import { AccountTransaction } from 'models';
|
||||
import { IInventoryTransaction } from 'interfaces';
|
||||
import AccountsService from '../Accounts/AccountsService';
|
||||
import { IInventoryTransaction, IInventoryTransaction } from '../../interfaces';
|
||||
|
||||
@@ -120,6 +120,21 @@ export default class JournalCommands{
|
||||
this.journal.credit(creditEntry);
|
||||
}
|
||||
|
||||
async revertJournalEntries(
|
||||
referenceId: number|number[],
|
||||
referenceType: string
|
||||
) {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.where('reference_type', referenceType)
|
||||
.whereIn('reference_id', Array.isArray(referenceId) ? referenceId : [referenceId])
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
this.journal.loadEntries(transactions);
|
||||
this.journal.removeEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and revert accounts balance journal entries that associated
|
||||
* to the given inventory transactions.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { omit } from 'lodash';
|
||||
import { Container } from 'typedi';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import {
|
||||
IJournalEntry,
|
||||
IJournalPoster,
|
||||
IAccountChange,
|
||||
IAccountsChange,
|
||||
TEntryType,
|
||||
} from '@/interfaces';
|
||||
} from 'interfaces';
|
||||
|
||||
export default class JournalPoster implements IJournalPoster {
|
||||
tenantId: number;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { kebabCase } from 'lodash'
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { IAccountDTO, IAccount } from '@/interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import { IAccountDTO, IAccount } from 'interfaces';
|
||||
import { difference } from 'lodash';
|
||||
import { Account } from 'src/models';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import fs from 'fs';
|
||||
import { Service, Container } from "typedi";
|
||||
import Mustache from 'mustache';
|
||||
import path from 'path';
|
||||
import { ISystemUser } from '@/interfaces';
|
||||
import config from '@/../config/config';
|
||||
import { ISystemUser } from 'interfaces';
|
||||
import config from 'config';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationMailMesssages {
|
||||
@@ -16,7 +16,7 @@ export default class AuthenticationMailMesssages {
|
||||
sendWelcomeMessage(user: ISystemUser, organizationName: string): Promise<void> {
|
||||
const Mail = Container.get('mail');
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
|
||||
const filePath = path.join(global.__root, 'views/mail/Welcome.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, {
|
||||
email: user.email,
|
||||
@@ -50,7 +50,7 @@ export default class AuthenticationMailMesssages {
|
||||
sendResetPasswordMessage(user: ISystemUser, token: string): Promise<void> {
|
||||
const Mail = Container.get('mail');
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/ResetPassword.html');
|
||||
const filePath = path.join(global.__root, 'views/mail/ResetPassword.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, {
|
||||
resetPasswordUrl: `${config.baseURL}/reset/${token}`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import { ISystemUser, ITenant } from "@/interfaces";
|
||||
import { ISystemUser, ITenant } from "interfaces";
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationSMSMessages {
|
||||
|
||||
@@ -6,34 +6,27 @@ import moment from "moment";
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from '@/decorators/eventDispatcher';
|
||||
import {
|
||||
SystemUser,
|
||||
PasswordReset,
|
||||
Tenant,
|
||||
} from '@/system/models';
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { SystemUser, PasswordReset } from 'system/models';
|
||||
import {
|
||||
IRegisterDTO,
|
||||
ITenant,
|
||||
ISystemUser,
|
||||
IPasswordReset,
|
||||
} from '@/interfaces';
|
||||
import TenantsManager from "@/system/TenantsManager";
|
||||
import { hashPassword } from '@/utils';
|
||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||
import config from '@/../config/config';
|
||||
import events from '@/subscribers/events';
|
||||
import AuthenticationMailMessages from '@/services/Authentication/AuthenticationMailMessages';
|
||||
import AuthenticationSMSMessages from '@/services/Authentication/AuthenticationSMSMessages';
|
||||
} from 'interfaces';
|
||||
import { hashPassword } from 'utils';
|
||||
import { ServiceError, ServiceErrors } from 'exceptions';
|
||||
import config from 'config';
|
||||
import events from 'subscribers/events';
|
||||
import AuthenticationMailMessages from 'services/Authentication/AuthenticationMailMessages';
|
||||
import AuthenticationSMSMessages from 'services/Authentication/AuthenticationSMSMessages';
|
||||
import TenantsManager from 'services/Tenancy/TenantsManager';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@@ -43,6 +36,12 @@ export default class AuthenticationService {
|
||||
@Inject()
|
||||
mailMessages: AuthenticationMailMessages;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
/**
|
||||
* Signin and generates JWT token.
|
||||
* @throws {ServiceError}
|
||||
@@ -50,20 +49,16 @@ export default class AuthenticationService {
|
||||
* @param {string} password - Password.
|
||||
* @return {Promise<{user: IUser, token: string}>}
|
||||
*/
|
||||
async signIn(emailOrPhone: string, password: string): Promise<{user: IUser, token: string }> {
|
||||
async signIn(emailOrPhone: string, password: string): Promise<{user: IUser, token: string, tenant: ITenant }> {
|
||||
this.logger.info('[login] Someone trying to login.', { emailOrPhone, password });
|
||||
|
||||
const user = await SystemUser.query()
|
||||
.where('email', emailOrPhone)
|
||||
.orWhere('phone_number', emailOrPhone)
|
||||
.withGraphFetched('tenant')
|
||||
.first();
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const user = await systemUserRepository.findByCrediential(emailOrPhone);
|
||||
|
||||
if (!user) {
|
||||
this.logger.info('[login] invalid data');
|
||||
throw new ServiceError('invalid_details');
|
||||
}
|
||||
|
||||
this.logger.info('[login] check password validation.');
|
||||
if (!user.verifyPassword(password)) {
|
||||
throw new ServiceError('invalid_password');
|
||||
@@ -78,9 +73,7 @@ export default class AuthenticationService {
|
||||
const token = this.generateToken(user);
|
||||
|
||||
this.logger.info('[login] updating user last login at.');
|
||||
await SystemUser.query()
|
||||
.where('id', user.id)
|
||||
.patch({ last_login_at: moment().toMySqlDateTime() });
|
||||
await systemUserRepository.patchLastLoginAt(user.id);
|
||||
|
||||
this.logger.info('[login] Logging success.', { user, token });
|
||||
|
||||
@@ -88,11 +81,15 @@ export default class AuthenticationService {
|
||||
this.eventDispatcher.dispatch(events.auth.login, {
|
||||
emailOrPhone, password,
|
||||
});
|
||||
const tenant = await user.$relatedQuery('tenant');
|
||||
|
||||
// Remove password property from user object.
|
||||
Reflect.deleteProperty(user, 'password');
|
||||
|
||||
return { user, token };
|
||||
// Remove id property from tenant object.
|
||||
Reflect.deleteProperty(tenant, 'id');
|
||||
|
||||
return { user, token, tenant };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,18 +98,17 @@ export default class AuthenticationService {
|
||||
* @param {IRegisterDTO} registerDTO - Register data object.
|
||||
*/
|
||||
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
|
||||
const user: ISystemUser = await SystemUser.query()
|
||||
.where('email', registerDTO.email)
|
||||
.orWhere('phone_number', registerDTO.phoneNumber)
|
||||
.first();
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const isEmailExists = await systemUserRepository.getByEmail(registerDTO.email);
|
||||
const isPhoneExists = await systemUserRepository.getByPhoneNumber(registerDTO.phoneNumber);
|
||||
|
||||
const errorReasons: ServiceErrors[] = [];
|
||||
const errorReasons: ServiceError[] = [];
|
||||
|
||||
if (user && user.phoneNumber === registerDTO.phoneNumber) {
|
||||
if (isPhoneExists) {
|
||||
this.logger.info('[register] phone number exists on the storage.');
|
||||
errorReasons.push(new ServiceError('phone_number_exists'));
|
||||
}
|
||||
if (user && user.email === registerDTO.email) {
|
||||
if (isEmailExists) {
|
||||
this.logger.info('[register] email exists on the storage.');
|
||||
errorReasons.push(new ServiceError('email_exists'));
|
||||
}
|
||||
@@ -136,13 +132,13 @@ export default class AuthenticationService {
|
||||
this.logger.info('[register] Trying hashing the password.')
|
||||
const hashedPassword = await hashPassword(registerDTO.password);
|
||||
|
||||
const registeredUser = await SystemUser.query().insert({
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const registeredUser = await systemUserRepository.create({
|
||||
...omit(registerDTO, 'country', 'organizationName'),
|
||||
active: true,
|
||||
password: hashedPassword,
|
||||
tenant_id: tenant.id,
|
||||
});
|
||||
|
||||
// Triggers `onRegister` event.
|
||||
this.eventDispatcher.dispatch(events.auth.register, {
|
||||
registerDTO, user: registeredUser
|
||||
@@ -156,30 +152,7 @@ export default class AuthenticationService {
|
||||
* @return {Promise<ITenant>}
|
||||
*/
|
||||
private async newTenantOrganization(): Promise<ITenant> {
|
||||
const organizationId = uniqid();
|
||||
const tenantOrganization = await Tenant.query().insert({
|
||||
organization_id: organizationId,
|
||||
});
|
||||
return tenantOrganization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tenant database.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
* @return {void}
|
||||
*/
|
||||
async initializeTenant(tenantId: number): Promise<void> {
|
||||
const dbManager = Container.get('dbManager');
|
||||
|
||||
const tenant = await Tenant.query().findById(tenantId);
|
||||
|
||||
this.logger.info('[tenant_init] Tenant DB creating.', { tenant });
|
||||
await dbManager.createDb(`bigcapital_tenant_${tenant.organizationId}`);
|
||||
|
||||
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
|
||||
|
||||
this.logger.info('[tenant_init] Tenant DB migrating to latest version.', { tenant });
|
||||
await tenantDb.migrate.latest();
|
||||
return this.tenantsManager.createTenant();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,12 +161,14 @@ export default class AuthenticationService {
|
||||
* @param {string} email - email address.
|
||||
*/
|
||||
private async validateEmailExistance(email: string) {
|
||||
const foundEmail = await SystemUser.query().findOne('email', email);
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const userByEmail = await systemUserRepository.getByEmail(email);
|
||||
|
||||
if (!foundEmail) {
|
||||
if (!userByEmail) {
|
||||
this.logger.info('[send_reset_password] The given email not found.');
|
||||
throw new ServiceError('email_not_found');
|
||||
}
|
||||
return userByEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +178,7 @@ export default class AuthenticationService {
|
||||
*/
|
||||
async sendResetPassword(email: string): Promise<IPasswordReset> {
|
||||
this.logger.info('[send_reset_password] Trying to send reset password.');
|
||||
await this.validateEmailExistance(email);
|
||||
const user = await this.validateEmailExistance(email);
|
||||
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
this.logger.info('[send_reset_password] trying to delete all tokens by email.');
|
||||
@@ -213,7 +188,6 @@ export default class AuthenticationService {
|
||||
|
||||
this.logger.info('[send_reset_password] insert the generated token.');
|
||||
const passwordReset = await PasswordReset.query().insert({ email, token });
|
||||
const user = await SystemUser.query().findOne('email', email);
|
||||
|
||||
// Triggers `onSendResetPassword` event.
|
||||
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token });
|
||||
@@ -228,7 +202,8 @@ export default class AuthenticationService {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async resetPassword(token: string, password: string): Promise<void> {
|
||||
const tokenModel = await PasswordReset.query().findOne('token', token)
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const tokenModel = await PasswordReset.query().findOne('token', token);
|
||||
|
||||
if (!tokenModel) {
|
||||
this.logger.info('[reset_password] token invalid.');
|
||||
@@ -242,7 +217,7 @@ export default class AuthenticationService {
|
||||
await this.deletePasswordResetToken(tokenModel.email);
|
||||
throw new ServiceError('token_expired');
|
||||
}
|
||||
const user = await SystemUser.query().findOne('email', tokenModel.email)
|
||||
const user = await systemUserRepository.getByEmail(tokenModel.email);
|
||||
|
||||
if (!user) {
|
||||
throw new ServiceError('user_not_found');
|
||||
@@ -250,10 +225,8 @@ export default class AuthenticationService {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
this.logger.info('[reset_password] saving a new hashed password.');
|
||||
await SystemUser.query()
|
||||
.where('email', tokenModel.email)
|
||||
.update({ password: hashedPassword });
|
||||
|
||||
await systemUserRepository.edit(user.id, { password: hashedPassword });
|
||||
|
||||
// Deletes the used token.
|
||||
await this.deletePasswordResetToken(tokenModel.email);
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { difference } from 'lodash';
|
||||
import { ServiceError } from "@/exceptions";
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { difference, upperFirst } from 'lodash';
|
||||
import { ServiceError } from "exceptions";
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import {
|
||||
IContact,
|
||||
IContactNewDTO,
|
||||
IContactEditDTO,
|
||||
} from "@/interfaces";
|
||||
} from "interfaces";
|
||||
import JournalPoster from '../Accounting/JournalPoster';
|
||||
|
||||
type TContactService = 'customer' | 'vendor';
|
||||
|
||||
@@ -128,4 +129,31 @@ export default class ContactsService {
|
||||
|
||||
await Contact.query().whereIn('id', contactsIds).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts journal entries of the given contacts.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} contactsIds
|
||||
* @param {TContactService} contactService
|
||||
*/
|
||||
async revertJEntriesContactsOpeningBalance(
|
||||
tenantId: number,
|
||||
contactsIds: number[],
|
||||
contactService: TContactService
|
||||
) {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const journal = new JournalPoster(tenantId);
|
||||
|
||||
const contactsTransactions = await AccountTransaction.query()
|
||||
.whereIn('reference_id', contactsIds)
|
||||
.where('reference_type', `${upperFirst(contactService)}OpeningBalance`);
|
||||
|
||||
journal.loadEntries(contactsTransactions);
|
||||
journal.removeEntries();
|
||||
|
||||
await Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.deleteEntries(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit, difference } from 'lodash';
|
||||
import JournalPoster from "@/services/Accounting/JournalPoster";
|
||||
import JournalCommands from "@/services/Accounting/JournalCommands";
|
||||
import ContactsService from '@/services/Contacts/ContactsService';
|
||||
import JournalPoster from "services/Accounting/JournalPoster";
|
||||
import JournalCommands from "services/Accounting/JournalCommands";
|
||||
import ContactsService from 'services/Contacts/ContactsService';
|
||||
import {
|
||||
ICustomerNewDTO,
|
||||
ICustomerEditDTO,
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
} from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ICustomer } from 'src/interfaces';
|
||||
|
||||
@Service()
|
||||
@@ -44,7 +44,7 @@ export default class CustomersService {
|
||||
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
|
||||
|
||||
// Writes the customer opening balance journal entries.
|
||||
if (customer.openingBalance) {
|
||||
if (customer.openingBalance) {
|
||||
await this.writeCustomerOpeningBalanceJournal(
|
||||
tenantId,
|
||||
customer.id,
|
||||
@@ -71,8 +71,16 @@ export default class CustomersService {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteCustomer(tenantId: number, customerId: number) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
await this.getCustomerByIdOrThrowError(tenantId, customerId);
|
||||
await this.customerHasNoInvoicesOrThrowError(tenantId, customerId);
|
||||
return this.contactService.deleteContact(tenantId, customerId, 'customer');
|
||||
|
||||
await Contact.query().findById(customerId).delete();
|
||||
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId, [customerId], 'customer',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,6 +115,15 @@ export default class CustomersService {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given customer by id or throw not found.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
*/
|
||||
getCustomerByIdOrThrowError(tenantId: number, customerId: number) {
|
||||
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'customer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given customers or throw error if one of them not found.
|
||||
* @param {numebr} tenantId
|
||||
@@ -129,6 +146,12 @@ export default class CustomersService {
|
||||
await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds);
|
||||
|
||||
await Contact.query().whereIn('id', customersIds).delete();
|
||||
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId,
|
||||
customersIds,
|
||||
'Customer'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { difference } from 'lodash';
|
||||
import JournalPoster from "@/services/Accounting/JournalPoster";
|
||||
import JournalCommands from "@/services/Accounting/JournalCommands";
|
||||
import ContactsService from '@/services/Contacts/ContactsService';
|
||||
import JournalPoster from "services/Accounting/JournalPoster";
|
||||
import JournalCommands from "services/Accounting/JournalCommands";
|
||||
import ContactsService from 'services/Contacts/ContactsService';
|
||||
import {
|
||||
IVendorNewDTO,
|
||||
IVendorEditDTO,
|
||||
IVendor
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
} from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class VendorsService {
|
||||
@@ -39,7 +39,8 @@ export default class VendorsService {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) {
|
||||
const contactDTO = this.vendorToContactDTO(vendorDTO)
|
||||
const contactDTO = this.vendorToContactDTO(vendorDTO);
|
||||
|
||||
const vendor = await this.contactService.newContact(tenantId, contactDTO, 'vendor');
|
||||
|
||||
// Writes the vendor opening balance journal entries.
|
||||
@@ -63,6 +64,15 @@ export default class VendorsService {
|
||||
return this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given vendor details by id or throw not found.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
*/
|
||||
getVendorByIdOrThrowError(tenantId: number, customerId: number) {
|
||||
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'vendor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given vendor from the storage.
|
||||
* @param {number} tenantId
|
||||
@@ -70,8 +80,16 @@ export default class VendorsService {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteVendor(tenantId: number, vendorId: number) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
await this.getVendorByIdOrThrowError(tenantId, vendorId);
|
||||
await this.vendorHasNoBillsOrThrowError(tenantId, vendorId);
|
||||
return this.contactService.deleteContact(tenantId, vendorId, 'vendor');
|
||||
|
||||
await Contact.query().findById(vendorId).delete();
|
||||
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId, [vendorId], 'vendor',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +146,10 @@ export default class VendorsService {
|
||||
await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds);
|
||||
|
||||
await Contact.query().whereIn('id', vendorsIds).delete();
|
||||
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId, vendorsIds, 'vendor',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +161,7 @@ export default class VendorsService {
|
||||
const { vendorRepository } = this.tenancy.repositories(tenantId);
|
||||
const bills = await vendorRepository.getBills(vendorId);
|
||||
|
||||
if (bills) {
|
||||
if (bills.length > 0) {
|
||||
throw new ServiceError('vendor_has_bills')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Resource from '@/models/Resource';
|
||||
import ResourceField from '@/models/ResourceField';
|
||||
import ResourceFieldMetadata from '@/models/ResourceFieldMetadata';
|
||||
import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection';
|
||||
import Resource from 'models/Resource';
|
||||
import ResourceField from 'models/ResourceField';
|
||||
import ResourceFieldMetadata from 'models/ResourceFieldMetadata';
|
||||
import ResourceFieldMetadataCollection from 'collection/ResourceFieldMetadataCollection';
|
||||
|
||||
export default class ResourceCustomFieldRepository {
|
||||
/**
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
DynamicFilterSortBy,
|
||||
DynamicFilterViews,
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
} from 'lib/DynamicFilter';
|
||||
import {
|
||||
mapViewRolesToConditionals,
|
||||
mapFilterRolesToDynamicFilter,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
} from 'lib/ViewRolesBuilder';
|
||||
|
||||
export const DYNAMIC_LISTING_ERRORS = {
|
||||
LOGIC_INVALID: 'VIEW.LOGIC.EXPRESSION.INVALID',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DYNAMIC_LISTING_ERRORS } from '@/services/DynamicListing/DynamicListing';
|
||||
import { DYNAMIC_LISTING_ERRORS } from 'services/DynamicListing/DynamicListing';
|
||||
|
||||
export const dynamicListingErrorsToResponse = (error) => {
|
||||
let _errors;
|
||||
|
||||
432
server/src/services/Expenses/ExpensesService.ts
Normal file
432
server/src/services/Expenses/ExpensesService.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import { difference, sumBy } from 'lodash';
|
||||
import moment from "moment";
|
||||
import { ServiceError } from "exceptions";
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import JournalCommands from 'services/Accounting/JournalCommands';
|
||||
import { IExpense, IAccount, IExpenseDTO, IExpenseCategory, IExpensesService, ISystemUser } from 'interfaces';
|
||||
|
||||
const ERRORS = {
|
||||
EXPENSE_NOT_FOUND: 'expense_not_found',
|
||||
PAYMENT_ACCOUNT_NOT_FOUND: 'payment_account_not_found',
|
||||
SOME_ACCOUNTS_NOT_FOUND: 'some_expenses_not_found',
|
||||
TOTAL_AMOUNT_EQUALS_ZERO: 'total_amount_equals_zero',
|
||||
PAYMENT_ACCOUNT_HAS_INVALID_TYPE: 'payment_account_has_invalid_type',
|
||||
EXPENSES_ACCOUNT_HAS_INVALID_TYPE: 'expenses_account_has_invalid_type',
|
||||
EXPENSE_ACCOUNT_ALREADY_PUBLISED: 'expense_already_published',
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class ExpensesService implements IExpensesService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Retrieve the payment account details or returns not found server error in case the
|
||||
* given account not found on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentAccountId
|
||||
* @returns {Promise<IAccount>}
|
||||
*/
|
||||
async getPaymentAccountOrThrowError(tenantId: number, paymentAccountId: number) {
|
||||
this.logger.info('[expenses] trying to get the given payment account.', { tenantId, paymentAccountId });
|
||||
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const paymentAccount = await accountRepository.getById(paymentAccountId)
|
||||
|
||||
if (!paymentAccount) {
|
||||
this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId });
|
||||
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
return paymentAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve expense accounts or throw error in case one of the given accounts
|
||||
* not found not the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseAccountsIds
|
||||
* @throws {ServiceError}
|
||||
* @returns {Promise<IAccount[]>}
|
||||
*/
|
||||
async getExpensesAccountsOrThrowError(tenantId: number, expenseAccountsIds: number[]) {
|
||||
this.logger.info('[expenses] trying to get expenses accounts.', { tenantId, expenseAccountsIds });
|
||||
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const storedExpenseAccounts = await Account.query().whereIn(
|
||||
'id', expenseAccountsIds,
|
||||
);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map((a: IAccount) => a.id);
|
||||
const notStoredAccountsIds = difference(
|
||||
expenseAccountsIds,
|
||||
storedExpenseAccountsIds
|
||||
);
|
||||
if (notStoredAccountsIds.length > 0) {
|
||||
this.logger.info('[expenses] some of expense accounts not found.', { tenantId, expenseAccountsIds });
|
||||
throw new ServiceError(ERRORS.SOME_ACCOUNTS_NOT_FOUND);
|
||||
}
|
||||
return storedExpenseAccounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates expense categories not equals zero.
|
||||
* @param {IExpenseDTO|ServiceError} expenseDTO
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
validateCategoriesNotEqualZero(expenseDTO: IExpenseDTO) {
|
||||
this.logger.info('[expenses] validate the expenses categoires not equal zero.', { expenseDTO });
|
||||
const totalAmount = sumBy(expenseDTO.categories, 'amount') || 0;
|
||||
|
||||
if (totalAmount <= 0) {
|
||||
this.logger.info('[expenses] the given expense categories equal zero.', { expenseDTO });
|
||||
throw new ServiceError(ERRORS.TOTAL_AMOUNT_EQUALS_ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate expenses accounts type.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} expensesAccountsIds
|
||||
*/
|
||||
async validateExpensesAccountsType(tenantId: number, expensesAccounts: number[]) {
|
||||
this.logger.info('[expenses] trying to validate expenses accounts type.', { tenantId, expensesAccounts });
|
||||
|
||||
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
|
||||
const expensesTypes = await accountTypeRepository.getByRootType('expense');
|
||||
const expensesTypesIds = expensesTypes.map(t => t.id);
|
||||
const invalidExpenseAccounts: number[] = [];
|
||||
|
||||
expensesAccounts.forEach((expenseAccount) => {
|
||||
if (expensesTypesIds.indexOf(expenseAccount.accountTypeId) === -1) {
|
||||
invalidExpenseAccounts.push(expenseAccount.id);
|
||||
}
|
||||
});
|
||||
if (invalidExpenseAccounts.length > 0) {
|
||||
throw new ServiceError(ERRORS.EXPENSES_ACCOUNT_HAS_INVALID_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates payment account type in case has invalid type throws errors.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentAccountId
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
async validatePaymentAccountType(tenantId: number, paymentAccount: number[]) {
|
||||
this.logger.info('[expenses] trying to validate payment account type.', { tenantId, paymentAccount });
|
||||
|
||||
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
|
||||
const validAccountsType = await accountTypeRepository.getByKeys([
|
||||
'current_asset', 'fixed_asset',
|
||||
]);
|
||||
const validAccountsTypeIds = validAccountsType.map(t => t.id);
|
||||
|
||||
if (validAccountsTypeIds.indexOf(paymentAccount.accountTypeId) === -1) {
|
||||
this.logger.info('[expenses] the given payment account has invalid type', { tenantId, paymentAccount });
|
||||
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_HAS_INVALID_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async revertJournalEntries(
|
||||
tenantId: number,
|
||||
expenseId: number|number[],
|
||||
) {
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommands = new JournalCommands(journal);
|
||||
|
||||
if (revertOld) {
|
||||
await journalCommands.revertJournalEntries(expenseId, 'Expense');
|
||||
}
|
||||
return Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.deleteEntries(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes expense journal entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IExpense} expense
|
||||
* @param {IUser} authorizedUser
|
||||
*/
|
||||
async writeJournalEntries(
|
||||
tenantId: number,
|
||||
expense: IExpense,
|
||||
revertOld: boolean,
|
||||
authorizedUser: ISystemUser
|
||||
) {
|
||||
this.logger.info('[expense[ trying to write expense journal entries.', { tenantId, expense });
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommands = new JournalCommands(journal);
|
||||
|
||||
if (revertOld) {
|
||||
await journalCommands.revertJournalEntries(expense.id, 'Expense');
|
||||
}
|
||||
const mixinEntry = {
|
||||
referenceType: 'Expense',
|
||||
referenceId: expense.id,
|
||||
date: expense.paymentDate,
|
||||
userId: authorizedUser.id,
|
||||
draft: !expense.publish,
|
||||
};
|
||||
const paymentJournalEntry = new JournalEntry({
|
||||
credit: expense.totalAmount,
|
||||
account: expense.paymentAccountId,
|
||||
...mixinEntry,
|
||||
});
|
||||
journal.credit(paymentJournalEntry);
|
||||
|
||||
expense.categories.forEach((category: IExpenseCategory) => {
|
||||
const expenseJournalEntry = new JournalEntry({
|
||||
account: category.expenseAccountId,
|
||||
debit: category.amount,
|
||||
note: category.description,
|
||||
...mixinEntry,
|
||||
});
|
||||
journal.debit(expenseJournalEntry);
|
||||
});
|
||||
return Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.saveEntries(),
|
||||
journal.deleteEntries(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given expenses or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @returns {IExpense|ServiceError}
|
||||
*/
|
||||
async getExpenseOrThrowError(tenantId: number, expenseId: number) {
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[expense] trying to get the given expense.', { tenantId, expenseId });
|
||||
const expense = await expenseRepository.getById(expenseId);
|
||||
|
||||
if (!expense) {
|
||||
this.logger.info('[expense] the given expense not found.', { tenantId, expenseId });
|
||||
throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND);
|
||||
}
|
||||
return expense;
|
||||
}
|
||||
|
||||
async getExpensesOrThrowError(tenantId: number, expensesIds: number[]) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates expenses is not already published before.
|
||||
* @param {IExpense} expense
|
||||
*/
|
||||
validateExpenseIsNotPublished(expense: IExpense) {
|
||||
if (expense.published) {
|
||||
throw new ServiceError(ERRORS.EXPENSE_ACCOUNT_ALREADY_PUBLISED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping expense DTO to model.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @return {IExpense}
|
||||
*/
|
||||
expenseDTOToModel(expenseDTO: IExpenseDTO) {
|
||||
const totalAmount = sumBy(expenseDTO.categories, 'amount');
|
||||
|
||||
return {
|
||||
published: false,
|
||||
categories: [],
|
||||
...expenseDTO,
|
||||
totalAmount,
|
||||
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the expenses accounts ids from expense DTO.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @return {number[]}
|
||||
*/
|
||||
mapExpensesAccountsIdsFromDTO(expenseDTO: IExpenseDTO) {
|
||||
return expenseDTO.categories.map((category) => category.expenseAccountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Precedures.
|
||||
* ---------
|
||||
* 1. Validate expense existance.
|
||||
* 2. Validate payment account existance on the storage.
|
||||
* 3. Validate expense accounts exist on the storage.
|
||||
* 4. Validate payment account type.
|
||||
* 5. Validate expenses accounts type.
|
||||
* 6. Validate the given expense categories not equal zero.
|
||||
* 7. Stores the expense to the storage.
|
||||
* ---------
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
*/
|
||||
async editExpense(
|
||||
tenantId: number,
|
||||
expenseId: number,
|
||||
expenseDTO: IExpenseDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<IExpense> {
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
||||
|
||||
// 1. Validate payment account existance on the storage.
|
||||
const paymentAccount = await this.getPaymentAccountOrThrowError(
|
||||
tenantId,
|
||||
expenseDTO.paymentAccountId,
|
||||
);
|
||||
// 2. Validate expense accounts exist on the storage.
|
||||
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
|
||||
tenantId,
|
||||
this.mapExpensesAccountsIdsFromDTO(expenseDTO),
|
||||
);
|
||||
// 3. Validate payment account type.
|
||||
await this.validatePaymentAccountType(tenantId, paymentAccount);
|
||||
|
||||
// 4. Validate expenses accounts type.
|
||||
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
|
||||
|
||||
// 5. Validate the given expense categories not equal zero.
|
||||
this.validateCategoriesNotEqualZero(expenseDTO);
|
||||
|
||||
// 6. Update the expense on the storage.
|
||||
const expenseObj = this.expenseDTOToModel(expenseDTO);
|
||||
const expenseModel = await expenseRepository.update(expenseId, expenseObj, null);
|
||||
|
||||
// 7. In case expense published, write journal entries.
|
||||
if (expenseObj.published) {
|
||||
await this.writeJournalEntries(tenantId, expenseModel, true, authorizedUser);
|
||||
}
|
||||
this.logger.info('[expense] the expense updated on the storage successfully.', { tenantId, expenseDTO });
|
||||
return expenseModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Precedures.
|
||||
* ---------
|
||||
* 1. Validate payment account existance on the storage.
|
||||
* 2. Validate expense accounts exist on the storage.
|
||||
* 3. Validate payment account type.
|
||||
* 4. Validate expenses accounts type.
|
||||
* 5. Validate the given expense categories not equal zero.
|
||||
* 6. Stores the expense to the storage.
|
||||
* ---------
|
||||
* @param {number} tenantId
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
*/
|
||||
async newExpense(tenantId: number, expenseDTO: IExpenseDTO, authorizedUser: ISystemUser): Promise<IExpense> {
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// 1. Validate payment account existance on the storage.
|
||||
const paymentAccount = await this.getPaymentAccountOrThrowError(
|
||||
tenantId,
|
||||
expenseDTO.paymentAccountId,
|
||||
);
|
||||
// 2. Validate expense accounts exist on the storage.
|
||||
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
|
||||
tenantId,
|
||||
this.mapExpensesAccountsIdsFromDTO(expenseDTO),
|
||||
);
|
||||
// 3. Validate payment account type.
|
||||
await this.validatePaymentAccountType(tenantId, paymentAccount);
|
||||
|
||||
// 4. Validate expenses accounts type.
|
||||
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
|
||||
|
||||
// 5. Validate the given expense categories not equal zero.
|
||||
this.validateCategoriesNotEqualZero(expenseDTO);
|
||||
|
||||
// 6. Save the expense to the storage.
|
||||
const expenseObj = this.expenseDTOToModel(expenseDTO);
|
||||
const expenseModel = await expenseRepository.create(expenseObj);
|
||||
|
||||
// 7. In case expense published, write journal entries.
|
||||
if (expenseObj.published) {
|
||||
await this.writeJournalEntries(tenantId, expenseModel, false, authorizedUser);
|
||||
}
|
||||
this.logger.info('[expense] the expense stored to the storage successfully.', { tenantId, expenseDTO });
|
||||
|
||||
return expenseModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the given expense.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async publishExpense(tenantId: number, expenseId: number) {
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
||||
|
||||
if (expense instanceof ServiceError) {
|
||||
throw expense;
|
||||
}
|
||||
this.validateExpenseIsNotPublished(expense);
|
||||
|
||||
this.logger.info('[expense] trying to publish the expense.', { tenantId, expenseId });
|
||||
await expenseRepository.publish(expenseId);
|
||||
|
||||
this.logger.info('[expense] the expense published successfully.', { tenantId, expenseId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given expense.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
async deleteExpense(tenantId: number, expenseId: number) {
|
||||
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId });
|
||||
await expenseRepository.delete(expenseId);
|
||||
|
||||
if (expense.published) {
|
||||
await this.revertJournalEntries(tenantId, expenseId);
|
||||
}
|
||||
this.logger.info('[expense] the expense deleted successfully.', { tenantId, expenseId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given expenses in bulk.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} expensesIds
|
||||
*/
|
||||
async deleteBulkExpenses(tenantId: number, expensesIds: number[]) {
|
||||
const expenses = await this.getExpensesOrThrowError(tenantId, expensesIds);
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[expense] trying to delete the given expenses.', { tenantId, expensesIds });
|
||||
await expenseRepository.bulkDelete(expensesIds);
|
||||
await this.revertJournalEntries(tenantId, expensesIds);
|
||||
|
||||
this.logger.info('[expense] the given expenses deleted successfully.', { tenantId, expensesIds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given expenses in bulk.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} expensesIds
|
||||
*/
|
||||
async publishBulkExpenses(tenantId: number, expensesIds: number[]) {
|
||||
const expenses = await this.getExpensesOrThrowError(tenantId, expensesIds);
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds });
|
||||
await expenseRepository.publishBulk(expensesIds);
|
||||
|
||||
this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import InventoryAverageCost from '@/services/Inventory/InventoryAverageCost';
|
||||
import InventoryCostLotTracker from '@/services/Inventory/InventoryCostLotTracker';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InventoryAverageCost from 'services/Inventory/InventoryAverageCost';
|
||||
import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { pick } from 'lodash';
|
||||
import { IInventoryTransaction } from '@/interfaces';
|
||||
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
|
||||
import { IInventoryTransaction } from 'interfaces';
|
||||
import InventoryCostMethod from 'services/Inventory/InventoryCostMethod';
|
||||
|
||||
export default class InventoryAverageCostMethod extends InventoryCostMethod implements IInventoryCostMethod {
|
||||
startingDate: Date;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { pick, chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { IInventoryLotCost, IInventoryTransaction } from "@/interfaces";
|
||||
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
|
||||
import { IInventoryLotCost, IInventoryTransaction } from "interfaces";
|
||||
import InventoryCostMethod from 'services/Inventory/InventoryCostMethod';
|
||||
|
||||
type TCostMethod = 'FIFO' | 'LIFO';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { omit } from 'lodash';
|
||||
import { Inject } from 'typedi';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { IInventoryLotCost } from '@/interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { IInventoryLotCost } from 'interfaces';
|
||||
|
||||
export default class InventoryCostMethod {
|
||||
@Inject()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Service } from "typedi";
|
||||
export default class InviteUsersMailMessages {
|
||||
|
||||
sendInviteMail() {
|
||||
const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html');
|
||||
const filePath = path.join(global.__root, 'views/mail/UserInvite.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const rendered = Mustache.render(template, {
|
||||
|
||||
@@ -4,19 +4,18 @@ import moment from 'moment';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from '@/decorators/eventDispatcher';
|
||||
import { ServiceError } from "@/exceptions";
|
||||
import { SystemUser, Invite, Tenant } from "@/system/models";
|
||||
import { Option } from '@/models';
|
||||
import { hashPassword } from '@/utils';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import TenantsManager from "@/system/TenantsManager";
|
||||
import InviteUsersMailMessages from "@/services/InviteUsers/InviteUsersMailMessages";
|
||||
import events from '@/subscribers/events';
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { ServiceError } from "exceptions";
|
||||
import { Invite, Tenant } from "system/models";
|
||||
import { Option } from 'models';
|
||||
import { hashPassword } from 'utils';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import InviteUsersMailMessages from "services/InviteUsers/InviteUsersMailMessages";
|
||||
import events from 'subscribers/events';
|
||||
import {
|
||||
ISystemUser,
|
||||
IInviteUserInput,
|
||||
} from '@/interfaces';
|
||||
} from 'interfaces';
|
||||
|
||||
@Service()
|
||||
export default class InviteUserService {
|
||||
@@ -26,15 +25,15 @@ export default class InviteUserService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
mailMessages: InviteUsersMailMessages;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
/**
|
||||
* Accept the received invite.
|
||||
* @param {string} token
|
||||
@@ -50,24 +49,26 @@ export default class InviteUserService {
|
||||
const hashedPassword = await hashPassword(inviteUserInput.password);
|
||||
|
||||
this.logger.info('[accept_invite] trying to update user details.');
|
||||
const updateUserOper = SystemUser.query()
|
||||
.where('email', inviteToken.email)
|
||||
.patch({
|
||||
...inviteUserInput,
|
||||
active: 1,
|
||||
invite_accepted_at: moment().format('YYYY-MM-DD'),
|
||||
password: hashedPassword,
|
||||
});
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
|
||||
const user = await systemUserRepository.getByEmail(inviteToken.email);
|
||||
|
||||
const updateUserOper = systemUserRepository.edit(user.id, {
|
||||
...inviteUserInput,
|
||||
active: 1,
|
||||
invite_accepted_at: moment().format('YYYY-MM-DD'),
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
this.logger.info('[accept_invite] trying to delete the given token.');
|
||||
const deleteInviteTokenOper = Invite.query().where('token', inviteToken.token).delete();
|
||||
|
||||
// Await all async operations.
|
||||
const [user] = await Promise.all([updateUserOper, deleteInviteTokenOper]);
|
||||
const [updatedUser] = await Promise.all([updateUserOper, deleteInviteTokenOper]);
|
||||
|
||||
// Triggers `onUserAcceptInvite` event.
|
||||
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
|
||||
inviteToken, user,
|
||||
inviteToken, user: updatedUser,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,7 +80,7 @@ export default class InviteUserService {
|
||||
*
|
||||
* @return {Promise<IInvite>}
|
||||
*/
|
||||
public async sendInvite(tenantId: number, email: string, authorizedUser: ISystemUser): Promise<IInvite> {
|
||||
public async sendInvite(tenantId: number, email: string, authorizedUser: ISystemUser): Promise<{ invite: IInvite, user: ISystemUser }> {
|
||||
await this.throwErrorIfUserEmailExists(email);
|
||||
|
||||
this.logger.info('[send_invite] trying to store invite token.');
|
||||
@@ -90,11 +91,12 @@ export default class InviteUserService {
|
||||
});
|
||||
|
||||
this.logger.info('[send_invite] trying to store user with email and tenant.');
|
||||
const user = await SystemUser.query().insert({
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const user = await systemUserRepository.create({
|
||||
email,
|
||||
tenant_id: authorizedUser.tenantId,
|
||||
active: 1,
|
||||
})
|
||||
});
|
||||
|
||||
// Triggers `onUserSendInvite` event.
|
||||
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
|
||||
@@ -130,12 +132,14 @@ export default class InviteUserService {
|
||||
* Throws error in case the given user email not exists on the storage.
|
||||
* @param {string} email
|
||||
*/
|
||||
private async throwErrorIfUserEmailExists(email: string) {
|
||||
const foundUser = await SystemUser.query().findOne('email', email);
|
||||
private async throwErrorIfUserEmailExists(email: string): Promise<ISystemUser> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const foundUser = await systemUserRepository.getByEmail(email);
|
||||
|
||||
if (foundUser) {
|
||||
throw new ServiceError('email_already_invited');
|
||||
}
|
||||
return foundUser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,12 +162,9 @@ export default class InviteUserService {
|
||||
* Validate the given user email and phone number uniquine.
|
||||
* @param {IInviteUserInput} inviteUserInput
|
||||
*/
|
||||
private async validateUserPhoneNumber(inviteUserInput: IInviteUserInput) {
|
||||
const foundUser = await SystemUser.query()
|
||||
.onBuild(query => {
|
||||
query.where('phone_number', inviteUserInput.phoneNumber);
|
||||
query.first();
|
||||
});
|
||||
private async validateUserPhoneNumber(inviteUserInput: IInviteUserInput): Promise<ISystemUser> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const foundUser = await systemUserRepository.getByPhoneNumber(inviteUserInput.phoneNumber)
|
||||
|
||||
if (foundUser) {
|
||||
throw new ServiceError('phone_number_exists');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { difference } from "lodash";
|
||||
import { Service, Inject } from "typedi";
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class ItemsService {
|
||||
|
||||
@@ -1,54 +1,94 @@
|
||||
import { Service, Inject, Container } from 'typedi';
|
||||
import { Tenant, SystemUser } from '@/system/models';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ITenant } from '@/interfaces';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import { ITenant } from 'interfaces';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from '@/decorators/eventDispatcher';
|
||||
import events from '@/subscribers/events';
|
||||
} from 'decorators/eventDispatcher';
|
||||
import events from 'subscribers/events';
|
||||
import {
|
||||
TenantAlreadyInitialized,
|
||||
TenantAlreadySeeded,
|
||||
TenantDatabaseNotBuilt
|
||||
} from 'exceptions';
|
||||
import TenantsManager from 'services/Tenancy/TenantsManager';
|
||||
|
||||
const ERRORS = {
|
||||
TENANT_NOT_FOUND: 'tenant_not_found',
|
||||
TENANT_ALREADY_INITIALIZED: 'tenant_already_initialized',
|
||||
TENANT_ALREADY_SEEDED: 'tenant_already_seeded',
|
||||
TENANT_DB_NOT_BUILT: 'tenant_db_not_built',
|
||||
};
|
||||
@Service()
|
||||
export default class OrganizationService {
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
@Inject('dbManager')
|
||||
dbManager: any;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
/**
|
||||
* Builds the database schema and seed data of the given organization id.
|
||||
* @param {srting} organizationId
|
||||
* @param {srting} organizationId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async build(organizationId: string): Promise<void> {
|
||||
const tenant = await Tenant.query().findOne('organization_id', organizationId);
|
||||
this.throwIfTenantNotExists(tenant);
|
||||
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
|
||||
this.throwIfTenantInitizalized(tenant);
|
||||
|
||||
this.logger.info('[tenant_db_build] tenant DB creating.', { tenant });
|
||||
await this.dbManager.createDb(`bigcapital_tenant_${tenant.organizationId}`);
|
||||
|
||||
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
|
||||
const tenantHasDB = await this.tenantsManager.hasDatabase(tenant);
|
||||
|
||||
this.logger.info('[tenant_db_build] tenant DB migrating to latest version.', { tenant });
|
||||
await tenantDb.migrate.latest();
|
||||
try {
|
||||
if (!tenantHasDB) {
|
||||
this.logger.info('[organization] trying to create tenant database.', { organizationId });
|
||||
await this.tenantsManager.createDatabase(tenant);
|
||||
}
|
||||
this.logger.info('[organization] trying to migrate tenant database.', { organizationId });
|
||||
await this.tenantsManager.migrateTenant(tenant);
|
||||
|
||||
this.logger.info('[tenant_db_build] mark tenant as initialized.', { tenant });
|
||||
await tenant.$query().update({ initialized: true });
|
||||
// Throws `onOrganizationBuild` event.
|
||||
this.eventDispatcher.dispatch(events.organization.build, { tenant });
|
||||
|
||||
// Retrieve the tenant system user.
|
||||
const user = await SystemUser.query().findOne('tenant_id', tenant.id);
|
||||
} catch (error) {
|
||||
if (error instanceof TenantAlreadyInitialized) {
|
||||
throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throws `onOrganizationBuild` event.
|
||||
this.eventDispatcher.dispatch(events.organization.build, { tenant, user });
|
||||
/**
|
||||
* Seeds initial core data to the given organization tenant.
|
||||
* @param {number} organizationId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async seed(organizationId: string): Promise<void> {
|
||||
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
|
||||
this.throwIfTenantSeeded(tenant);
|
||||
|
||||
try {
|
||||
this.logger.info('[organization] trying to seed tenant database.', { organizationId });
|
||||
await this.tenantsManager.seedTenant(tenant);
|
||||
|
||||
// Throws `onOrganizationBuild` event.
|
||||
this.eventDispatcher.dispatch(events.organization.seeded, { tenant });
|
||||
|
||||
} catch (error) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +98,7 @@ export default class OrganizationService {
|
||||
private throwIfTenantNotExists(tenant: ITenant) {
|
||||
if (!tenant) {
|
||||
this.logger.info('[tenant_db_build] organization id not found.');
|
||||
throw new ServiceError('tenant_not_found');
|
||||
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +107,32 @@ export default class OrganizationService {
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
private throwIfTenantInitizalized(tenant: ITenant) {
|
||||
if (tenant.initialized) {
|
||||
throw new ServiceError('tenant_initialized');
|
||||
if (tenant.initializedAt) {
|
||||
throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws service if the tenant already seeded.
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
private throwIfTenantSeeded(tenant: ITenant) {
|
||||
if (tenant.seededAt) {
|
||||
throw new ServiceError(ERRORS.TENANT_ALREADY_SEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tenant model by the given organization id or throw not found
|
||||
* error if the tenant not exists on the storage.
|
||||
* @param {string} organizationId
|
||||
* @return {ITenant}
|
||||
*/
|
||||
private async getTenantByOrgIdOrThrowError(organizationId: string) {
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
const tenant = await tenantRepository.getByOrgId(organizationId);
|
||||
this.throwIfTenantNotExists(tenant);
|
||||
|
||||
return tenant;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Service, Container, Inject } from 'typedi';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
import { times } from 'lodash';
|
||||
import { License } from "@/system/models";
|
||||
import { ILicense } from '@/interfaces';
|
||||
import LicenseMailMessages from '@/services/Payment/LicenseMailMessages';
|
||||
import LicenseSMSMessages from '@/services/Payment/LicenseSMSMessages';
|
||||
import { License } from "system/models";
|
||||
import { ILicense } from 'interfaces';
|
||||
import LicenseMailMessages from 'services/Payment/LicenseMailMessages';
|
||||
import LicenseSMSMessages from 'services/Payment/LicenseSMSMessages';
|
||||
|
||||
@Service()
|
||||
export default class LicenseService {
|
||||
@@ -27,8 +27,6 @@ export default class LicenseService {
|
||||
let licenseCode: string;
|
||||
let repeat: boolean = true;
|
||||
|
||||
console.log(License);
|
||||
|
||||
while(repeat) {
|
||||
licenseCode = cryptoRandomString({ length: 10, type: 'numeric' });
|
||||
const foundLicenses = await License.query().where('license_code', licenseCode);
|
||||
|
||||
@@ -13,7 +13,7 @@ export default class SubscriptionMailMessages {
|
||||
const Logger = Container.get('logger');
|
||||
const Mail = Container.get('mail');
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/LicenseReceive.html');
|
||||
const filePath = path.join(global.__root, 'views/mail/LicenseReceive.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, { licenseCode });
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { License } from "@/system/models";
|
||||
import PaymentMethod from '@/services/Payment/PaymentMethod';
|
||||
import { Plan } from '@/system/models';
|
||||
import { IPaymentMethod, ILicensePaymentModel } from '@/interfaces';
|
||||
import { ILicensePaymentModel } from "@/interfaces";
|
||||
import { PaymentInputInvalid, PaymentAmountInvalidWithPlan } from '@/exceptions';
|
||||
import { License } from "system/models";
|
||||
import PaymentMethod from 'services/Payment/PaymentMethod';
|
||||
import { Plan } from 'system/models';
|
||||
import { IPaymentMethod, ILicensePaymentModel } from 'interfaces';
|
||||
import { ILicensePaymentModel } from "interfaces";
|
||||
import { PaymentInputInvalid, PaymentAmountInvalidWithPlan } from 'exceptions';
|
||||
|
||||
export default class LicensePaymentMethod extends PaymentMethod implements IPaymentMethod {
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import SMSClient from '@/services/SMSClient';
|
||||
import SMSClient from 'services/SMSClient';
|
||||
|
||||
export default class SubscriptionSMSMessages {
|
||||
@Inject('SMSClient')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import moment from 'moment';
|
||||
import { IPaymentModel } from '@/interfaces';
|
||||
import { IPaymentModel } from 'interfaces';
|
||||
|
||||
export default class PaymentMethod implements IPaymentModel {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IPaymentMethod, IPaymentContext } from "@/interfaces";
|
||||
import { Plan } from '@/system/models';
|
||||
import { IPaymentMethod, IPaymentContext } from "interfaces";
|
||||
import { Plan } from 'system/models';
|
||||
|
||||
export default class PaymentContext<PaymentModel> implements IPaymentContext{
|
||||
paymentMethod: IPaymentMethod;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { IBillPaymentOTD, IBillPayment } from '@/interfaces';
|
||||
import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries';
|
||||
import AccountsService from '@/services/Accounts/AccountsService';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import { IBillPaymentOTD, IBillPayment } from 'interfaces';
|
||||
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
|
||||
/**
|
||||
* Bill payments service.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { omit, sumBy, pick } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import AccountsService from '@/services/Accounts/AccountsService';
|
||||
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
||||
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import{ IBillOTD, IBill, IItem } from '@/interfaces';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
import{ IBillOTD, IBill, IItem } from 'interfaces';
|
||||
|
||||
/**
|
||||
* Vendor bills services.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import SMSClientInterface from '@/services/SMSClient/SMSClientInterfaces';
|
||||
import config from '@/../config/config';
|
||||
import SMSClientInterface from 'services/SMSClient/SMSClientInterfaces';
|
||||
import config from 'config';
|
||||
|
||||
export default class EasySMSClient implements SMSClientInterface {
|
||||
clientName: string = 'easysms';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SMSClientInterface from '@/services/SMSClient/SMSClientInterface';
|
||||
import SMSClientInterface from 'services/SMSClient/SMSClientInterface';
|
||||
|
||||
export default class SMSAPI {
|
||||
smsClient: SMSClientInterface;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { difference, omit } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ItemEntry } from '@/models';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ItemEntry } from 'models';
|
||||
|
||||
@Service()
|
||||
export default class HasItemEntries {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class JournalPosterService {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { omit, sumBy, chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { IPaymentReceiveOTD } from '@/interfaces';
|
||||
import AccountsService from '@/services/Accounts/AccountsService';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
||||
import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries';
|
||||
import PaymentReceiveEntryRepository from '@/repositories/PaymentReceiveEntryRepository';
|
||||
import CustomerRepository from '@/repositories/CustomerRepository';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import { IPaymentReceiveOTD } from 'interfaces';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
|
||||
import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository';
|
||||
import CustomerRepository from 'repositories/CustomerRepository';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
|
||||
/**
|
||||
* Payment receive service.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { omit, difference, sumBy, mixin } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import { formatDateFields } from 'utils';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
/**
|
||||
* Sale estimate service.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { omit, sumBy, difference, pick, chain } from 'lodash';
|
||||
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from '@/interfaces';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from 'interfaces';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
|
||||
/**
|
||||
* Sales invoices service
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ISaleInvoice, IItemEntry } from '@/interfaces';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ISaleInvoice, IItemEntry } from 'interfaces';
|
||||
|
||||
@Service()
|
||||
export default class SaleInvoicesCost {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { omit, difference, sumBy } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
||||
import HasItemEntries from '@/services/Sales/HasItemsEntries';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import HasItemEntries from 'services/Sales/HasItemsEntries';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
|
||||
@Service()
|
||||
export default class SalesReceiptService {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SessionModel from '@/services/SessionModel';
|
||||
import SessionModel from 'services/SessionModel';
|
||||
|
||||
export default class SessionQueryBuilder extends SessionModel.QueryBuilder {
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SessionQueryBuilder from '@/services/SessionModel/SessionQueryBuilder';
|
||||
import SessionQueryBuilder from 'services/SessionModel/SessionQueryBuilder';
|
||||
|
||||
export default class SessionModel {
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Knex from 'knex';
|
||||
import MetableStoreDB from '@/lib/Metable/MetableStoreDB';
|
||||
import Setting from '@/models/Setting';
|
||||
import MetableStoreDB from 'lib/Metable/MetableStoreDB';
|
||||
import Setting from 'models/Setting';
|
||||
|
||||
export default class SettingsStore extends MetableStoreDB {
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import SMSClient from '@/services/SMSClient';
|
||||
import SMSClient from 'services/SMSClient';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionSMSMessages {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Tenant, Plan } from '@/system/models';
|
||||
import { IPaymentContext } from '@/interfaces';
|
||||
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
|
||||
import { NoPaymentModelWithPricedPlan } from '@/exceptions';
|
||||
import { Tenant, Plan } from 'system/models';
|
||||
import { IPaymentContext } from 'interfaces';
|
||||
import { NotAllowedChangeSubscriptionPlan } from 'exceptions';
|
||||
import { NoPaymentModelWithPricedPlan } from 'exceptions';
|
||||
|
||||
export default class Subscription<PaymentModel> {
|
||||
paymentContext: IPaymentContext|null;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Plan, Tenant } from '@/system/models';
|
||||
import Subscription from '@/services/Subscription/Subscription';
|
||||
import LicensePaymentMethod from '@/services/Payment/LicensePaymentMethod';
|
||||
import PaymentContext from '@/services/Payment';
|
||||
import SubscriptionSMSMessages from '@/services/Subscription/SMSMessages';
|
||||
import SubscriptionMailMessages from '@/services/Subscription/MailMessages';
|
||||
import { ILicensePaymentModel } from '@/interfaces';
|
||||
import { Plan, Tenant } from 'system/models';
|
||||
import Subscription from 'services/Subscription/Subscription';
|
||||
import LicensePaymentMethod from 'services/Payment/LicensePaymentMethod';
|
||||
import PaymentContext from 'services/Payment';
|
||||
import SubscriptionSMSMessages from 'services/Subscription/SMSMessages';
|
||||
import SubscriptionMailMessages from 'services/Subscription/MailMessages';
|
||||
import { ILicensePaymentModel } from 'interfaces';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionService {
|
||||
@@ -18,6 +18,9 @@ export default class SubscriptionService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
/**
|
||||
* Handles the payment process via license code and than subscribe to
|
||||
* the given tenant.
|
||||
@@ -35,8 +38,10 @@ export default class SubscriptionService {
|
||||
this.logger.info('[subscription_via_license] try to subscribe via given license.', {
|
||||
tenantId, paymentModel
|
||||
});
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
|
||||
const plan = await Plan.query().findOne('slug', planSlug);
|
||||
const tenant = await Tenant.query().findById(tenantId);
|
||||
const tenant = await tenantRepository.getById(tenantId);
|
||||
|
||||
const paymentViaLicense = new LicensePaymentMethod();
|
||||
const paymentContext = new PaymentContext(paymentViaLicense);
|
||||
|
||||
26
server/src/services/Tenancy/SystemService.ts
Normal file
26
server/src/services/Tenancy/SystemService.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Container from "typedi"
|
||||
import { Service, Inject } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export default class HasSystemService implements SystemService{
|
||||
|
||||
private container(key: string) {
|
||||
return Container.get(key);
|
||||
}
|
||||
|
||||
knex() {
|
||||
return this.container('knex');
|
||||
}
|
||||
|
||||
repositories() {
|
||||
return this.container('repositories');
|
||||
}
|
||||
|
||||
cache() {
|
||||
return this.container('cache');
|
||||
}
|
||||
|
||||
dbManager() {
|
||||
return this.container('dbManager');
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,55 @@
|
||||
import { Container, Service } from 'typedi';
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import TenantsManagerService from 'services/Tenancy/TenantsManager';
|
||||
import tenantModelsLoader from 'loaders/tenantModels';
|
||||
import tenantRepositoriesLoader from 'loaders/tenantRepositories';
|
||||
import tenantCacheLoader from 'loaders/tenantCache';
|
||||
|
||||
@Service()
|
||||
export default class HasTenancyService {
|
||||
@Inject()
|
||||
tenantsManager: TenantsManagerService;
|
||||
|
||||
/**
|
||||
* Retrieve the given tenant container.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId
|
||||
* @return {Container}
|
||||
*/
|
||||
tenantContainer(tenantId: number) {
|
||||
return Container.of(`tenant-${tenantId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton tenant service.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} key - Service key.
|
||||
* @param {Function} callback
|
||||
*/
|
||||
singletonService(tenantId: number, key: string, callback: Function) {
|
||||
const container = this.tenantContainer(tenantId);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
const hasServiceInstnace = container.has(key);
|
||||
|
||||
if (!hasServiceInstnace) {
|
||||
const serviceInstance = callback();
|
||||
|
||||
container.set(key, serviceInstance);
|
||||
Logger.info(`[tenant_DI] ${key} injected to tenant container.`, { tenantId, key });
|
||||
|
||||
return serviceInstance;
|
||||
} else {
|
||||
return container.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve knex instance of the given tenant id.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
knex(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('knex');
|
||||
return this.singletonService(tenantId, 'tenantManager', () => {
|
||||
return this.tenantsManager.getKnexInstance(tenantId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,30 +57,39 @@ export default class HasTenancyService {
|
||||
* @param {number} tenantId - The tenant id.
|
||||
*/
|
||||
models(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('models');
|
||||
const knexInstance = this.knex(tenantId);
|
||||
|
||||
return this.singletonService(tenantId, 'models', () => {
|
||||
return tenantModelsLoader(knexInstance);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve repositories of the given tenant id.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId - Tenant id.
|
||||
*/
|
||||
repositories(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('repositories');
|
||||
return this.singletonService(tenantId, 'repositories', () => {
|
||||
return tenantRepositoriesLoader(tenantId);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve i18n locales methods.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId - Tenant id.
|
||||
*/
|
||||
i18n(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('i18n');
|
||||
return this.singletonService(tenantId, 'i18n', () => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tenant cache instance.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} tenantId - Tenant id.
|
||||
*/
|
||||
cache(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('cache');
|
||||
return this.singletonService(tenantId, 'cache', () => {
|
||||
return tenantCacheLoader(tenantId);
|
||||
});
|
||||
}
|
||||
}
|
||||
123
server/src/services/Tenancy/TenantDBManager.ts
Normal file
123
server/src/services/Tenancy/TenantDBManager.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Container } from 'typedi';
|
||||
import Knex from 'knex';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import config from 'config';
|
||||
import { ITenant, ITenantDBManager, ISystemService } from 'interfaces';
|
||||
import SystemService from 'services/Tenancy/SystemService';
|
||||
import { TenantDBAlreadyExists } from 'exceptions';
|
||||
import { tenantKnexConfig, tenantSeedConfig } from 'config/knexConfig';
|
||||
|
||||
export default class TenantDBManager implements ITenantDBManager{
|
||||
static knexCache: { [key: string]: Knex; } = {};
|
||||
|
||||
// System database manager.
|
||||
dbManager: any;
|
||||
|
||||
// System knex instance.
|
||||
sysKnex: Knex;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
constructor() {
|
||||
const systemService = Container.get(SystemService);
|
||||
|
||||
this.dbManager = systemService.dbManager();
|
||||
this.sysKnex = systemService.knex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the tenant database name.
|
||||
* @return {string}
|
||||
*/
|
||||
private getDatabaseName(tenant: ITenant) {
|
||||
return `${config.tenant.db_name_prefix}${tenant.organizationId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the tenant database weather exists.
|
||||
* @return {Promise<boolean>}
|
||||
*/
|
||||
public async databaseExists(tenant: ITenant) {
|
||||
const databaseName = this.getDatabaseName(tenant);
|
||||
const results = await this.sysKnex
|
||||
.raw('SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "?"', databaseName);
|
||||
|
||||
return results[0].length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tenant database.
|
||||
* @throws {TenantAlreadyInitialized}
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async createDatabase(tenant: ITenant): Promise<void> {
|
||||
await this.throwErrorIfTenantDBExists(tenant);
|
||||
|
||||
const databaseName = this.getDatabaseName(tenant);
|
||||
await this.dbManager.createDb(databaseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate tenant database schema to the latest version.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async migrate(tenant: ITenant): Promise<void> {
|
||||
const knex = this.setupKnexInstance(tenant);
|
||||
|
||||
await knex.migrate.latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds initial data to the tenant database.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async seed(tenant: ITenant): Promise<void> {
|
||||
const knex = this.setupKnexInstance(tenant);
|
||||
|
||||
await knex.migrate.latest({
|
||||
...tenantSeedConfig(tenant),
|
||||
disableMigrationsListValidation: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the knex instance of tenant.
|
||||
* @return {Knex}
|
||||
*/
|
||||
public setupKnexInstance(tenant: ITenant) {
|
||||
const key: string = `${tenant.id}`;
|
||||
let knexInstance = TenantDBManager.knexCache[key];
|
||||
|
||||
if (!knexInstance) {
|
||||
knexInstance = Knex({
|
||||
...tenantKnexConfig(tenant),
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
});
|
||||
TenantDBManager.knexCache[key] = knexInstance;
|
||||
}
|
||||
return knexInstance;
|
||||
}
|
||||
|
||||
public getKnexInstance(tenantId: number) {
|
||||
const key: string = `${tenantId}`;
|
||||
let knexInstance = TenantDBManager.knexCache[key];
|
||||
|
||||
if (!knexInstance) {
|
||||
throw new Error('Knex instance is not initialized yut.');
|
||||
}
|
||||
return knexInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant database already exists.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async throwErrorIfTenantDBExists(tenant: ITenant) {
|
||||
const isExists = await this.databaseExists(tenant);
|
||||
if (isExists) {
|
||||
throw new TenantDBAlreadyExists();
|
||||
}
|
||||
}
|
||||
}
|
||||
0
server/src/services/Tenancy/TenantService.ts
Normal file
0
server/src/services/Tenancy/TenantService.ts
Normal file
158
server/src/services/Tenancy/TenantsManager.ts
Normal file
158
server/src/services/Tenancy/TenantsManager.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Container, Inject, Service } from 'typedi';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import {
|
||||
ITenantManager,
|
||||
ITenant,
|
||||
ITenantDBManager,
|
||||
} from 'interfaces';
|
||||
import {
|
||||
EventDispatcherInterface,
|
||||
EventDispatcher,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { TenantAlreadyInitialized, TenantAlreadySeeded, TenantDatabaseNotBuilt } from 'exceptions';
|
||||
import TenantDBManager from 'services/Tenancy/TenantDBManager';
|
||||
import events from 'subscribers/events';
|
||||
|
||||
const ERRORS = {
|
||||
TENANT_ALREADY_CREATED: 'TENANT_ALREADY_CREATED',
|
||||
TENANT_NOT_EXISTS: 'TENANT_NOT_EXISTS'
|
||||
};
|
||||
|
||||
// Tenants manager service.
|
||||
@Service()
|
||||
export default class TenantsManagerService implements ITenantManager{
|
||||
static instances: { [key: number]: ITenantManager } = {};
|
||||
|
||||
@EventDispatcher()
|
||||
private eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject('repositories')
|
||||
private sysRepositories: any;
|
||||
|
||||
private tenantDBManager: ITenantDBManager;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
this.tenantDBManager = new TenantDBManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new teant with unique organization id.
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<ITenant>}
|
||||
*/
|
||||
public async createTenant(): Promise<ITenant> {
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
const tenant = await tenantRepository.newTenantWithUniqueOrgId();
|
||||
|
||||
return tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tenant database.
|
||||
* @param {ITenant} tenant -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async createDatabase(tenant: ITenant): Promise<void> {
|
||||
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||
|
||||
await this.tenantDBManager.createDatabase(tenant);
|
||||
|
||||
this.eventDispatcher.dispatch(events.tenantManager.databaseCreated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the tenant has database.
|
||||
* @param {ITenant} tenant
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public async hasDatabase(tenant: ITenant): Promise<boolean> {
|
||||
return this.tenantDBManager.databaseExists(tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the tenant database.
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async migrateTenant(tenant: ITenant) {
|
||||
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
|
||||
await this.tenantDBManager.migrate(tenant);
|
||||
await tenantRepository.markAsInitialized(tenant.id);
|
||||
|
||||
this.eventDispatcher.dispatch(events.tenantManager.tenantMigrated, { tenant });
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the tenant database.
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async seedTenant(tenant: ITenant) {
|
||||
this.throwErrorIfTenantNotBuilt(tenant);
|
||||
this.throwErrorIfTenantAlreadySeeded(tenant);
|
||||
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
|
||||
// Seed the tenant database.
|
||||
await this.tenantDBManager.seed(tenant);
|
||||
|
||||
// Mark the tenant as seeded in specific date.
|
||||
await tenantRepository.markAsSeeded(tenant.id);
|
||||
|
||||
this.eventDispatcher.dispatch(events.tenantManager.tenantSeeded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize knex instance or retrieve the instance of cache map.
|
||||
* @param {ITenant} tenant
|
||||
* @returns {Knex}
|
||||
*/
|
||||
public setupKnexInstance(tenant: ITenant) {
|
||||
return this.tenantDBManager.setupKnexInstance(tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tenant knex instance or throw error in case was not initialized.
|
||||
* @param {number} tenantId
|
||||
* @returns {Knex}
|
||||
*/
|
||||
public getKnexInstance(tenantId: number) {
|
||||
return this.tenantDBManager.getKnexInstance(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant already seeded.
|
||||
* @throws {TenantAlreadySeeded}
|
||||
*/
|
||||
private throwErrorIfTenantAlreadySeeded(tenant: ITenant) {
|
||||
if (tenant.seededAt) {
|
||||
throw new TenantAlreadySeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant database is not built yut.
|
||||
* @param tenant
|
||||
*/
|
||||
private throwErrorIfTenantNotBuilt(tenant: ITenant) {
|
||||
if (!tenant.initializedAt) {
|
||||
throw new TenantDatabaseNotBuilt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant already migrated.
|
||||
* @throws {TenantAlreadyInitialized}
|
||||
*/
|
||||
private throwErrorIfTenantAlreadyInitialized(tenant: ITenant) {
|
||||
if (tenant.initializedAt) {
|
||||
throw new TenantAlreadyInitialized();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Inject, Service } from "typedi";
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { SystemUser } from "@/system/models";
|
||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||
import { ISystemUser, ISystemUserDTO } from "@/interfaces";
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { SystemUser } from "system/models";
|
||||
import { ServiceError, ServiceErrors } from "exceptions";
|
||||
import { ISystemUser, ISystemUserDTO } from "interfaces";
|
||||
import systemRepositories from "loaders/systemRepositories";
|
||||
|
||||
@Service()
|
||||
export default class UsersService {
|
||||
@@ -12,6 +13,9 @@ export default class UsersService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject('repositories')
|
||||
repositories: any;
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
* @param {number} tenantId
|
||||
@@ -20,26 +24,17 @@ export default class UsersService {
|
||||
* @return {Promise<ISystemUser>}
|
||||
*/
|
||||
async editUser(tenantId: number, userId: number, userDTO: ISystemUserDTO): Promise<ISystemUser> {
|
||||
const foundUsers = await SystemUser.query()
|
||||
.whereNot('id', userId)
|
||||
.andWhere((query) => {
|
||||
query.where('email', userDTO.email);
|
||||
query.orWhere('phone_number', userDTO.phoneNumber);
|
||||
})
|
||||
.where('tenant_id', tenantId);
|
||||
const { systemUserRepository } = this.repositories;
|
||||
|
||||
const sameUserEmail = foundUsers
|
||||
.some((u: ISystemUser) => u.email === userDTO.email);
|
||||
|
||||
const samePhoneNumber = foundUsers
|
||||
.some((u: ISystemUser) => u.phoneNumber === userDTO.phone_number);
|
||||
const isEmailExists = await systemUserRepository.isEmailExists(userDTO.email, userId);
|
||||
const isPhoneNumberExists = await systemUserRepository.isPhoneNumberExists(userDTO.phoneNumber, userId);
|
||||
|
||||
const serviceErrors: ServiceError[] = [];
|
||||
|
||||
if (sameUserEmail) {
|
||||
if (isEmailExists) {
|
||||
serviceErrors.push(new ServiceError('email_already_exists'));
|
||||
}
|
||||
if (samePhoneNumber) {
|
||||
if (isPhoneNumberExists) {
|
||||
serviceErrors.push(new ServiceError('phone_number_already_exist'));
|
||||
}
|
||||
if (serviceErrors.length > 0) {
|
||||
@@ -47,9 +42,8 @@ export default class UsersService {
|
||||
}
|
||||
const updateSystemUser = await SystemUser.query()
|
||||
.where('id', userId)
|
||||
.update({
|
||||
...userDTO,
|
||||
});
|
||||
.update({ ...userDTO });
|
||||
|
||||
return updateSystemUser;
|
||||
}
|
||||
|
||||
@@ -60,9 +54,9 @@ export default class UsersService {
|
||||
* @returns {ISystemUser}
|
||||
*/
|
||||
async getUserOrThrowError(tenantId: number, userId: number): void {
|
||||
const user = await SystemUser.query().findOne({
|
||||
id: userId, tenant_id: tenantId,
|
||||
});
|
||||
const { systemUserRepository } = this.repositories;
|
||||
const user = await systemUserRepository.getByIdAndTenant(userId, tenantId);
|
||||
|
||||
if (!user) {
|
||||
this.logger.info('[users] the given user not found.', { tenantId, userId });
|
||||
throw new ServiceError('user_not_found');
|
||||
@@ -76,11 +70,11 @@ export default class UsersService {
|
||||
* @param {number} userId
|
||||
*/
|
||||
async deleteUser(tenantId: number, userId: number): Promise<void> {
|
||||
const { systemUserRepository } = this.repositories;
|
||||
await this.getUserOrThrowError(tenantId, userId);
|
||||
|
||||
this.logger.info('[users] trying to delete the given user.', { tenantId, userId });
|
||||
await SystemUser.query().where('tenant_id', tenantId)
|
||||
.where('id', userId).delete();
|
||||
await systemUserRepository.deleteById(userId);
|
||||
|
||||
this.logger.info('[users] the given user deleted successfully.', { tenantId, userId });
|
||||
}
|
||||
@@ -91,12 +85,14 @@ export default class UsersService {
|
||||
* @param {number} userId
|
||||
*/
|
||||
async activateUser(tenantId: number, userId: number): Promise<void> {
|
||||
const { systemUserRepository } = this.repositories;
|
||||
|
||||
const user = await this.getUserOrThrowError(tenantId, userId);
|
||||
this.throwErrorIfUserActive(user);
|
||||
|
||||
await SystemUser.query().findById(userId).update({ active: true });
|
||||
await systemUserRepository.activateUser(userId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inactivate the given user id.
|
||||
* @param {number} tenantId
|
||||
@@ -104,10 +100,11 @@ export default class UsersService {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async inactivateUser(tenantId: number, userId: number): Promise<void> {
|
||||
const { systemUserRepository } = this.repositories;
|
||||
const user = await this.getUserOrThrowError(tenantId, userId);
|
||||
this.throwErrorIfUserInactive(user);
|
||||
|
||||
await SystemUser.query().findById(userId).update({ active: false });
|
||||
await systemUserRepository.inactivateById(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user