mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
Merge branch 'master' of https://github.com/abouolia/Bigcapital into RegisterWizard
This commit is contained in:
@@ -2,9 +2,12 @@ import { sumBy, chain } from 'lodash';
|
||||
import JournalPoster from "./JournalPoster";
|
||||
import JournalEntry from "./JournalEntry";
|
||||
import { AccountTransaction } from 'models';
|
||||
import { IInventoryTransaction, IManualJournal } from 'interfaces';
|
||||
import AccountsService from '../Accounts/AccountsService';
|
||||
import { IInventoryTransaction, IInventoryTransaction } from '../../interfaces';
|
||||
import {
|
||||
IInventoryTransaction,
|
||||
IManualJournal,
|
||||
IExpense,
|
||||
IExpenseCategory,
|
||||
} from 'interfaces';
|
||||
|
||||
interface IInventoryCostEntity {
|
||||
date: Date,
|
||||
@@ -120,6 +123,38 @@ export default class JournalCommands{
|
||||
this.journal.credit(creditEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes journal entries of expense model object.
|
||||
* @param {IExpense} expense
|
||||
*/
|
||||
expense(expense: IExpense) {
|
||||
const mixinEntry = {
|
||||
referenceType: 'Expense',
|
||||
referenceId: expense.id,
|
||||
date: expense.paymentDate,
|
||||
userId: expense.userId,
|
||||
draft: !expense.publishedAt,
|
||||
};
|
||||
const paymentJournalEntry = new JournalEntry({
|
||||
credit: expense.totalAmount,
|
||||
account: expense.paymentAccountId,
|
||||
index: 1,
|
||||
...mixinEntry,
|
||||
});
|
||||
this.journal.credit(paymentJournalEntry);
|
||||
|
||||
expense.categories.forEach((category: IExpenseCategory, index) => {
|
||||
const expenseJournalEntry = new JournalEntry({
|
||||
account: category.expenseAccountId,
|
||||
debit: category.amount,
|
||||
note: category.description,
|
||||
...mixinEntry,
|
||||
index: index + 2,
|
||||
});
|
||||
this.journal.debit(expenseJournalEntry);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number|number[]} referenceId
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { difference } from 'lodash';
|
||||
import { kebabCase } from 'lodash'
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces';
|
||||
import { difference } from 'lodash';
|
||||
import { IAccountDTO, IAccount, IAccountsFilter, IFilterMeta } from 'interfaces';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import events from 'subscribers/events';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import { Account } from 'models';
|
||||
import AccountRepository from 'repositories/AccountRepository';
|
||||
|
||||
@Service()
|
||||
export default class AccountsService {
|
||||
@@ -17,6 +25,9 @@ export default class AccountsService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Retrieve account type or throws service error.
|
||||
* @param {number} tenantId -
|
||||
@@ -104,10 +115,10 @@ export default class AccountsService {
|
||||
* @return {IAccount}
|
||||
*/
|
||||
private async getAccountOrThrowError(tenantId: number, accountId: number) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[accounts] validating the account existance.', { tenantId, accountId });
|
||||
const account = await Account.query().findById(accountId);
|
||||
const account = await accountRepository.findById(accountId);
|
||||
|
||||
if (!account) {
|
||||
this.logger.info('[accounts] the given account not found.', { accountId });
|
||||
@@ -159,8 +170,8 @@ export default class AccountsService {
|
||||
* @returns {IAccount}
|
||||
*/
|
||||
public async newAccount(tenantId: number, accountDTO: IAccountDTO) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Validate account name uniquiness.
|
||||
await this.validateAccountNameUniquiness(tenantId, accountDTO.name);
|
||||
|
||||
@@ -176,11 +187,15 @@ export default class AccountsService {
|
||||
);
|
||||
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
|
||||
}
|
||||
const account = await Account.query().insertAndFetch({
|
||||
const account = await accountRepository.insert({
|
||||
...accountDTO,
|
||||
slug: kebabCase(accountDTO.name),
|
||||
});
|
||||
this.logger.info('[account] account created successfully.', { account, accountDTO });
|
||||
|
||||
// Triggers `onAccountCreated` event.
|
||||
this.eventDispatcher.dispatch(events.accounts.onCreated);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
@@ -191,7 +206,7 @@ export default class AccountsService {
|
||||
* @param {IAccountDTO} accountDTO
|
||||
*/
|
||||
public async editAccount(tenantId: number, accountId: number, accountDTO: IAccountDTO) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const oldAccount = await this.getAccountOrThrowError(tenantId, accountId);
|
||||
|
||||
// Validate account name uniquiness.
|
||||
@@ -214,12 +229,13 @@ export default class AccountsService {
|
||||
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
|
||||
}
|
||||
// Update the account on the storage.
|
||||
const account = await Account.query().patchAndFetchById(
|
||||
oldAccount.id, { ...accountDTO }
|
||||
);
|
||||
const account = await accountRepository.edit(oldAccount.id, accountDTO);
|
||||
this.logger.info('[account] account edited successfully.', {
|
||||
account, accountDTO, tenantId
|
||||
});
|
||||
// Triggers `onAccountEdited` event.
|
||||
this.eventDispatcher.dispatch(events.accounts.onEdited);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
@@ -247,17 +263,6 @@ export default class AccountsService {
|
||||
return foundAccounts.length > 0;
|
||||
}
|
||||
|
||||
public async getAccountByType(tenantId: number, accountTypeKey: string) {
|
||||
const { AccountType, Account } = this.tenancy.models(tenantId);
|
||||
const accountType = await AccountType.query()
|
||||
.findOne('key', accountTypeKey);
|
||||
|
||||
const account = await Account.query()
|
||||
.findOne('account_type_id', accountType.id);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the account was prefined.
|
||||
* @param {IAccount} account
|
||||
@@ -309,7 +314,7 @@ export default class AccountsService {
|
||||
* @param {number} accountId
|
||||
*/
|
||||
public async deleteAccount(tenantId: number, accountId: number) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const account = await this.getAccountOrThrowError(tenantId, accountId);
|
||||
|
||||
this.throwErrorIfAccountPredefined(account);
|
||||
@@ -317,10 +322,13 @@ export default class AccountsService {
|
||||
await this.throwErrorIfAccountHasChildren(tenantId, accountId);
|
||||
await this.throwErrorIfAccountHasTransactions(tenantId, accountId);
|
||||
|
||||
await Account.query().deleteById(account.id);
|
||||
await accountRepository.deleteById(account.id);
|
||||
this.logger.info('[account] account has been deleted successfully.', {
|
||||
tenantId, accountId,
|
||||
})
|
||||
});
|
||||
|
||||
// Triggers `onAccountDeleted` event.
|
||||
this.eventDispatcher.dispatch(events.accounts.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,6 +408,9 @@ export default class AccountsService {
|
||||
this.logger.info('[account] given accounts deleted in bulk successfully.', {
|
||||
tenantId, accountsIds
|
||||
});
|
||||
|
||||
// Triggers `onBulkDeleted` event.
|
||||
this.eventDispatcher.dispatch(events.accounts.onBulkDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,6 +429,9 @@ export default class AccountsService {
|
||||
active: activate ? 1 : 0,
|
||||
});
|
||||
this.logger.info('[account] accounts have been activated successfully.', { tenantId, accountsIds });
|
||||
|
||||
// Triggers `onAccountBulkActivated` event.
|
||||
this.eventDispatcher.dispatch(events.accounts.onActivated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -436,6 +450,9 @@ export default class AccountsService {
|
||||
active: activate ? 1 : 0,
|
||||
})
|
||||
this.logger.info('[account] account have been activated successfully.', { tenantId, accountId });
|
||||
|
||||
// Triggers `onAccountActivated` event.
|
||||
this.eventDispatcher.dispatch(events.accounts.onActivated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,9 +460,11 @@ export default class AccountsService {
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountsFilter} accountsFilter
|
||||
*/
|
||||
public async getAccountsList(tenantId: number, filter: IAccountsFilter) {
|
||||
public async getAccountsList(
|
||||
tenantId: number,
|
||||
filter: IAccountsFilter,
|
||||
): Promise<{ accounts: IAccount[], filterMeta: IFilterMeta }> {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter);
|
||||
|
||||
this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter });
|
||||
@@ -453,6 +472,60 @@ export default class AccountsService {
|
||||
builder.withGraphFetched('type');
|
||||
dynamicList.buildQuery()(builder);
|
||||
});
|
||||
return accounts;
|
||||
|
||||
return {
|
||||
accounts,
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the given account.
|
||||
* -----------
|
||||
* Precedures.
|
||||
* -----------
|
||||
* - Transfer the given account transactions to another account
|
||||
* with the same root type.
|
||||
* - Delete the given account.
|
||||
* -------
|
||||
* @param {number} tenantId -
|
||||
* @param {number} accountId -
|
||||
* @param {number} toAccountId -
|
||||
* @param {boolean} deleteAfterClosing -
|
||||
*/
|
||||
public async closeAccount(
|
||||
tenantId: number,
|
||||
accountId: number,
|
||||
toAccountId: number,
|
||||
deleteAfterClosing: boolean,
|
||||
) {
|
||||
this.logger.info('[account] trying to close account.', { tenantId, accountId, toAccountId, deleteAfterClosing });
|
||||
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const account = await this.getAccountOrThrowError(tenantId, accountId);
|
||||
const toAccount = await this.getAccountOrThrowError(tenantId, toAccountId);
|
||||
|
||||
this.throwErrorIfAccountPredefined(account);
|
||||
|
||||
const accountType = await accountTypeRepository.getTypeMeta(account.accountTypeId);
|
||||
const toAccountType = await accountTypeRepository.getTypeMeta(toAccount.accountTypeId);
|
||||
|
||||
if (accountType.rootType !== toAccountType.rootType) {
|
||||
throw new ServiceError('close_account_and_to_account_not_same_type');
|
||||
}
|
||||
const updateAccountBalanceOper = await accountRepository.balanceChange(accountId, account.balance || 0);
|
||||
|
||||
// Move transactiosn operation.
|
||||
const moveTransactionsOper = await AccountTransaction.query()
|
||||
.where('account_id', accountId)
|
||||
.patch({ accountId: toAccountId });
|
||||
|
||||
await Promise.all([ moveTransactionsOper, updateAccountBalanceOper ]);
|
||||
|
||||
if (deleteAfterClosing) {
|
||||
await accountRepository.deleteById(accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { SystemUser, PasswordReset } from 'system/models';
|
||||
import { PasswordReset } from 'system/models';
|
||||
import {
|
||||
IRegisterDTO,
|
||||
ITenant,
|
||||
ISystemUser,
|
||||
IPasswordReset,
|
||||
IAuthenticationService,
|
||||
} from 'interfaces';
|
||||
import { hashPassword } from 'utils';
|
||||
import { ServiceError, ServiceErrors } from 'exceptions';
|
||||
@@ -134,7 +135,7 @@ export default class AuthenticationService implements IAuthenticationService {
|
||||
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const registeredUser = await systemUserRepository.create({
|
||||
...omit(registerDTO, 'country', 'organizationName'),
|
||||
...omit(registerDTO, 'country'),
|
||||
active: true,
|
||||
password: hashedPassword,
|
||||
tenant_id: tenant.id,
|
||||
|
||||
@@ -45,10 +45,10 @@ export default class ContactsService {
|
||||
* @param {IContactDTO} contactDTO
|
||||
*/
|
||||
async newContact(tenantId: number, contactDTO: IContactNewDTO, contactService: TContactService) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
const { contactRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO });
|
||||
const contact = await Contact.query().insert({ contactService, ...contactDTO });
|
||||
const contact = await contactRepository.insert({ contactService, ...contactDTO });
|
||||
|
||||
this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact });
|
||||
return contact;
|
||||
@@ -77,11 +77,11 @@ export default class ContactsService {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteContact(tenantId: number, contactId: number, contactService: TContactService) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
const { contactRepository } = this.tenancy.repositories(tenantId);
|
||||
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
|
||||
|
||||
this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId });
|
||||
await Contact.query().findById(contactId).delete();
|
||||
await contactRepository.deleteById(contactId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,10 +124,10 @@ export default class ContactsService {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteBulkContacts(tenantId: number, contactsIds: number[], contactService: TContactService) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
const { contactRepository } = this.tenancy.repositories(tenantId);
|
||||
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
|
||||
|
||||
await Contact.query().whereIn('id', contactsIds).delete();
|
||||
await contactRepository.bulkDelete(contactsIds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,10 +6,13 @@ import ContactsService from 'services/Contacts/ContactsService';
|
||||
import {
|
||||
ICustomerNewDTO,
|
||||
ICustomerEditDTO,
|
||||
ICustomer,
|
||||
IPaginationMeta,
|
||||
ICustomersFilter
|
||||
} from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ICustomer } from 'src/interfaces';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
|
||||
@Service()
|
||||
export default class CustomersService {
|
||||
@@ -19,12 +22,15 @@ export default class CustomersService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Converts customer to contact DTO.
|
||||
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
|
||||
* @returns {IContactDTO}
|
||||
*/
|
||||
customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO) {
|
||||
private customerToContactDTO(customerDTO: ICustomerNewDTO | ICustomerEditDTO) {
|
||||
return {
|
||||
...omit(customerDTO, ['customerType']),
|
||||
contactType: customerDTO.customerType,
|
||||
@@ -39,7 +45,7 @@ export default class CustomersService {
|
||||
* @param {ICustomerNewDTO} customerDTO
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
|
||||
public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
|
||||
const contactDTO = this.customerToContactDTO(customerDTO)
|
||||
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
|
||||
|
||||
@@ -59,7 +65,7 @@ export default class CustomersService {
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerEditDTO} customerDTO
|
||||
*/
|
||||
async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) {
|
||||
public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) {
|
||||
const contactDTO = this.customerToContactDTO(customerDTO);
|
||||
return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
|
||||
}
|
||||
@@ -70,7 +76,7 @@ export default class CustomersService {
|
||||
* @param {number} customerId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteCustomer(tenantId: number, customerId: number) {
|
||||
public async deleteCustomer(tenantId: number, customerId: number) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
await this.getCustomerByIdOrThrowError(tenantId, customerId);
|
||||
@@ -88,10 +94,34 @@ export default class CustomersService {
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
*/
|
||||
async getCustomer(tenantId: number, customerId: number) {
|
||||
public async getCustomer(tenantId: number, customerId: number) {
|
||||
return this.contactService.getContact(tenantId, customerId, 'customer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve customers paginated list.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {ICustomersFilter} filter - Cusotmers filter.
|
||||
*/
|
||||
public async getCustomersList(
|
||||
tenantId: number,
|
||||
filter: ICustomersFilter
|
||||
): Promise<{ customers: ICustomer[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, filter);
|
||||
|
||||
const { results, pagination } = await Contact.query().onBuild((query) => {
|
||||
query.modify('customer');
|
||||
dynamicList.buildQuery()(query);
|
||||
});
|
||||
|
||||
return {
|
||||
customers: results,
|
||||
pagination,
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes customer opening balance journal entries.
|
||||
* @param {number} tenantId
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import Resource from 'models/Resource';
|
||||
import ResourceField from 'models/ResourceField';
|
||||
import ResourceFieldMetadata from 'models/ResourceFieldMetadata';
|
||||
import ResourceFieldMetadataCollection from 'collection/ResourceFieldMetadataCollection';
|
||||
|
||||
export default class ResourceCustomFieldRepository {
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
constructor(model) {
|
||||
if (typeof model === 'function') {
|
||||
this.resourceName = model.name;
|
||||
} else if (typeof model === 'string') {
|
||||
this.resourceName = model;
|
||||
}
|
||||
// Custom fields of the given resource.
|
||||
this.customFields = [];
|
||||
this.filledCustomFields = {};
|
||||
|
||||
// metadata of custom fields of the given resource.
|
||||
this.fieldsMetadata = {};
|
||||
this.resource = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches metadata of custom fields of the given resource.
|
||||
* @param {Integer} id - Resource item id.
|
||||
*/
|
||||
async fetchCustomFieldsMetadata(id) {
|
||||
if (typeof id === 'undefined') {
|
||||
throw new Error('Please define the resource item id.');
|
||||
}
|
||||
if (!this.resource) {
|
||||
throw new Error('Target resource model is not found.');
|
||||
}
|
||||
const metadata = await ResourceFieldMetadata.query()
|
||||
.where('resource_id', this.resource.id)
|
||||
.where('resource_item_id', id);
|
||||
|
||||
this.fieldsMetadata[id] = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load resource.
|
||||
*/
|
||||
async loadResource() {
|
||||
const resource = await Resource.query().where('name', this.resourceName).first();
|
||||
|
||||
if (!resource) {
|
||||
throw new Error('There is no stored resource in the storage with the given model name.');
|
||||
}
|
||||
this.setResource(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load metadata of the resource.
|
||||
*/
|
||||
async loadResourceCustomFields() {
|
||||
if (typeof this.resource.id === 'undefined') {
|
||||
throw new Error('Please fetch resource details before fetch custom fields of the resource.');
|
||||
}
|
||||
const customFields = await ResourceField.query()
|
||||
.where('resource_id', this.resource.id)
|
||||
.modify('whereNotPredefined');
|
||||
|
||||
this.setResourceCustomFields(customFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets resource model.
|
||||
* @param {Resource} resource -
|
||||
*/
|
||||
setResource(resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets resource custom fields collection.
|
||||
* @param {Array} customFields -
|
||||
*/
|
||||
setResourceCustomFields(customFields) {
|
||||
this.customFields = customFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve metadata of the resource custom fields.
|
||||
* @param {Integer} itemId -
|
||||
*/
|
||||
getMetadata(itemId) {
|
||||
return this.fieldsMetadata[itemId] || this.fieldsMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill metadata of the custom fields that associated to the resource.
|
||||
* @param {Inter} id - Resource item id.
|
||||
* @param {Array} attributes -
|
||||
*/
|
||||
fillCustomFields(id, attributes) {
|
||||
if (typeof this.filledCustomFields[id] === 'undefined') {
|
||||
this.filledCustomFields[id] = [];
|
||||
}
|
||||
attributes.forEach((attr) => {
|
||||
this.filledCustomFields[id].push(attr);
|
||||
|
||||
if (!this.fieldsMetadata[id]) {
|
||||
this.fieldsMetadata[id] = new ResourceFieldMetadataCollection();
|
||||
}
|
||||
this.fieldsMetadata[id].setMeta(attr.key, attr.value, {
|
||||
resource_id: this.resource.id,
|
||||
resource_item_id: id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the instered, updated and deleted custom fields metadata.
|
||||
* @param {Integer} id - Optional resource item id.
|
||||
*/
|
||||
async saveCustomFields(id) {
|
||||
if (id) {
|
||||
if (typeof this.fieldsMetadata[id] === 'undefined') {
|
||||
throw new Error('There is no resource item with the given id.');
|
||||
}
|
||||
await this.fieldsMetadata[id].saveMeta();
|
||||
} else {
|
||||
const opers = [];
|
||||
this.fieldsMetadata.forEach((metadata) => {
|
||||
const oper = metadata.saveMeta();
|
||||
opers.push(oper);
|
||||
});
|
||||
await Promise.all(opers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the exist custom fields.
|
||||
*/
|
||||
validateExistCustomFields() {
|
||||
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return this.fieldsMetadata.toArray();
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.loadResource();
|
||||
await this.loadResourceCustomFields();
|
||||
}
|
||||
|
||||
static forgeMetadataCollection() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import validator from 'is-my-json-valid';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import {
|
||||
DynamicFilter,
|
||||
@@ -12,8 +12,13 @@ import {
|
||||
validateFieldKeyExistance,
|
||||
validateFilterRolesFieldsExistance,
|
||||
} from 'lib/ViewRolesBuilder';
|
||||
import {
|
||||
IDynamicListFilterDTO,
|
||||
IFilterRole,
|
||||
IDynamicListService,
|
||||
IModel,
|
||||
} from 'interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { IDynamicListFilterDTO, IFilterRole, IDynamicListService } from 'interfaces';
|
||||
|
||||
const ERRORS = {
|
||||
VIEW_NOT_FOUND: 'view_not_found',
|
||||
@@ -32,11 +37,11 @@ export default class DynamicListService implements IDynamicListService {
|
||||
* @param {number} viewId
|
||||
* @return {Promise<IView>}
|
||||
*/
|
||||
private async getCustomViewOrThrowError(tenantId: number, viewId: number) {
|
||||
private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) {
|
||||
const { viewRepository } = this.tenancy.repositories(tenantId);
|
||||
const view = await viewRepository.getById(viewId);
|
||||
|
||||
if (!view || view.resourceModel !== 'Account') {
|
||||
if (!view || view.resourceModel !== model.name) {
|
||||
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
|
||||
}
|
||||
return view;
|
||||
@@ -49,9 +54,9 @@ export default class DynamicListService implements IDynamicListService {
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
private validateSortColumnExistance(model: any, columnSortBy: string) {
|
||||
const notExistsField = validateFieldKeyExistance(model.tableName, columnSortBy);
|
||||
const notExistsField = validateFieldKeyExistance(model, columnSortBy);
|
||||
|
||||
if (notExistsField) {
|
||||
if (!notExistsField) {
|
||||
throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
@@ -62,8 +67,10 @@ export default class DynamicListService implements IDynamicListService {
|
||||
* @param {IFilterRole[]} filterRoles
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
private validateRolesFieldsExistance(model: any, filterRoles: IFilterRole[]) {
|
||||
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model.tableName, filterRoles);
|
||||
private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) {
|
||||
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles);
|
||||
|
||||
console.log(invalidFieldsKeys);
|
||||
|
||||
if (invalidFieldsKeys.length > 0) {
|
||||
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
|
||||
@@ -96,23 +103,21 @@ export default class DynamicListService implements IDynamicListService {
|
||||
* Dynamic listing.
|
||||
* @param {number} tenantId
|
||||
* @param {IModel} model
|
||||
* @param {IAccountsFilter} filter
|
||||
* @param {IDynamicListFilterDTO} filter
|
||||
*/
|
||||
async dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO) {
|
||||
const { viewRoleRepository } = this.tenancy.repositories(tenantId);
|
||||
const dynamicFilter = new DynamicFilter(model.tableName);
|
||||
public async dynamicList(tenantId: number, model: IModel, filter: IDynamicListFilterDTO) {
|
||||
const dynamicFilter = new DynamicFilter(model);
|
||||
|
||||
// Custom view filter roles.
|
||||
if (filter.customViewId) {
|
||||
const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId);
|
||||
const viewRoles = await viewRoleRepository.allByView(view.id);
|
||||
const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId, model);
|
||||
|
||||
const viewFilter = new DynamicFilterViews(viewRoles, view.rolesLogicExpression);
|
||||
const viewFilter = new DynamicFilterViews(view);
|
||||
dynamicFilter.setFilter(viewFilter);
|
||||
}
|
||||
// Sort by the given column.
|
||||
if (filter.columnSortBy) {
|
||||
this.validateSortColumnExistance(model, filter.columnSortBy);;
|
||||
this.validateSortColumnExistance(model, filter.columnSortBy);
|
||||
|
||||
const sortByFilter = new DynamicFilterSortBy(
|
||||
filter.columnSortBy, filter.sortOrder
|
||||
@@ -124,7 +129,7 @@ export default class DynamicListService implements IDynamicListService {
|
||||
this.validateFilterRolesSchema(filter.filterRoles);
|
||||
this.validateRolesFieldsExistance(model, filter.filterRoles);
|
||||
|
||||
// Validate the accounts resource fields.
|
||||
// Validate the model resource fields.
|
||||
const filterRoles = new DynamicFilterFilterRoles(filter.filterRoles);
|
||||
dynamicFilter.setFilter(filterRoles);
|
||||
}
|
||||
@@ -138,7 +143,7 @@ export default class DynamicListService implements IDynamicListService {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerErrorsToResponse(error, req: Request, res: Response, next: NextFunction) {
|
||||
public handlerErrorsToResponse(error: Error, req: Request, res: Response, next: NextFunction) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'sort_column_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
@@ -147,8 +152,8 @@ export default class DynamicListService implements IDynamicListService {
|
||||
}
|
||||
if (error.errorType === 'view_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }]
|
||||
})
|
||||
errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'filter_roles_fields_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import { difference, sumBy, omit } from 'lodash';
|
||||
import moment from "moment";
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
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';
|
||||
import { IExpense, IExpensesFilter, IAccount, IExpenseDTO, IExpensesService, ISystemUser, IPaginationMeta } from 'interfaces';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import events from 'subscribers/events';
|
||||
|
||||
const ERRORS = {
|
||||
EXPENSE_NOT_FOUND: 'expense_not_found',
|
||||
@@ -30,6 +34,9 @@ export default class ExpensesService implements IExpensesService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Retrieve the payment account details or returns not found server error in case the
|
||||
* given account not found on the storage.
|
||||
@@ -41,7 +48,7 @@ export default class ExpensesService implements IExpensesService {
|
||||
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)
|
||||
const paymentAccount = await accountRepository.findById(paymentAccountId)
|
||||
|
||||
if (!paymentAccount) {
|
||||
this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId });
|
||||
@@ -136,16 +143,15 @@ export default class ExpensesService implements IExpensesService {
|
||||
}
|
||||
}
|
||||
|
||||
private async revertJournalEntries(
|
||||
public async revertJournalEntries(
|
||||
tenantId: number,
|
||||
expenseId: number|number[],
|
||||
) {
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommands = new JournalCommands(journal);
|
||||
|
||||
if (revertOld) {
|
||||
await journalCommands.revertJournalEntries(expenseId, 'Expense');
|
||||
}
|
||||
|
||||
await journalCommands.revertJournalEntries(expenseId, 'Expense');
|
||||
|
||||
return Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.deleteEntries(),
|
||||
@@ -158,11 +164,10 @@ export default class ExpensesService implements IExpensesService {
|
||||
* @param {IExpense} expense
|
||||
* @param {IUser} authorizedUser
|
||||
*/
|
||||
private async writeJournalEntries(
|
||||
public 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);
|
||||
@@ -171,29 +176,8 @@ export default class ExpensesService implements IExpensesService {
|
||||
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);
|
||||
});
|
||||
journalCommands.expense(expense);
|
||||
|
||||
return Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.saveEntries(),
|
||||
@@ -229,7 +213,7 @@ export default class ExpensesService implements IExpensesService {
|
||||
* @param {IExpense} expense
|
||||
*/
|
||||
private validateExpenseIsNotPublished(expense: IExpense) {
|
||||
if (expense.published) {
|
||||
if (expense.publishedAt) {
|
||||
throw new ServiceError(ERRORS.EXPENSE_ACCOUNT_ALREADY_PUBLISED);
|
||||
}
|
||||
}
|
||||
@@ -291,33 +275,29 @@ export default class ExpensesService implements IExpensesService {
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
||||
|
||||
// 1. Validate payment account existance on the storage.
|
||||
// - Validate payment account existance on the storage.
|
||||
const paymentAccount = await this.getPaymentAccountOrThrowError(
|
||||
tenantId,
|
||||
expenseDTO.paymentAccountId,
|
||||
);
|
||||
// 2. Validate expense accounts exist on the storage.
|
||||
// - Validate expense accounts exist on the storage.
|
||||
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
|
||||
tenantId,
|
||||
this.mapExpensesAccountsIdsFromDTO(expenseDTO),
|
||||
);
|
||||
// 3. Validate payment account type.
|
||||
// - Validate payment account type.
|
||||
await this.validatePaymentAccountType(tenantId, paymentAccount);
|
||||
|
||||
// 4. Validate expenses accounts type.
|
||||
// - Validate expenses accounts type.
|
||||
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
|
||||
|
||||
// 5. Validate the given expense categories not equal zero.
|
||||
// - Validate the given expense categories not equal zero.
|
||||
this.validateCategoriesNotEqualZero(expenseDTO);
|
||||
|
||||
// 6. Update the expense on the storage.
|
||||
// - 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;
|
||||
}
|
||||
@@ -364,13 +344,12 @@ export default class ExpensesService implements IExpensesService {
|
||||
// 6. Save the expense to the storage.
|
||||
const expenseObj = this.expenseDTOToModel(expenseDTO, authorizedUser);
|
||||
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 });
|
||||
|
||||
// Triggers `onExpenseCreated` event.
|
||||
this.eventDispatcher.dispatch(events.expenses.onCreated, { tenantId, expenseId: expenseModel.id });
|
||||
|
||||
return expenseModel;
|
||||
}
|
||||
|
||||
@@ -394,6 +373,9 @@ export default class ExpensesService implements IExpensesService {
|
||||
await expenseRepository.publish(expenseId);
|
||||
|
||||
this.logger.info('[expense] the expense published successfully.', { tenantId, expenseId });
|
||||
|
||||
// Triggers `onExpensePublished` event.
|
||||
this.eventDispatcher.dispatch(events.expenses.onPublished, { tenantId, expenseId });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -409,10 +391,10 @@ export default class ExpensesService implements IExpensesService {
|
||||
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 });
|
||||
|
||||
// Triggers `onExpenseDeleted` event.
|
||||
this.eventDispatcher.dispatch(events.expenses.onDeleted, { tenantId, expenseId });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -427,9 +409,11 @@ export default class ExpensesService implements IExpensesService {
|
||||
|
||||
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 });
|
||||
|
||||
// Triggers `onExpenseBulkDeleted` event.
|
||||
this.eventDispatcher.dispatch(events.expenses.onBulkDeleted, { tenantId, expensesIds });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,9 +427,12 @@ export default class ExpensesService implements IExpensesService {
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds });
|
||||
await expenseRepository.publishBulk(expensesIds);
|
||||
await expenseRepository.bulkPublish(expensesIds);
|
||||
|
||||
this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds });
|
||||
|
||||
// Triggers `onExpenseBulkDeleted` event.
|
||||
this.eventDispatcher.dispatch(events.expenses.onBulkPublished, { tenantId, expensesIds });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -454,17 +441,43 @@ export default class ExpensesService implements IExpensesService {
|
||||
* @param {IExpensesFilter} expensesFilter
|
||||
* @return {IExpense[]}
|
||||
*/
|
||||
public async getExpensesList(tenantId: number, expensesFilter: IExpensesFilter) {
|
||||
public async getExpensesList(
|
||||
tenantId: number,
|
||||
expensesFilter: IExpensesFilter
|
||||
): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
const { Expense } = this.tenancy.models(tenantId);
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, expensesFilter);
|
||||
|
||||
this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, expensesFilter });
|
||||
const expenses = await Expense.query().onBuild((builder) => {
|
||||
const { results, pagination } = await Expense.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
builder.withGraphFetched('user');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
});
|
||||
return expenses;
|
||||
}).pagination(expensesFilter.page - 1, expensesFilter.pageSize);
|
||||
|
||||
return {
|
||||
expenses: results,
|
||||
pagination, filterMeta:
|
||||
dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve expense details.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @return {Promise<IExpense>}
|
||||
*/
|
||||
public async getExpense(tenantId: number, expenseId: number): Promise<IExpense> {
|
||||
const { Expense } = this.tenancy.models(tenantId);
|
||||
|
||||
const expense = await Expense.query().findById(expenseId)
|
||||
.withGraphFetched('paymentAccount')
|
||||
.withGraphFetched('media')
|
||||
.withGraphFetched('categories');
|
||||
|
||||
if (!expense) {
|
||||
throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND);
|
||||
}
|
||||
return expense;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
IItemCategoriesFilter,
|
||||
ISystemUser,
|
||||
} from "interfaces";
|
||||
import ItemCategory from "models/ItemCategory";
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
@@ -21,6 +20,7 @@ const ERRORS = {
|
||||
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
|
||||
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
|
||||
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||
CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS'
|
||||
};
|
||||
|
||||
export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
@@ -108,7 +108,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
|
||||
this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId });
|
||||
const incomeType = await accountTypeRepository.getByKey('income');
|
||||
const foundAccount = await accountRepository.getById(sellAccountId);
|
||||
const foundAccount = await accountRepository.findById(sellAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] sell account not found.', { tenantId, sellAccountId });
|
||||
@@ -130,7 +130,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
|
||||
this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId });
|
||||
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
|
||||
const foundAccount = await accountRepository.getById(costAccountId)
|
||||
const foundAccount = await accountRepository.findById(costAccountId)
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] cost account not found.', { tenantId, costAccountId });
|
||||
@@ -152,7 +152,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
|
||||
this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId });
|
||||
const otherAsset = await accountTypeRepository.getByKey('other_asset');
|
||||
const foundAccount = await accountRepository.getById(inventoryAccountId);
|
||||
const foundAccount = await accountRepository.findById(inventoryAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId });
|
||||
@@ -202,6 +202,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) {
|
||||
this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId });
|
||||
await this.getItemCategoryOrThrowError(tenantId, itemCategoryId);
|
||||
await this.unassociateItemsWithCategories(tenantId, itemCategoryId);
|
||||
|
||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||
await ItemCategory.query().findById(itemCategoryId).delete();
|
||||
@@ -214,7 +215,9 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
* @param {number[]} itemCategoriesIds
|
||||
*/
|
||||
private async getItemCategoriesOrThrowError(tenantId: number, itemCategoriesIds: number[]) {
|
||||
const itemCategories = await ItemCategory.query().whereIn('id', ids);
|
||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||
const itemCategories = await ItemCategory.query().whereIn('id', itemCategoriesIds);
|
||||
|
||||
const storedItemCategoriesIds = itemCategories.map((category: IItemCategory) => category.id);
|
||||
const notFoundCategories = difference(itemCategoriesIds, storedItemCategoriesIds);
|
||||
|
||||
@@ -233,10 +236,22 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter);
|
||||
|
||||
const itemCategories = await ItemCategory.query().onBuild((query) => {
|
||||
query.orderBy('createdAt', 'ASC');
|
||||
dynamicList.buildQuery()(query);
|
||||
});
|
||||
return itemCategories;
|
||||
return { itemCategories, filterMeta: dynamicList.getResponseMeta() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink items relations with item categories.
|
||||
* @param {number} tenantId
|
||||
* @param {number|number[]} itemCategoryId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
private async unassociateItemsWithCategories(tenantId: number, itemCategoryId: number|number[]): Promise<void> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const ids = Array.isArray(itemCategoryId) ? itemCategoryId : [itemCategoryId];
|
||||
|
||||
await Item.query().whereIn('id', ids).patch({ category_id: null });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,8 +261,11 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
*/
|
||||
public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) {
|
||||
this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds });
|
||||
await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds);
|
||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||
|
||||
await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds);
|
||||
await this.unassociateItemsWithCategories(tenantId, itemCategoriesIds);
|
||||
|
||||
await ItemCategory.query().whereIn('id', itemCategoriesIds).delete();
|
||||
this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds });
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ServiceError } from "exceptions";
|
||||
import { Item } from "models";
|
||||
|
||||
const ERRORS = {
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
@@ -17,6 +16,9 @@ const ERRORS = {
|
||||
|
||||
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
|
||||
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||
|
||||
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
|
||||
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS'
|
||||
}
|
||||
|
||||
@Service()
|
||||
@@ -83,7 +85,7 @@ export default class ItemsService implements IItemsService {
|
||||
|
||||
this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId });
|
||||
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
|
||||
const foundAccount = await accountRepository.getById(costAccountId)
|
||||
const foundAccount = await accountRepository.findById(costAccountId)
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] cost account not found.', { tenantId, costAccountId });
|
||||
@@ -104,7 +106,7 @@ export default class ItemsService implements IItemsService {
|
||||
|
||||
this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId });
|
||||
const incomeType = await accountTypeRepository.getByKey('income');
|
||||
const foundAccount = await accountRepository.getById(sellAccountId);
|
||||
const foundAccount = await accountRepository.findById(sellAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] sell account not found.', { tenantId, sellAccountId });
|
||||
@@ -125,7 +127,7 @@ export default class ItemsService implements IItemsService {
|
||||
|
||||
this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId });
|
||||
const otherAsset = await accountTypeRepository.getByKey('other_asset');
|
||||
const foundAccount = await accountRepository.getById(inventoryAccountId);
|
||||
const foundAccount = await accountRepository.findById(inventoryAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId });
|
||||
@@ -222,6 +224,7 @@ export default class ItemsService implements IItemsService {
|
||||
|
||||
this.logger.info('[items] trying to delete item.', { tenantId, itemId });
|
||||
await this.getItemOrThrowError(tenantId, itemId);
|
||||
await this.validateHasNoInvoicesOrBills(tenantId, itemId);
|
||||
|
||||
await Item.query().findById(itemId).delete();
|
||||
this.logger.info('[items] deleted successfully.', { tenantId, itemId });
|
||||
@@ -269,6 +272,7 @@ export default class ItemsService implements IItemsService {
|
||||
|
||||
this.logger.info('[items] trying to delete items in bulk.', { tenantId, itemsIds });
|
||||
await this.validateItemsIdsExists(tenantId, itemsIds);
|
||||
await this.validateHasNoInvoicesOrBills(tenantId, itemsIds);
|
||||
|
||||
await Item.query().whereIn('id', itemsIds).delete();
|
||||
this.logger.info('[items] deleted successfully in bulk.', { tenantId, itemsIds });
|
||||
@@ -283,14 +287,39 @@ export default class ItemsService implements IItemsService {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter);
|
||||
|
||||
const items = await Item.query().onBuild((builder) => {
|
||||
const { results, pagination } = await Item.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('inventoryAccount');
|
||||
builder.withGraphFetched('sellAccount');
|
||||
builder.withGraphFetched('costAccount');
|
||||
builder.withGraphFetched('category');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
});
|
||||
return items;
|
||||
}).pagination(
|
||||
itemsFilter.page - 1,
|
||||
itemsFilter.pageSize,
|
||||
);
|
||||
return { items: results, pagination, filterMeta: dynamicFilter.getResponseMeta() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given item or items have no associated invoices or bills.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number|number[]} itemId - Item id.
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
private async validateHasNoInvoicesOrBills(tenantId: number, itemId: number[]|number) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const ids = Array.isArray(itemId) ? itemId : [itemId];
|
||||
const foundItemEntries = await ItemEntry.query()
|
||||
.whereIn('item_id', ids)
|
||||
.whereIn('reference_type', ['SaleInvoice', 'Bill']);
|
||||
|
||||
if (foundItemEntries.length > 0) {
|
||||
throw new ServiceError(ids.length > 1 ?
|
||||
ERRORS.ITEMS_HAVE_ASSOCIATED_TRANSACTIONS :
|
||||
ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTINS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ISystemUser,
|
||||
IManualJournal,
|
||||
IManualJournalEntryDTO,
|
||||
IPaginationMeta,
|
||||
} from 'interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
@@ -227,7 +228,7 @@ export default class ManualJournalsService implements IManuaLJournalsService {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Transform DTO to model.
|
||||
* @param {IManualJournalEntryDTO[]} entries
|
||||
*/
|
||||
private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) {
|
||||
@@ -396,16 +397,23 @@ export default class ManualJournalsService implements IManuaLJournalsService {
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalsFilter} filter
|
||||
*/
|
||||
public async getManualJournals(tenantId: number, filter: IManualJournalsFilter) {
|
||||
public async getManualJournals(
|
||||
tenantId: number,
|
||||
filter: IManualJournalsFilter
|
||||
): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ManualJournal, filter);
|
||||
|
||||
this.logger.info('[manual_journals] trying to get manual journals list.', { tenantId, filter });
|
||||
const manualJournal = await ManualJournal.query().onBuild((builder) => {
|
||||
const { results, pagination } = await ManualJournal.query().onBuild((builder) => {
|
||||
dynamicList.buildQuery()(builder);
|
||||
});
|
||||
return manualJournal;
|
||||
}).pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
return {
|
||||
manualJournals: results,
|
||||
pagination,
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,7 +429,8 @@ export default class ManualJournalsService implements IManuaLJournalsService {
|
||||
this.logger.info('[manual_journals] trying to get specific manual journal.', { tenantId, manualJournalId });
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.findById(manualJournalId)
|
||||
.withGraphFetched('entries');
|
||||
.withGraphFetched('entries')
|
||||
.withGraphFetched('media');
|
||||
|
||||
return manualJournal;
|
||||
}
|
||||
|
||||
223
server/src/services/Media/MediaService.ts
Normal file
223
server/src/services/Media/MediaService.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import fs from 'fs';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ServiceError } from "exceptions";
|
||||
import { IMedia, IMediaService } from 'interfaces';
|
||||
import { difference } from 'lodash';
|
||||
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
const ERRORS = {
|
||||
MINETYPE_NOT_SUPPORTED: 'MINETYPE_NOT_SUPPORTED',
|
||||
MEDIA_NOT_FOUND: 'MEDIA_NOT_FOUND',
|
||||
MODEL_NAME_HAS_NO_MEDIA: 'MODEL_NAME_HAS_NO_MEDIA',
|
||||
MODEL_ID_NOT_FOUND: 'MODEL_ID_NOT_FOUND',
|
||||
MEDIA_IDS_NOT_FOUND: 'MEDIA_IDS_NOT_FOUND',
|
||||
MEDIA_LINK_EXISTS: 'MEDIA_LINK_EXISTS'
|
||||
}
|
||||
const publicPath = 'storage/app/public/';
|
||||
const attachmentsMimes = ['image/png', 'image/jpeg'];
|
||||
|
||||
@Service()
|
||||
export default class MediaService implements IMediaService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
/**
|
||||
* Retrieve media model or throw not found error
|
||||
* @param tenantId
|
||||
* @param mediaId
|
||||
*/
|
||||
async getMediaOrThrowError(tenantId: number, mediaId: number) {
|
||||
const { Media } = this.tenancy.models(tenantId);
|
||||
const foundMedia = await Media.query().findById(mediaId);
|
||||
|
||||
if (!foundMedia) {
|
||||
throw new ServiceError(ERRORS.MEDIA_NOT_FOUND);
|
||||
}
|
||||
return foundMedia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive media models by the given ids or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} mediaIds
|
||||
*/
|
||||
async getMediaByIdsOrThrowError(tenantId: number, mediaIds: number[]) {
|
||||
const { Media } = this.tenancy.models(tenantId);
|
||||
const foundMedia = await Media.query().whereIn('id', mediaIds);
|
||||
|
||||
const storedMediaIds = foundMedia.map((m) => m.id);
|
||||
const notFoundMedia = difference(mediaIds, storedMediaIds);
|
||||
|
||||
if (notFoundMedia.length > 0) {
|
||||
throw new ServiceError(ERRORS.MEDIA_IDS_NOT_FOUND);
|
||||
}
|
||||
return foundMedia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the model name and id.
|
||||
* @param {number} tenantId
|
||||
* @param {string} modelName
|
||||
* @param {number} modelId
|
||||
*/
|
||||
async validateModelNameAndIdExistance(tenantId: number, modelName: string, modelId: number) {
|
||||
const models = this.tenancy.models(tenantId);
|
||||
this.logger.info('[media] trying to validate model name and id.', { tenantId, modelName, modelId });
|
||||
|
||||
if (!models[modelName]) {
|
||||
this.logger.info('[media] model name not found.', { tenantId, modelName, modelId });
|
||||
throw new ServiceError(ERRORS.MODEL_NAME_HAS_NO_MEDIA);
|
||||
}
|
||||
if (!models[modelName].media) {
|
||||
this.logger.info('[media] model is not media-able.', { tenantId, modelName, modelId });
|
||||
throw new ServiceError(ERRORS.MODEL_NAME_HAS_NO_MEDIA);
|
||||
}
|
||||
|
||||
const foundModel = await models[modelName].query().findById(modelId);
|
||||
|
||||
if (!foundModel) {
|
||||
this.logger.info('[media] model is not found.', { tenantId, modelName, modelId });
|
||||
throw new ServiceError(ERRORS.MODEL_ID_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the media existance.
|
||||
* @param {number} tenantId
|
||||
* @param {number} mediaId
|
||||
* @param {number} modelId
|
||||
* @param {string} modelName
|
||||
*/
|
||||
async validateMediaLinkExistance(
|
||||
tenantId: number,
|
||||
mediaId: number,
|
||||
modelId: number,
|
||||
modelName: string
|
||||
) {
|
||||
const { MediaLink } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundMediaLinks = await MediaLink.query()
|
||||
.where('media_id', mediaId)
|
||||
.where('model_id', modelId)
|
||||
.where('model_name', modelName);
|
||||
|
||||
if (foundMediaLinks.length > 0) {
|
||||
throw new ServiceError(ERRORS.MEDIA_LINK_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given media to the specific media-able model resource.
|
||||
* @param {number} tenantId
|
||||
* @param {number} mediaId
|
||||
* @param {number} modelId
|
||||
* @param {string} modelType
|
||||
*/
|
||||
async linkMedia(tenantId: number, mediaId: number, modelId: number, modelName: string) {
|
||||
this.logger.info('[media] trying to link media.', { tenantId, mediaId, modelId, modelName });
|
||||
const { MediaLink } = this.tenancy.models(tenantId);
|
||||
await this.validateMediaLinkExistance(tenantId, mediaId, modelId, modelName);
|
||||
|
||||
const media = await this.getMediaOrThrowError(tenantId, mediaId);
|
||||
await this.validateModelNameAndIdExistance(tenantId, modelName, modelId);
|
||||
|
||||
await MediaLink.query().insert({ mediaId, modelId, modelName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve media metadata.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} mediaId - Media id.
|
||||
* @return {Promise<IMedia>}
|
||||
*/
|
||||
public async getMedia(tenantId: number, mediaId: number): Promise<IMedia> {
|
||||
this.logger.info('[media] try to get media.', { tenantId, mediaId });
|
||||
return this.getMediaOrThrowError(tenantId, mediaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given media.
|
||||
* @param {number} tenantId
|
||||
* @param {number} mediaId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteMedia(tenantId: number, mediaId: number|number[]): Promise<void> {
|
||||
const { Media, MediaLink } = this.tenancy.models(tenantId);
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
|
||||
this.logger.info('[media] trying to delete media.', { tenantId, mediaId });
|
||||
|
||||
const mediaIds = Array.isArray(mediaId) ? mediaId : [mediaId];
|
||||
|
||||
const tenant = await tenantRepository.getById(tenantId);
|
||||
const media = await this.getMediaByIdsOrThrowError(tenantId, mediaIds);
|
||||
|
||||
const tenantPath = `${publicPath}${tenant.organizationId}`;
|
||||
const unlinkOpers = [];
|
||||
|
||||
media.forEach((mediaModel) => {
|
||||
const oper = fsPromises.unlink(`${tenantPath}/${mediaModel.attachmentFile}`);
|
||||
unlinkOpers.push(oper);
|
||||
});
|
||||
await Promise.all(unlinkOpers)
|
||||
.then((resolved) => {
|
||||
resolved.forEach(() => {
|
||||
this.logger.info('[attachment] file has been deleted.');
|
||||
});
|
||||
})
|
||||
.catch((errors) => {
|
||||
this.logger.info('[attachment] Delete item attachment file delete failed.', { errors });
|
||||
});
|
||||
await MediaLink.query().whereIn('media_id', mediaIds).delete();
|
||||
await Media.query().whereIn('id', mediaIds).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the given attachment.
|
||||
* @param {number} tenantId -
|
||||
* @param {any} attachment -
|
||||
* @return {Promise<IMedia>}
|
||||
*/
|
||||
public async upload(tenantId: number, attachment: any, modelName?: string, modelId?: number): Promise<IMedia> {
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
const { Media } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[media] trying to upload media.', { tenantId });
|
||||
|
||||
const tenant = await tenantRepository.getById(tenantId);
|
||||
const fileName = `${attachment.md5}.png`;
|
||||
|
||||
// Validate the attachment.
|
||||
if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) {
|
||||
throw new ServiceError(ERRORS.MINETYPE_NOT_SUPPORTED);
|
||||
}
|
||||
if (modelName && modelId) {
|
||||
await this.validateModelNameAndIdExistance(tenantId, modelName, modelId);
|
||||
}
|
||||
try {
|
||||
await attachment.mv(`${publicPath}${tenant.organizationId}/${fileName}`);
|
||||
this.logger.info('[attachment] uploaded successfully');
|
||||
} catch (error) {
|
||||
this.logger.info('[attachment] uploading failed.', { error });
|
||||
}
|
||||
const media = await Media.query().insertGraph({
|
||||
attachmentFile: `${fileName}`,
|
||||
...(modelName && modelId) ? {
|
||||
links: [{
|
||||
modelName,
|
||||
modelId,
|
||||
}]
|
||||
} : {},
|
||||
});
|
||||
this.logger.info('[media] uploaded successfully.', { tenantId, fileName, modelName, modelId });
|
||||
return media;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { IBillPaymentOTD, IBillPayment } from 'interfaces';
|
||||
import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } 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 DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { formatDateFields } from 'utils';
|
||||
|
||||
/**
|
||||
@@ -25,6 +26,9 @@ export default class BillPaymentsService {
|
||||
@Inject()
|
||||
journalService: JournalPosterService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Creates a new bill payment transcations and store it to the storage
|
||||
* with associated bills entries and journal transactions.
|
||||
@@ -39,7 +43,7 @@ export default class BillPaymentsService {
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {BillPaymentDTO} billPayment - Bill payment object.
|
||||
*/
|
||||
async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) {
|
||||
public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) {
|
||||
const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
|
||||
|
||||
const billPayment = {
|
||||
@@ -102,7 +106,7 @@ export default class BillPaymentsService {
|
||||
* @param {BillPaymentDTO} billPayment
|
||||
* @param {IBillPayment} oldBillPayment
|
||||
*/
|
||||
async editBillPayment(
|
||||
public async editBillPayment(
|
||||
tenantId: number,
|
||||
billPaymentId: number,
|
||||
billPaymentDTO,
|
||||
@@ -171,7 +175,7 @@ export default class BillPaymentsService {
|
||||
* @param {Integer} billPaymentId - The given bill payment id.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async deleteBillPayment(tenantId: number, billPaymentId: number) {
|
||||
public async deleteBillPayment(tenantId: number, billPaymentId: number) {
|
||||
const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
|
||||
const billPayment = await BillPayment.query().where('id', billPaymentId).first();
|
||||
|
||||
@@ -203,7 +207,7 @@ export default class BillPaymentsService {
|
||||
* @param {BillPayment} billPayment
|
||||
* @param {Integer} billPaymentId
|
||||
*/
|
||||
async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
|
||||
private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
|
||||
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentAmount = sumBy(billPayment.entries, 'payment_amount');
|
||||
@@ -252,6 +256,35 @@ export default class BillPaymentsService {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve bill payment paginted and filterable list.
|
||||
* @param {number} tenantId
|
||||
* @param {IBillPaymentsFilter} billPaymentsFilter
|
||||
*/
|
||||
public async listBillPayments(
|
||||
tenantId: number,
|
||||
billPaymentsFilter: IBillPaymentsFilter,
|
||||
): Promise<{ billPayments: IBillPayment, pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
const { BillPayment } = this.tenancy.models(tenantId);
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, BillPayment, billPaymentsFilter);
|
||||
|
||||
this.logger.info('[bill_payment] try to get bill payments list.', { tenantId });
|
||||
const { results, pagination } = await BillPayment.query().onBuild(builder => {
|
||||
builder.withGraphFetched('vendor');
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(
|
||||
billPaymentsFilter.page - 1,
|
||||
billPaymentsFilter.pageSize,
|
||||
);
|
||||
|
||||
return {
|
||||
billPayments: results,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve bill payment with associated metadata.
|
||||
* @param {number} billPaymentId - The bill payment id.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
export default class ResourceService {
|
||||
|
||||
}
|
||||
78
server/src/services/Resource/ResourceService.ts
Normal file
78
server/src/services/Resource/ResourceService.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { camelCase, upperFirst } from 'lodash'
|
||||
import { IModel } from 'interfaces';
|
||||
import resourceFieldsKeys from 'data/ResourceFieldsKeys';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class ResourceService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} resourceName
|
||||
*/
|
||||
getResourceFieldsRelations(modelName: string) {
|
||||
const fieldsRelations = resourceFieldsKeys[modelName];
|
||||
|
||||
if (!fieldsRelations) {
|
||||
throw new Error('Fields relation not found in thte given resource model.');
|
||||
}
|
||||
return fieldsRelations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform resource to model name.
|
||||
* @param {string} resourceName
|
||||
*/
|
||||
private resourceToModelName(resourceName: string): string {
|
||||
return upperFirst(camelCase(resourceName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve model from resource name in specific tenant.
|
||||
* @param {number} tenantId
|
||||
* @param {string} resourceName
|
||||
*/
|
||||
public getModel(tenantId: number, resourceName: string) {
|
||||
const models = this.tenancy.models(tenantId);
|
||||
const modelName = this.resourceToModelName(resourceName);
|
||||
|
||||
return models[modelName];
|
||||
}
|
||||
|
||||
getModelFields(Model: IModel) {
|
||||
const fields = Object.keys(Model.fields);
|
||||
|
||||
return fields.sort((a, b) => {
|
||||
if (a < b) { return -1; }
|
||||
if (a > b) { return 1; }
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} resourceName
|
||||
*/
|
||||
getResourceFields(Model: IModel) {
|
||||
console.log(Model);
|
||||
|
||||
if (Model.resourceable) {
|
||||
return this.getModelFields(Model);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} resourceName
|
||||
*/
|
||||
getResourceColumns(Model: IModel) {
|
||||
if (Model.resourceable) {
|
||||
return this.getModelFields(Model);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export default class JournalPosterService {
|
||||
* @param {string} referenceType - The transaction reference type.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async deleteJournalTransactions(
|
||||
async revertJournalTransactions(
|
||||
tenantId: number,
|
||||
referenceId: number,
|
||||
referenceType: string
|
||||
|
||||
@@ -10,6 +10,7 @@ import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
|
||||
import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository';
|
||||
import CustomerRepository from 'repositories/CustomerRepository';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { formatDateFields } from 'utils';
|
||||
|
||||
/**
|
||||
@@ -27,6 +28,9 @@ export default class PaymentReceiveService {
|
||||
@Inject()
|
||||
journalService: JournalPosterService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@@ -37,7 +41,7 @@ export default class PaymentReceiveService {
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
*/
|
||||
async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) {
|
||||
public async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) {
|
||||
const {
|
||||
PaymentReceive,
|
||||
PaymentReceiveEntry,
|
||||
@@ -107,7 +111,7 @@ export default class PaymentReceiveService {
|
||||
* @param {IPaymentReceive} paymentReceive -
|
||||
* @param {IPaymentReceive} oldPaymentReceive -
|
||||
*/
|
||||
async editPaymentReceive(
|
||||
public async editPaymentReceive(
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
paymentReceive: any,
|
||||
@@ -242,7 +246,7 @@ export default class PaymentReceiveService {
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {Integer} paymentReceiveId - Payment receive id.
|
||||
*/
|
||||
async getPaymentReceive(tenantId: number, paymentReceiveId: number) {
|
||||
public async getPaymentReceive(tenantId: number, paymentReceiveId: number) {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
const paymentReceive = await PaymentReceive.query()
|
||||
.where('id', paymentReceiveId)
|
||||
@@ -250,6 +254,30 @@ export default class PaymentReceiveService {
|
||||
.first();
|
||||
return paymentReceive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payment receives paginated and filterable list.
|
||||
* @param {number} tenantId
|
||||
* @param {IPaymentReceivesFilter} paymentReceivesFilter
|
||||
*/
|
||||
public async listPaymentReceives(tenantId: number, paymentReceivesFilter: IPaymentReceivesFilter) {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter);
|
||||
|
||||
const { results, pagination } = await PaymentReceive.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('customer');
|
||||
builder.withGraphFetched('depositAccount');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(
|
||||
paymentReceivesFilter.page - 1,
|
||||
paymentReceivesFilter.pageSize,
|
||||
);
|
||||
return {
|
||||
paymentReceives: results,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the payment receive details with associated invoices.
|
||||
@@ -310,7 +338,7 @@ export default class PaymentReceiveService {
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @param {Number} paymentReceiveId
|
||||
*/
|
||||
async recordPaymentReceiveJournalEntries(
|
||||
private async recordPaymentReceiveJournalEntries(
|
||||
tenantId: number,
|
||||
paymentReceive: any,
|
||||
paymentReceiveId?: number
|
||||
@@ -370,7 +398,7 @@ export default class PaymentReceiveService {
|
||||
* @param {Array} revertInvoices
|
||||
* @return {Promise}
|
||||
*/
|
||||
async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) {
|
||||
private async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const opers: Promise<T>[] = [];
|
||||
|
||||
@@ -392,7 +420,7 @@ export default class PaymentReceiveService {
|
||||
* @param {Array} newPaymentReceiveEntries
|
||||
* @return
|
||||
*/
|
||||
async saveChangeInvoicePaymentAmount(
|
||||
private async saveChangeInvoicePaymentAmount(
|
||||
tenantId: number,
|
||||
paymentReceiveEntries: [],
|
||||
newPaymentReceiveEntries: [],
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { omit, difference, sumBy, mixin } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import { formatDateFields } from 'utils';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
|
||||
/**
|
||||
* Sale estimate service.
|
||||
@@ -19,6 +21,9 @@ export default class SaleEstimateService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Creates a new estimate with associated entries.
|
||||
* @async
|
||||
@@ -208,4 +213,32 @@ export default class SaleEstimateService {
|
||||
});
|
||||
return foundEstimates.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves estimates filterable and paginated list.
|
||||
* @param {number} tenantId
|
||||
* @param {IEstimatesFilter} estimatesFilter
|
||||
*/
|
||||
public async estimatesList(
|
||||
tenantId: number,
|
||||
estimatesFilter: IEstimatesFilter
|
||||
): Promise<{ salesEstimates: ISaleEstimate[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleEstimate, estimatesFilter);
|
||||
|
||||
const { results, pagination } = await SaleEstimate.query().onBuild(builder => {
|
||||
builder.withGraphFetched('customer');
|
||||
builder.withGraphFetched('entries');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(
|
||||
estimatesFilter.page - 1,
|
||||
estimatesFilter.pageSize,
|
||||
);
|
||||
|
||||
return {
|
||||
salesEstimates: results,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { omit, sumBy, difference, pick, chain } from 'lodash';
|
||||
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from 'interfaces';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } 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 DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { formatDateFields } from 'utils';
|
||||
|
||||
/**
|
||||
@@ -26,6 +31,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Creates a new sale invoices and store it to the storage
|
||||
* with associated to entries and journal transactions.
|
||||
@@ -34,7 +45,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @param {ISaleInvoice} saleInvoiceDTO -
|
||||
* @return {ISaleInvoice}
|
||||
*/
|
||||
async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) {
|
||||
public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) {
|
||||
const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
@@ -94,7 +105,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @param {Number} saleInvoiceId -
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
*/
|
||||
async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) {
|
||||
public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) {
|
||||
const { SaleInvoice, ItemEntry, Customer } = this.tenancy.models(tenantId);
|
||||
|
||||
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
@@ -152,7 +163,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @async
|
||||
* @param {Number} saleInvoiceId - The given sale invoice id.
|
||||
*/
|
||||
async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) {
|
||||
public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) {
|
||||
const {
|
||||
SaleInvoice,
|
||||
ItemEntry,
|
||||
@@ -215,7 +226,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @param {number} saleInvoiceId -
|
||||
* @param {boolean} override -
|
||||
*/
|
||||
recordInventoryTranscactions(
|
||||
private recordInventoryTranscactions(
|
||||
tenantId: number,
|
||||
saleInvoice,
|
||||
saleInvoiceId: number,
|
||||
@@ -243,7 +254,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @param {string} transactionType
|
||||
* @param {number} transactionId
|
||||
*/
|
||||
async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
|
||||
private async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
|
||||
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
||||
const opers: Promise<[]>[] = [];
|
||||
|
||||
@@ -280,7 +291,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @async
|
||||
* @param {Number} saleInvoiceId
|
||||
*/
|
||||
async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) {
|
||||
public async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
return SaleInvoice.query()
|
||||
.where('id', saleInvoiceId)
|
||||
@@ -405,4 +416,27 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sales invoices filterable and paginated list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public async salesInvoicesList(tenantId: number, salesInvoicesFilter: ISalesInvoicesFilter):
|
||||
Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter);
|
||||
|
||||
this.logger.info('[sale_invoice] try to get sales invoices list.', { tenantId, salesInvoicesFilter });
|
||||
const { results, pagination } = await SaleInvoice.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('entries');
|
||||
builder.withGraphFetched('customer');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(
|
||||
salesInvoicesFilter.page - 1,
|
||||
salesInvoicesFilter.pageSize,
|
||||
);
|
||||
return { salesInvoices: results, pagination, filterMeta: dynamicFilter.getResponseMeta() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,17 @@ import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import HasItemEntries from 'services/Sales/HasItemsEntries';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
import { IFilterMeta, IPaginationMeta } from 'interfaces';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
|
||||
@Service()
|
||||
export default class SalesReceiptService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
journalService: JournalPosterService;
|
||||
|
||||
@@ -22,7 +27,7 @@ export default class SalesReceiptService {
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
* @return {Object}
|
||||
*/
|
||||
async createSaleReceipt(tenantId: number, saleReceiptDTO: any) {
|
||||
public async createSaleReceipt(tenantId: number, saleReceiptDTO: any) {
|
||||
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
@@ -55,7 +60,7 @@ export default class SalesReceiptService {
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
* @return {void}
|
||||
*/
|
||||
async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) {
|
||||
public async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) {
|
||||
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
@@ -88,7 +93,7 @@ export default class SalesReceiptService {
|
||||
* @param {Integer} saleReceiptId
|
||||
* @return {void}
|
||||
*/
|
||||
async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
|
||||
public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
|
||||
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
|
||||
const deleteSaleReceiptOper = SaleReceipt.query()
|
||||
.where('id', saleReceiptId)
|
||||
@@ -160,4 +165,35 @@ export default class SalesReceiptService {
|
||||
|
||||
return saleReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sales receipts paginated and filterable list.
|
||||
* @param {number} tenantId
|
||||
* @param {ISaleReceiptFilter} salesReceiptsFilter
|
||||
*/
|
||||
public async salesReceiptsList(
|
||||
tenantId: number,
|
||||
salesReceiptsFilter: ISaleReceiptFilter,
|
||||
): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleReceipt, salesReceiptsFilter);
|
||||
|
||||
this.logger.info('[sale_receipt] try to get sales receipts list.', { tenantId });
|
||||
const { results, pagination } = await SaleReceipt.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('depositAccount');
|
||||
builder.withGraphFetched('customer');
|
||||
builder.withGraphFetched('entries');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(
|
||||
salesReceiptsFilter.page - 1,
|
||||
salesReceiptsFilter.pageSize,
|
||||
);
|
||||
|
||||
return {
|
||||
salesReceipts: results,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ export default class HasTenancyService {
|
||||
singletonService(tenantId: number, key: string, callback: Function) {
|
||||
const container = this.tenantContainer(tenantId);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
const hasServiceInstnace = container.has(key);
|
||||
|
||||
if (!hasServiceInstnace) {
|
||||
@@ -74,12 +73,24 @@ export default class HasTenancyService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets i18n locals function.
|
||||
* @param {number} tenantId
|
||||
* @param locals
|
||||
*/
|
||||
setI18nLocals(tenantId: number, locals: any) {
|
||||
return this.singletonService(tenantId, 'i18n', () => {
|
||||
return locals;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve i18n locales methods.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
*/
|
||||
i18n(tenantId: number) {
|
||||
return this.singletonService(tenantId, 'i18n', () => {
|
||||
throw new Error('I18n locals is not set yet.');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import { pick, difference } from 'lodash';
|
||||
import { difference } from 'lodash';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import {
|
||||
IViewsService,
|
||||
IViewDTO,
|
||||
IView,
|
||||
IViewRole,
|
||||
IViewHasColumn,
|
||||
IViewEditDTO,
|
||||
} from 'interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import ResourceService from "services/Resource/ResourceService";
|
||||
import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder';
|
||||
|
||||
const ERRORS = {
|
||||
VIEW_NOT_FOUND: 'VIEW_NOT_FOUND',
|
||||
VIEW_PREDEFINED: 'VIEW_PREDEFINED',
|
||||
INVALID_LOGIC_EXPRESSION: 'INVALID_LOGIC_EXPRESSION',
|
||||
VIEW_NAME_NOT_UNIQUE: 'VIEW_NAME_NOT_UNIQUE',
|
||||
LOGIC_EXPRESSION_INVALID: 'INVALID_LOGIC_EXPRESSION',
|
||||
RESOURCE_FIELDS_KEYS_NOT_FOUND: 'RESOURCE_FIELDS_KEYS_NOT_FOUND',
|
||||
RESOURCE_COLUMNS_KEYS_NOT_FOUND: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND',
|
||||
RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND'
|
||||
};
|
||||
|
||||
@Service()
|
||||
@@ -25,29 +29,131 @@ export default class ViewsService implements IViewsService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
resourceService: ResourceService;
|
||||
|
||||
/**
|
||||
* Listing resource views.
|
||||
* @param {number} tenantId
|
||||
* @param {string} resourceModel
|
||||
* @param {number} tenantId -
|
||||
* @param {string} resourceModel -
|
||||
*/
|
||||
public async listViews(tenantId: number, resourceModel: string) {
|
||||
const { View } = this.tenancy.models(tenantId);
|
||||
return View.query().where('resource_model', resourceModel);
|
||||
}
|
||||
public async listResourceViews(tenantId: number, resourceModel: string): Promise<IView[]> {
|
||||
this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel });
|
||||
|
||||
validateResourceFieldsExistance() {
|
||||
|
||||
}
|
||||
|
||||
validateResourceColumnsExistance() {
|
||||
|
||||
}
|
||||
|
||||
getView(tenantId: number, viewId: number) {
|
||||
// Validate the resource model name is valid.
|
||||
this.getResourceModelOrThrowError(tenantId, resourceModel);
|
||||
|
||||
const { viewRepository } = this.tenancy.repositories(tenantId);
|
||||
return viewRepository.allByResource(resourceModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate model resource conditions fields existance.
|
||||
* @param {string} resourceName
|
||||
* @param {IViewRoleDTO[]} viewRoles
|
||||
*/
|
||||
private validateResourceRolesFieldsExistance(ResourceModel: IModel, viewRoles: IViewRoleDTO[]) {
|
||||
const resourceFieldsKeys = this.resourceService.getResourceFields(ResourceModel);
|
||||
|
||||
const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey);
|
||||
const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys);
|
||||
|
||||
if (notFoundFieldsKeys.length > 0) {
|
||||
throw new ServiceError(ERRORS.RESOURCE_FIELDS_KEYS_NOT_FOUND);
|
||||
}
|
||||
return notFoundFieldsKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates model resource columns existance.
|
||||
* @param {string} resourceName
|
||||
* @param {IViewColumnDTO[]} viewColumns
|
||||
*/
|
||||
private validateResourceColumnsExistance(ResourceModel: IModel, viewColumns: IViewColumnDTO[]) {
|
||||
const resourceFieldsKeys = this.resourceService.getResourceColumns(ResourceModel);
|
||||
|
||||
const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey);
|
||||
const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys);
|
||||
|
||||
if (notFoundFieldsKeys.length > 0) {
|
||||
throw new ServiceError(ERRORS.RESOURCE_COLUMNS_KEYS_NOT_FOUND);
|
||||
}
|
||||
return notFoundFieldsKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given view details with associated conditions and columns.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} viewId - View id.
|
||||
*/
|
||||
public getView(tenantId: number, viewId: number): Promise<IView> {
|
||||
this.logger.info('[view] trying to get view from storage.', { tenantId, viewId });
|
||||
return this.getViewOrThrowError(tenantId, viewId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve view or throw not found error.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} viewId - View id.
|
||||
*/
|
||||
private async getViewOrThrowError(tenantId: number, viewId: number): Promise<IView> {
|
||||
const { viewRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[view] trying to get view from storage.', { tenantId, viewId });
|
||||
const view = await viewRepository.getById(viewId);
|
||||
|
||||
if (!view) {
|
||||
this.logger.info('[view] view not found.', { tenantId, viewId });
|
||||
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve resource model from resource name or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {number} resourceModel
|
||||
*/
|
||||
private getResourceModelOrThrowError(tenantId: number, resourceModel: string): IModel {
|
||||
const ResourceModel = this.resourceService.getModel(tenantId, resourceModel);
|
||||
|
||||
if (!ResourceModel || !ResourceModel.resourceable) {
|
||||
throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND);
|
||||
}
|
||||
return ResourceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates view name uniqiness in the given resource.
|
||||
* @param {number} tenantId
|
||||
* @param {stirng} resourceModel
|
||||
* @param {string} viewName
|
||||
* @param {number} notViewId
|
||||
*/
|
||||
private async validateViewNameUniquiness(
|
||||
tenantId: number,
|
||||
resourceModel: string,
|
||||
viewName: string,
|
||||
notViewId?: number
|
||||
): void {
|
||||
const { View } = this.tenancy.models(tenantId);
|
||||
const foundViews = await View.query()
|
||||
.where('resource_model', resourceModel)
|
||||
.where('name', viewName)
|
||||
.onBuild((builder) => {
|
||||
if (notViewId) {
|
||||
builder.whereNot('id', notViewId);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundViews.length > 0) {
|
||||
throw new ServiceError(ERRORS.VIEW_NAME_NOT_UNIQUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new custom view to specific resource.
|
||||
* ----––––––
|
||||
* Precedures.
|
||||
* ----––––––
|
||||
* - Validate resource fields existance.
|
||||
@@ -60,116 +166,78 @@ export default class ViewsService implements IViewsService {
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IViewDTO} viewDTO - View DTO.
|
||||
*/
|
||||
async newView(tenantId: number, viewDTO: IViewDTO): Promise<void> {
|
||||
const { View, ViewColumn, ViewRole } = this.tenancy.models(tenantId);
|
||||
|
||||
public async newView(tenantId: number, viewDTO: IViewDTO): Promise<IView> {
|
||||
const { viewRepository } = this.tenancy.repositories(tenantId);
|
||||
this.logger.info('[views] trying to create a new view.', { tenantId, viewDTO });
|
||||
|
||||
// Validate the resource name is exists and resourcable.
|
||||
const ResourceModel = this.getResourceModelOrThrowError(tenantId, viewDTO.resourceModel);
|
||||
|
||||
// Validate view name uniquiness.
|
||||
await this.validateViewNameUniquiness(tenantId, viewDTO.resourceModel, viewDTO.name);
|
||||
|
||||
// Validate the given fields keys exist on the storage.
|
||||
this.validateResourceRolesFieldsExistance(ResourceModel, viewDTO.roles);
|
||||
|
||||
// Validate the given columnable fields keys exists on the storage.
|
||||
this.validateResourceColumnsExistance(ResourceModel, viewDTO.columns);
|
||||
|
||||
// Validates the view conditional logic expression.
|
||||
if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) {
|
||||
throw new ServiceError(ERRORS.INVALID_LOGIC_EXPRESSION);
|
||||
throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID);
|
||||
}
|
||||
// Save view details.
|
||||
const view = await View.query().insert({
|
||||
name: viewDTO.name,
|
||||
const view = await viewRepository.insert({
|
||||
predefined: false,
|
||||
name: viewDTO.name,
|
||||
rolesLogicExpression: viewDTO.logicExpression,
|
||||
resourceModel: viewDTO.resourceModel,
|
||||
roles: viewDTO.roles,
|
||||
columns: viewDTO.columns,
|
||||
});
|
||||
this.logger.info('[views] inserted to the storage.', { tenantId, viewDTO });
|
||||
|
||||
// Save view roles async operations.
|
||||
const saveViewRolesOpers = [];
|
||||
|
||||
viewDTO.roles.forEach((role) => {
|
||||
const saveViewRoleOper = ViewRole.query().insert({
|
||||
...pick(role, ['fieldKey', 'comparator', 'value', 'index']),
|
||||
viewId: view.id,
|
||||
});
|
||||
saveViewRolesOpers.push(saveViewRoleOper);
|
||||
});
|
||||
|
||||
viewDTO.columns.forEach((column) => {
|
||||
const saveViewColumnOper = ViewColumn.query().insert({
|
||||
viewId: view.id,
|
||||
index: column.index,
|
||||
});
|
||||
saveViewRolesOpers.push(saveViewColumnOper);
|
||||
});
|
||||
this.logger.info('[views] roles and columns inserted to the storage.', { tenantId, viewDTO });
|
||||
|
||||
await Promise.all(saveViewRolesOpers);
|
||||
this.logger.info('[views] inserted to the storage successfully.', { tenantId, viewDTO });
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits view details, roles and columns on the storage.
|
||||
* --------
|
||||
* Precedures.
|
||||
* --------
|
||||
* - Validate view existance.
|
||||
* - Validate view resource fields existance.
|
||||
* - Validate view resource columns existance.
|
||||
* - Validate view logic expression.
|
||||
* - Delete old view columns and roles.
|
||||
* - Re-save view columns and roles.
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} viewId
|
||||
* @param {IViewEditDTO}
|
||||
*/
|
||||
async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO) {
|
||||
const { View, ViewRole, ViewColumn } = req.models;
|
||||
const view = await View.query().where('id', viewId)
|
||||
.withGraphFetched('roles.field')
|
||||
.withGraphFetched('columns')
|
||||
.first();
|
||||
public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise<void> {
|
||||
const { View } = this.tenancy.models(tenantId);
|
||||
this.logger.info('[view] trying to edit custom view.', { tenantId, viewId });
|
||||
|
||||
const errorReasons = [];
|
||||
const fieldsSlugs = viewEditDTO.roles.map((role) => role.field_key);
|
||||
const resourceFieldsKeys = resource.fields.map((f) => f.key);
|
||||
const resourceFieldsKeysMap = new Map(resource.fields.map((field) => [field.key, field]));
|
||||
const columnsKeys = viewEditDTO.columns.map((c) => c.key);
|
||||
// Retrieve view details or throw not found error.
|
||||
const view = await this.getViewOrThrowError(tenantId, viewId);
|
||||
|
||||
// The difference between the stored resource fields and submit fields keys.
|
||||
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
|
||||
// Validate the resource name is exists and resourcable.
|
||||
const ResourceModel = this.getResourceModelOrThrowError(tenantId, view.resourceModel);
|
||||
|
||||
// Validate not found resource fields keys.
|
||||
if (notFoundFields.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields,
|
||||
});
|
||||
}
|
||||
// The difference between the stored resource fields and the submit columns keys.
|
||||
const notFoundColumns = difference(columnsKeys, resourceFieldsKeys);
|
||||
// Validate view name uniquiness.
|
||||
await this.validateViewNameUniquiness(tenantId, view.resourceModel, viewEditDTO.name, viewId);
|
||||
|
||||
// Validate the given fields keys exist on the storage.
|
||||
this.validateResourceRolesFieldsExistance(ResourceModel, view.roles);
|
||||
|
||||
// Validate the given columnable fields keys exists on the storage.
|
||||
this.validateResourceColumnsExistance(ResourceModel, view.columns);
|
||||
|
||||
// Validate not found view columns.
|
||||
if (notFoundColumns.length > 0) {
|
||||
errorReasons.push({ type: 'RESOURCE_COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns });
|
||||
}
|
||||
// Validates the view conditional logic expression.
|
||||
if (!validateViewRoles(viewEditDTO.roles, viewEditDTO.logicExpression)) {
|
||||
errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) {
|
||||
throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID);
|
||||
}
|
||||
|
||||
const viewRolesIds = view.roles.map((r) => r.id);
|
||||
const viewColumnsIds = view.columns.map((c) => c.id);
|
||||
|
||||
const formUpdatedRoles = viewEditDTO.roles.filter((r) => r.id);
|
||||
const formInsertRoles = viewEditDTO.roles.filter((r) => !r.id);
|
||||
|
||||
const formRolesIds = formUpdatedRoles.map((r) => r.id);
|
||||
|
||||
const formUpdatedColumns = viewEditDTO.columns.filter((r) => r.id);
|
||||
const formInsertedColumns = viewEditDTO.columns.filter((r) => !r.id);
|
||||
const formColumnsIds = formUpdatedColumns.map((r) => r.id);
|
||||
|
||||
const rolesIdsShouldDeleted = difference(viewRolesIds, formRolesIds);
|
||||
const columnsIdsShouldDelete = difference(viewColumnsIds, formColumnsIds);
|
||||
|
||||
const notFoundViewRolesIds = difference(formRolesIds, viewRolesIds);
|
||||
const notFoundViewColumnsIds = difference(viewColumnsIds, viewColumnsIds);
|
||||
|
||||
// Validate the not found view roles ids.
|
||||
if (notFoundViewRolesIds.length) {
|
||||
errorReasons.push({ type: 'VIEW.ROLES.IDS.NOT.FOUND', code: 500, ids: notFoundViewRolesIds });
|
||||
}
|
||||
// Validate the not found view columns ids.
|
||||
if (notFoundViewColumnsIds.length) {
|
||||
errorReasons.push({ type: 'VIEW.COLUMNS.IDS.NOT.FOUND', code: 600, ids: notFoundViewColumnsIds });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const asyncOpers = [];
|
||||
|
||||
// Save view details.
|
||||
await View.query()
|
||||
.where('id', view.id)
|
||||
@@ -177,78 +245,15 @@ export default class ViewsService implements IViewsService {
|
||||
name: viewEditDTO.name,
|
||||
roles_logic_expression: viewEditDTO.logicExpression,
|
||||
});
|
||||
|
||||
// Update view roles.
|
||||
if (formUpdatedRoles.length > 0) {
|
||||
formUpdatedRoles.forEach((role) => {
|
||||
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
|
||||
const updateOper = ViewRole.query()
|
||||
.where('id', role.id)
|
||||
.update({
|
||||
...pick(role, ['comparator', 'value', 'index']),
|
||||
field_id: fieldModel.id,
|
||||
});
|
||||
asyncOpers.push(updateOper);
|
||||
});
|
||||
}
|
||||
// Insert a new view roles.
|
||||
if (formInsertRoles.length > 0) {
|
||||
formInsertRoles.forEach((role) => {
|
||||
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
|
||||
const insertOper = ViewRole.query()
|
||||
.insert({
|
||||
...pick(role, ['comparator', 'value', 'index']),
|
||||
field_id: fieldModel.id,
|
||||
view_id: view.id,
|
||||
});
|
||||
asyncOpers.push(insertOper);
|
||||
});
|
||||
}
|
||||
// Delete view roles.
|
||||
if (rolesIdsShouldDeleted.length > 0) {
|
||||
const deleteOper = ViewRole.query()
|
||||
.whereIn('id', rolesIdsShouldDeleted)
|
||||
.delete();
|
||||
asyncOpers.push(deleteOper);
|
||||
}
|
||||
// Insert a new view columns to the storage.
|
||||
if (formInsertedColumns.length > 0) {
|
||||
formInsertedColumns.forEach((column) => {
|
||||
const fieldModel = resourceFieldsKeysMap.get(column.key);
|
||||
const insertOper = ViewColumn.query()
|
||||
.insert({
|
||||
field_id: fieldModel.id,
|
||||
index: column.index,
|
||||
view_id: view.id,
|
||||
});
|
||||
asyncOpers.push(insertOper);
|
||||
});
|
||||
}
|
||||
// Update the view columns on the storage.
|
||||
if (formUpdatedColumns.length > 0) {
|
||||
formUpdatedColumns.forEach((column) => {
|
||||
const updateOper = ViewColumn.query()
|
||||
.where('id', column.id)
|
||||
.update({
|
||||
index: column.index,
|
||||
});
|
||||
asyncOpers.push(updateOper);
|
||||
});
|
||||
}
|
||||
// Delete the view columns from the storage.
|
||||
if (columnsIdsShouldDelete.length > 0) {
|
||||
const deleteOper = ViewColumn.query()
|
||||
.whereIn('id', columnsIdsShouldDelete)
|
||||
.delete();
|
||||
asyncOpers.push(deleteOper);
|
||||
}
|
||||
await Promise.all(asyncOpers);
|
||||
this.logger.info('[view] edited successfully.', { tenantId, viewId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve views details of the given id or throw not found error.
|
||||
* @private
|
||||
* @param {number} tenantId
|
||||
* @param {number} viewId
|
||||
* @return {Promise<IView>}
|
||||
*/
|
||||
private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise<IView> {
|
||||
const { View } = this.tenancy.models(tenantId);
|
||||
@@ -267,6 +272,7 @@ export default class ViewsService implements IViewsService {
|
||||
* Deletes the given view with associated roles and columns.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} viewId - View id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteView(tenantId: number, viewId: number): Promise<void> {
|
||||
const { View } = this.tenancy.models(tenantId);
|
||||
|
||||
Reference in New Issue
Block a user