add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
import { isNull } from 'lodash';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
import { IContact } from '@/interfaces';
export default class ContactTransfromer extends Transformer {
/**
* Retrieve formatted expense amount.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedBalance = (contact: IContact): string => {
return formatNumber(contact.balance, {
currencyCode: contact.currencyCode,
});
};
/**
* Retrieve formatted expense landed cost amount.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedOpeningBalance = (contact: IContact): string => {
return !isNull(contact.openingBalance)
? formatNumber(contact.openingBalance, {
currencyCode: contact.currencyCode,
})
: '';
};
/**
* Retriecve fromatted date.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedOpeningBalanceAt = (contact: IContact): string => {
return !isNull(contact.openingBalanceAt)
? this.formatDate(contact.openingBalanceAt)
: '';
};
}

View File

@@ -0,0 +1,378 @@
import { Inject, Service } from 'typedi';
import { difference, upperFirst, omit } from 'lodash';
import moment from 'moment';
import * as R from 'ramda';
import { Knex } from 'knex';
import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import {
IContact,
IContactNewDTO,
IContactEditDTO,
IContactsAutoCompleteFilter,
} from '@/interfaces';
import JournalPoster from '../Accounting/JournalPoster';
import { ERRORS } from './constants';
type TContactService = 'customer' | 'vendor';
@Service()
export default class ContactsService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
@Inject('logger')
logger: any;
/**
* Get the given contact or throw not found contact.
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @return {Promise<IContact>}
*/
public async getContactByIdOrThrowError(
tenantId: number,
contactId: number,
contactService?: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contact = await contactRepository.findOne({
id: contactId,
...(contactService && { contactService }),
});
if (!contact) {
throw new ServiceError('contact_not_found');
}
return contact;
}
/**
* Converts contact DTO object to model object attributes to insert or update.
* @param {IContactNewDTO | IContactEditDTO} contactDTO
*/
private commonTransformContactObj(
contactDTO: IContactNewDTO | IContactEditDTO
) {
return {
...omit(contactDTO, [
'billingAddress1',
'billingAddress2',
'shippingAddress1',
'shippingAddress2',
]),
billing_address_1: contactDTO?.billingAddress1,
billing_address_2: contactDTO?.billingAddress2,
shipping_address_1: contactDTO?.shippingAddress1,
shipping_address_2: contactDTO?.shippingAddress2,
};
}
/**
* Transforms contact new DTO object to model object to insert to the storage.
* @param {IContactNewDTO} contactDTO
*/
private transformNewContactDTO(contactDTO: IContactNewDTO) {
const baseCurrency = 'USD';
const currencyCode =
typeof contactDTO.currencyCode !== 'undefined'
? contactDTO.currencyCode
: baseCurrency;
return {
...this.commonTransformContactObj(contactDTO),
...(currencyCode ? { currencyCode } : {}),
};
}
/**
* Transforms contact edit DTO object to model object to update to the storage.
* @param {IContactEditDTO} contactDTO
*/
private transformEditContactDTO(contactDTO: IContactEditDTO) {
return {
...this.commonTransformContactObj(contactDTO),
};
}
/**
* Creates a new contact on the storage.
* @param {number} tenantId
* @param {TContactService} contactService
* @param {IContactDTO} contactDTO
*/
async newContact(
tenantId: number,
contactDTO: IContactNewDTO,
contactService: TContactService,
trx?: Knex.Transaction
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformNewContactDTO(contactDTO);
const contact = await contactRepository.create(
{
contactService,
...contactObj,
},
trx
);
return contact;
}
/**
* Edit details of the given on the storage.
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @param {IContactDTO} contactDTO
*/
async editContact(
tenantId: number,
contactId: number,
contactDTO: IContactEditDTO,
contactService: TContactService,
trx?: Knex.Transaction
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformEditContactDTO(contactDTO);
// Retrieve the given contact by id or throw not found service error.
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
return contactRepository.update({ ...contactObj }, { id: contactId }, trx);
}
/**
* Deletes the given contact from the storage.
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @return {Promise<void>}
*/
async deleteContact(
tenantId: number,
contactId: number,
contactService: TContactService,
trx?: Knex.Transaction
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
// Deletes contact of the given id.
await contactRepository.deleteById(contactId, trx);
}
/**
* Get contact details of the given contact id.
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @returns {Promise<IContact>}
*/
async getContact(
tenantId: number,
contactId: number,
contactService?: TContactService
) {
return this.getContactByIdOrThrowError(tenantId, contactId, contactService);
}
/**
* Parsees accounts list filter DTO.
* @param filterDTO
*/
private parseAutocompleteListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve auto-complete contacts list.
* @param {number} tenantId -
* @param {IContactsAutoCompleteFilter} contactsFilter -
* @return {IContactAutoCompleteItem}
*/
async autocompleteContacts(
tenantId: number,
query: IContactsAutoCompleteFilter
) {
const { Contact } = this.tenancy.models(tenantId);
// Parses auto-complete list filter DTO.
const filter = this.parseAutocompleteListFilterDTO(query);
// Dynamic list.
// const dynamicList = await this.dynamicListService.dynamicList(
// tenantId,
// Contact,
// filter
// );
// Retrieve contacts list by the given query.
const contacts = await Contact.query().onBuild((builder) => {
if (filter.keyword) {
builder.where('display_name', 'LIKE', `%${filter.keyword}%`);
}
// dynamicList.buildQuery()(builder);
builder.limit(filter.limit);
});
return contacts;
}
/**
* Retrieve contacts or throw not found error if one of ids were not found
* on the storage.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
* @return {Promise<IContact>}
*/
async getContactsOrThrowErrorNotFound(
tenantId: number,
contactsIds: number[],
contactService: TContactService
) {
const { Contact } = this.tenancy.models(tenantId);
const contacts = await Contact.query()
.whereIn('id', contactsIds)
.where('contact_service', contactService);
const storedContactsIds = contacts.map((contact: IContact) => contact.id);
const notFoundCustomers = difference(contactsIds, storedContactsIds);
if (notFoundCustomers.length > 0) {
throw new ServiceError('contacts_not_found');
}
return contacts;
}
/**
* Deletes the given contacts in bulk.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
* @return {Promise<void>}
*/
async deleteBulkContacts(
tenantId: number,
contactsIds: number[],
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given contacts or throw not found service error.
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
await contactRepository.deleteWhereIdIn(contactsIds);
}
/**
* Reverts journal entries of the given contacts.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
*/
async revertJEntriesContactsOpeningBalance(
tenantId: number,
contactsIds: number[],
contactService: TContactService,
trx?: Knex.Transaction
) {
const { AccountTransaction } = this.tenancy.models(tenantId);
const journal = new JournalPoster(tenantId, null, trx);
// Loads the contact opening balance journal transactions.
const contactsTransactions = await AccountTransaction.query()
.whereIn('reference_id', contactsIds)
.where('reference_type', `${upperFirst(contactService)}OpeningBalance`);
journal.fromTransactions(contactsTransactions);
journal.removeEntries();
await Promise.all([journal.saveBalance(), journal.deleteEntries()]);
}
/**
* Chanages the opening balance of the given contact.
* @param {number} tenantId
* @param {number} contactId
* @param {ICustomerChangeOpeningBalanceDTO} changeOpeningBalance
* @return {Promise<void>}
*/
public async changeOpeningBalance(
tenantId: number,
contactId: number,
contactService: string,
openingBalance: number,
openingBalanceAt?: Date | string
): Promise<void> {
const { contactRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given contact details or throw not found service error.
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
// Should the opening balance date be required.
if (!contact.openingBalanceAt && !openingBalanceAt) {
throw new ServiceError(ERRORS.OPENING_BALANCE_DATE_REQUIRED);
}
// Changes the customer the opening balance and opening balance date.
await contactRepository.update(
{
openingBalance: openingBalance,
...(openingBalanceAt && {
openingBalanceAt: moment(openingBalanceAt).toMySqlDateTime(),
}),
},
{
id: contactId,
contactService,
}
);
}
/**
* Inactive the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} contactId - Contact id.
*/
async inactivateContact(tenantId: number, contactId: number): Promise<void> {
const { Contact } = this.tenancy.models(tenantId);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId);
if (!contact.active) {
throw new ServiceError(ERRORS.CONTACT_ALREADY_INACTIVE);
}
await Contact.query().findById(contactId).update({ active: false });
}
/**
* Inactive the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} contactId - Contact id.
*/
async activateContact(tenantId: number, contactId: number): Promise<void> {
const { Contact } = this.tenancy.models(tenantId);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId);
if (contact.active) {
throw new ServiceError(ERRORS.CONTACT_ALREADY_ACTIVE);
}
await Contact.query().findById(contactId).update({ active: true });
}
}

View File

@@ -0,0 +1,70 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { CustomerValidators } from './CustomerValidators';
import {
ICustomerActivatingPayload,
ICustomerActivatedPayload,
} from '@/interfaces';
@Service()
export class ActivateCustomer {
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: CustomerValidators;
/**
* Inactive the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} contactId - Contact id.
* @returns {Promise<void>}
*/
public async activateCustomer(
tenantId: number,
customerId: number
): Promise<void> {
const { Contact } = this.tenancy.models(tenantId);
// Retrieves the customer or throw not found error.
const oldCustomer = await Contact.query()
.findById(customerId)
.modify('customer')
.throwIfNotFound();
this.validators.validateNotAlreadyPublished(oldCustomer);
// Edits the given customer with associated transactions on unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCustomerActivating` event.
await this.eventPublisher.emitAsync(events.customers.onActivating, {
tenantId,
trx,
oldCustomer,
} as ICustomerActivatingPayload);
// Update the given customer details.
const customer = await Contact.query(trx)
.findById(customerId)
.update({ active: true });
// Triggers `onCustomerActivated` event.
await this.eventPublisher.emitAsync(events.customers.onActivated, {
tenantId,
trx,
oldCustomer,
customer,
} as ICustomerActivatedPayload);
});
}
}

View File

@@ -0,0 +1,73 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
ICustomer,
ICustomerEventCreatedPayload,
ICustomerEventCreatingPayload,
ICustomerNewDTO,
ISystemUser,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { CreateEditCustomerDTO } from './CreateEditCustomerDTO';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class CreateCustomer {
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private customerDTO: CreateEditCustomerDTO;
@Inject()
private tenancy: HasTenancyService;
/**
* Creates a new customer.
* @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO
* @return {Promise<ICustomer>}
*/
public async createCustomer(
tenantId: number,
customerDTO: ICustomerNewDTO,
authorizedUser: ISystemUser
): Promise<ICustomer> {
const { Contact } = this.tenancy.models(tenantId);
// Transformes the customer DTO to customer object.
const customerObj = await this.customerDTO.transformCreateDTO(
tenantId,
customerDTO
);
// Creates a new customer under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCustomerCreating` event.
await this.eventPublisher.emitAsync(events.customers.onCreating, {
tenantId,
customerDTO,
trx,
} as ICustomerEventCreatingPayload);
// Creates a new contact as customer.
const customer = await Contact.query().insertAndFetch({
...customerObj,
});
// Triggers `onCustomerCreated` event.
await this.eventPublisher.emitAsync(events.customers.onCreated, {
customer,
tenantId,
customerId: customer.id,
authorizedUser,
trx,
} as ICustomerEventCreatedPayload);
return customer;
});
}
}

View File

@@ -0,0 +1,69 @@
import moment from 'moment';
import { defaultTo, omit, isEmpty } from 'lodash';
import { Service, Inject } from 'typedi';
import {
ContactService,
ICustomer,
ICustomerEditDTO,
ICustomerNewDTO,
} from '@/interfaces';
import { TenantMetadata } from '@/system/models';
@Service()
export class CreateEditCustomerDTO {
/**
* Transformes the create/edit DTO.
* @param {ICustomerNewDTO | ICustomerEditDTO} customerDTO
* @returns
*/
private transformCommonDTO = (
customerDTO: ICustomerNewDTO | ICustomerEditDTO
): Partial<ICustomer> => {
return {
...omit(customerDTO, ['customerType']),
contactType: customerDTO.customerType,
};
};
/**
* Transformes the create DTO.
* @param {ICustomerNewDTO} customerDTO
* @returns {}
*/
public transformCreateDTO = async (
tenantId: number,
customerDTO: ICustomerNewDTO
) => {
const commonDTO = this.transformCommonDTO(customerDTO);
// Retrieves the tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
return {
...commonDTO,
currencyCode: commonDTO.currencyCode || tenantMeta?.baseCurrency,
active: defaultTo(customerDTO.active, true),
contactService: ContactService.Customer,
...(!isEmpty(customerDTO.openingBalanceAt)
? {
openingBalanceAt: moment(
customerDTO?.openingBalanceAt
).toMySqlDateTime(),
}
: {}),
};
};
/**
* Transformes the edit DTO.
* @param {ICustomerEditDTO} customerDTO
* @returns
*/
public transformEditDTO = (customerDTO: ICustomerEditDTO) => {
const commonDTO = this.transformCommonDTO(customerDTO);
return {
...commonDTO,
};
};
}

View File

@@ -0,0 +1,16 @@
import { ServiceError } from '@/exceptions';
import { Service, Inject } from 'typedi';
import { ERRORS } from '../constants';
@Service()
export class CustomerValidators {
/**
* Validates the given customer is not already published.
* @param {ICustomer} customer
*/
public validateNotAlreadyPublished = (customer) => {
if (customer.active) {
throw new ServiceError(ERRORS.CUSTOMER_ALREADY_ACTIVE);
}
};
}

View File

@@ -0,0 +1,69 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
ICustomerDeletingPayload,
ICustomerEventDeletedPayload,
ISystemUser,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from '../constants';
@Service()
export class DeleteCustomer {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Deletes the given customer from the storage.
* @param {number} tenantId
* @param {number} customerId
* @return {Promise<void>}
*/
public async deleteCustomer(
tenantId: number,
customerId: number,
authorizedUser: ISystemUser
): Promise<void> {
const { Contact } = this.tenancy.models(tenantId);
// Retrieve the customer of throw not found service error.
const oldCustomer = await Contact.query()
.findById(customerId)
.modify('customer')
.throwIfNotFound()
.queryAndThrowIfHasRelations({
type: ERRORS.CUSTOMER_HAS_TRANSACTIONS,
});
// Triggers `onCustomerDeleting` event.
await this.eventPublisher.emitAsync(events.customers.onDeleting, {
tenantId,
customerId,
oldCustomer,
} as ICustomerDeletingPayload);
// Deletes the customer and associated entities under UOW transaction.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Delete the customer from the storage.
await Contact.query(trx).findById(customerId).delete();
// Throws `onCustomerDeleted` event.
await this.eventPublisher.emitAsync(events.customers.onDeleted, {
tenantId,
customerId,
oldCustomer,
authorizedUser,
trx,
} as ICustomerEventDeletedPayload);
});
}
}

View File

@@ -0,0 +1,77 @@
import { Knex } from 'knex';
import {
ICustomer,
ICustomerEditDTO,
ICustomerEventEditedPayload,
ICustomerEventEditingPayload,
ISystemUser,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CreateEditCustomerDTO } from './CreateEditCustomerDTO';
@Service()
export class EditCustomer {
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private customerDTO: CreateEditCustomerDTO;
/**
* Edits details of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {ICustomerEditDTO} customerDTO
* @return {Promise<ICustomer>}
*/
public async editCustomer(
tenantId: number,
customerId: number,
customerDTO: ICustomerEditDTO
): Promise<ICustomer> {
const { Contact } = this.tenancy.models(tenantId);
// Retrieve the vendor or throw not found error.
const oldCustomer = await Contact.query()
.findById(customerId)
.modify('customer')
.throwIfNotFound();
// Transformes the given customer DTO to object.
const customerObj = this.customerDTO.transformEditDTO(customerDTO);
// Edits the given customer under unit-of-work evnirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCustomerEditing` event.
await this.eventPublisher.emitAsync(events.customers.onEditing, {
tenantId,
customerDTO,
customerId,
trx,
} as ICustomerEventEditingPayload);
// Edits the customer details on the storage.
const customer = await Contact.query().updateAndFetchById(customerId, {
...customerObj,
});
// Triggers `onCustomerEdited` event.
await this.eventPublisher.emitAsync(events.customers.onEdited, {
customerId,
customer,
trx,
} as ICustomerEventEditedPayload);
return customer;
});
}
}

View File

@@ -0,0 +1,74 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ICustomer,
ICustomerOpeningBalanceEditDTO,
ICustomerOpeningBalanceEditedPayload,
ICustomerOpeningBalanceEditingPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
@Service()
export class EditOpeningBalanceCustomer {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Changes the opening balance of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {number} openingBalance
* @param {string|Date} openingBalanceAt
*/
public async changeOpeningBalance(
tenantId: number,
customerId: number,
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO
): Promise<ICustomer> {
const { Customer } = this.tenancy.models(tenantId);
// Retrieves the old customer or throw not found error.
const oldCustomer = await Customer.query()
.findById(customerId)
.throwIfNotFound();
// Mutates the customer opening balance under unit-of-work.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCustomerOpeningBalanceChanging` event.
await this.eventPublisher.emitAsync(
events.customers.onOpeningBalanceChanging,
{
tenantId,
oldCustomer,
openingBalanceEditDTO,
trx,
} as ICustomerOpeningBalanceEditingPayload
);
// Mutates the customer on the storage.
const customer = await Customer.query().patchAndFetchById(customerId, {
...openingBalanceEditDTO,
});
// Triggers `onCustomerOpeingBalanceChanged` event.
await this.eventPublisher.emitAsync(
events.customers.onOpeningBalanceChanged,
{
tenantId,
customer,
oldCustomer,
openingBalanceEditDTO,
trx,
} as ICustomerOpeningBalanceEditedPayload
);
return customer;
});
}
}

View File

@@ -0,0 +1,36 @@
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import I18nService from '@/services/I18n/I18nService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Service, Inject } from 'typedi';
import CustomerTransfromer from '../CustomerTransformer';
@Service()
export class GetCustomer {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the given customer details.
* @param {number} tenantId
* @param {number} customerId
*/
public async getCustomer(tenantId: number, customerId: number) {
const { Contact } = this.tenancy.models(tenantId);
// Retrieve the customer model or throw not found error.
const customer = await Contact.query()
.modify('customer')
.findById(customerId)
.throwIfNotFound();
// Retrieves the transformered customers.
return this.transformer.transform(
tenantId,
customer,
new CustomerTransfromer()
);
}
}

View File

@@ -0,0 +1,77 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import {
ICustomer,
ICustomersFilter,
IFilterMeta,
IPaginationMeta,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import CustomerTransfromer from '../CustomerTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetCustomers {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Parses customers list filter DTO.
* @param filterDTO -
*/
private parseCustomersListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve customers paginated list.
* @param {number} tenantId - Tenant id.
* @param {ICustomersFilter} filter - Cusotmers filter.
*/
public async getCustomersList(
tenantId: number,
filterDTO: ICustomersFilter
): Promise<{
customers: ICustomer[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Customer } = this.tenancy.models(tenantId);
// Parses customers list filter DTO.
const filter = this.parseCustomersListFilterDTO(filterDTO);
// Dynamic list.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Customer,
filter
);
// Customers.
const { results, pagination } = await Customer.query()
.onBuild((builder) => {
dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode);
})
.pagination(filter.page - 1, filter.pageSize);
// Retrieves the transformed customers.
const customers = await this.transformer.transform(
tenantId,
results,
new CustomerTransfromer()
);
return {
customers,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
}

View File

@@ -0,0 +1,117 @@
import { Service, Inject } from 'typedi';
import { AccountNormal, ICustomer, ILedgerEntry } from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export class CustomerGLEntries {
/**
* Retrieves the customer opening balance common entry attributes.
* @param {ICustomer} customer
*/
private getCustomerOpeningGLCommonEntry = (customer: ICustomer) => {
return {
exchangeRate: customer.openingBalanceExchangeRate,
currencyCode: customer.currencyCode,
transactionType: 'CustomerOpeningBalance',
transactionId: customer.id,
date: customer.openingBalanceAt,
userId: customer.userId,
contactId: customer.id,
credit: 0,
debit: 0,
branchId: customer.openingBalanceBranchId,
};
};
/**
* Retrieves the customer opening GL credit entry.
* @param {number} ARAccountId
* @param {ICustomer} customer
* @returns {ILedgerEntry}
*/
private getCustomerOpeningGLCreditEntry = (
ARAccountId: number,
customer: ICustomer
): ILedgerEntry => {
const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
return {
...commonEntry,
credit: 0,
debit: customer.localOpeningBalance,
accountId: ARAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
};
};
/**
* Retrieves the customer opening GL debit entry.
* @param {number} incomeAccountId
* @param {ICustomer} customer
* @returns {ILedgerEntry}
*/
private getCustomerOpeningGLDebitEntry = (
incomeAccountId: number,
customer: ICustomer
): ILedgerEntry => {
const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
return {
...commonEntry,
credit: customer.localOpeningBalance,
debit: 0,
accountId: incomeAccountId,
accountNormal: AccountNormal.CREDIT,
index: 2,
};
};
/**
* Retrieves the customer opening GL entries.
* @param {number} ARAccountId
* @param {number} incomeAccountId
* @param {ICustomer} customer
* @returns {ILedgerEntry[]}
*/
public getCustomerOpeningGLEntries = (
ARAccountId: number,
incomeAccountId: number,
customer: ICustomer
) => {
const debitEntry = this.getCustomerOpeningGLDebitEntry(
incomeAccountId,
customer
);
const creditEntry = this.getCustomerOpeningGLCreditEntry(
ARAccountId,
customer
);
return [debitEntry, creditEntry];
};
/**
* Retrieves the customer opening balance ledger.
* @param {number} ARAccountId
* @param {number} incomeAccountId
* @param {ICustomer} customer
* @returns {ILedger}
*/
public getCustomerOpeningLedger = (
ARAccountId: number,
incomeAccountId: number,
customer: ICustomer
) => {
const entries = this.getCustomerOpeningGLEntries(
ARAccountId,
incomeAccountId,
customer
);
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,90 @@
import { Knex } from 'knex';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Service, Inject } from 'typedi';
import { CustomerGLEntries } from './CustomerGLEntries';
@Service()
export class CustomerGLEntriesStorage {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private ledegrRepository: LedgerStorageService;
@Inject()
private customerGLEntries: CustomerGLEntries;
/**
* Customer opening balance journals.
* @param {number} tenantId
* @param {number} customerId
* @param {Knex.Transaction} trx
*/
public writeCustomerOpeningBalance = async (
tenantId: number,
customerId: number,
trx?: Knex.Transaction
) => {
const { Customer } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const customer = await Customer.query(trx).findById(customerId);
// Finds the income account.
const incomeAccount = await accountRepository.findOne({
slug: 'other-income',
});
// Find or create the A/R account.
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
customer.currencyCode,
{},
trx
);
// Retrieves the customer opening balance ledger.
const ledger = this.customerGLEntries.getCustomerOpeningLedger(
ARAccount.id,
incomeAccount.id,
customer
);
// Commits the ledger entries to the storage.
await this.ledegrRepository.commit(tenantId, ledger, trx);
};
/**
* Reverts the customer opening balance GL entries.
* @param {number} tenantId
* @param {number} customerId
* @param {Knex.Transaction} trx
*/
public revertCustomerOpeningBalance = async (
tenantId: number,
customerId: number,
trx?: Knex.Transaction
) => {
await this.ledegrRepository.deleteByReference(
tenantId,
customerId,
'CustomerOpeningBalance',
trx
);
};
/**
* Writes the customer opening balance GL entries.
* @param {number} tenantId
* @param {number} customerId
* @param {Knex.Transaction} trx
*/
public rewriteCustomerOpeningBalance = async (
tenantId: number,
customerId: number,
trx?: Knex.Transaction
) => {
// Reverts the customer opening balance entries.
await this.revertCustomerOpeningBalance(tenantId, customerId, trx);
// Write the customer opening balance entries.
await this.writeCustomerOpeningBalance(tenantId, customerId, trx);
};
}

View File

@@ -0,0 +1,38 @@
import ContactTransfromer from '../ContactTransformer';
export default class CustomerTransfromer extends ContactTransfromer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedBalance',
'formattedOpeningBalance',
'formattedOpeningBalanceAt',
'customerType',
'formattedCustomerType',
];
};
/**
* Retrieve customer type.
* @returns {string}
*/
protected customerType = (customer): string => {
return customer.contactType;
};
/**
* Retrieve the formatted customer type.
* @param customer
* @returns {string}
*/
protected formattedCustomerType = (customer): string => {
const keywords = {
individual: 'customer.type.individual',
business: 'customer.type.business',
};
return this.context.i18n.__(keywords[customer.contactType] || '');
};
}

View File

@@ -0,0 +1,130 @@
import {
ICustomer,
ICustomerEditDTO,
ICustomerNewDTO,
ICustomerOpeningBalanceEditDTO,
ICustomersFilter,
ISystemUser,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { CreateCustomer } from './CRUD/CreateCustomer';
import { DeleteCustomer } from './CRUD/DeleteCustomer';
import { EditCustomer } from './CRUD/EditCustomer';
import { EditOpeningBalanceCustomer } from './CRUD/EditOpeningBalanceCustomer';
import { GetCustomer } from './CRUD/GetCustomer';
import { GetCustomers } from './CRUD/GetCustomers';
@Service()
export class CustomersApplication {
@Inject()
private getCustomerService: GetCustomer;
@Inject()
private createCustomerService: CreateCustomer;
@Inject()
private editCustomerService: EditCustomer;
@Inject()
private deleteCustomerService: DeleteCustomer;
@Inject()
private editOpeningBalanceService: EditOpeningBalanceCustomer;
@Inject()
private getCustomersService: GetCustomers;
/**
* Retrieves the given customer details.
* @param {number} tenantId
* @param {number} customerId
*/
public getCustomer = (tenantId: number, customerId: number) => {
return this.getCustomerService.getCustomer(tenantId, customerId);
};
/**
* Creates a new customer.
* @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO
* @param {ISystemUser} authorizedUser
* @returns {Promise<ICustomer>}
*/
public createCustomer = (
tenantId: number,
customerDTO: ICustomerNewDTO,
authorizedUser: ISystemUser
) => {
return this.createCustomerService.createCustomer(
tenantId,
customerDTO,
authorizedUser
);
};
/**
* Edits details of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {ICustomerEditDTO} customerDTO
* @return {Promise<ICustomer>}
*/
public editCustomer = (
tenantId: number,
customerId: number,
customerDTO: ICustomerEditDTO
) => {
return this.editCustomerService.editCustomer(
tenantId,
customerId,
customerDTO
);
};
/**
* Deletes the given customer and associated transactions.
* @param {number} tenantId
* @param {number} customerId
* @param {ISystemUser} authorizedUser
* @returns {Promise<void>}
*/
public deleteCustomer = (
tenantId: number,
customerId: number,
authorizedUser: ISystemUser
) => {
return this.deleteCustomerService.deleteCustomer(
tenantId,
customerId,
authorizedUser
);
};
/**
* Changes the opening balance of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {Date|string} openingBalanceEditDTO
* @returns {Promise<ICustomer>}
*/
public editOpeningBalance = (
tenantId: number,
customerId: number,
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO
): Promise<ICustomer> => {
return this.editOpeningBalanceService.changeOpeningBalance(
tenantId,
customerId,
openingBalanceEditDTO
);
};
/**
* Retrieve customers paginated list.
* @param {number} tenantId - Tenant id.
* @param {ICustomersFilter} filter - Cusotmers filter.
*/
public getCustomers = (tenantId: number, filterDTO: ICustomersFilter) => {
return this.getCustomersService.getCustomersList(tenantId, filterDTO);
};
}

View File

@@ -0,0 +1,91 @@
import { Service, Inject } from 'typedi';
import {
ICustomerEventCreatedPayload,
ICustomerEventDeletedPayload,
ICustomerOpeningBalanceEditedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { CustomerGLEntriesStorage } from '../CustomerGLEntriesStorage';
@Service()
export class CustomerWriteGLOpeningBalanceSubscriber {
@Inject()
private customerGLEntries: CustomerGLEntriesStorage;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.customers.onCreated,
this.handleWriteOpenBalanceEntries
);
bus.subscribe(
events.customers.onDeleted,
this.handleRevertOpeningBalanceEntries
);
bus.subscribe(
events.customers.onOpeningBalanceChanged,
this.handleRewriteOpeningEntriesOnChanged
);
}
/**
* Handles the writing opening balance journal entries once the customer created.
* @param {ICustomerEventCreatedPayload} payload -
*/
private handleWriteOpenBalanceEntries = async ({
tenantId,
customer,
trx,
}: ICustomerEventCreatedPayload) => {
// Writes the customer opening balance journal entries.
if (customer.openingBalance) {
await this.customerGLEntries.writeCustomerOpeningBalance(
tenantId,
customer.id,
trx
);
}
};
/**
* Handles the deleting opeing balance journal entrise once the customer deleted.
* @param {ICustomerEventDeletedPayload} payload -
*/
private handleRevertOpeningBalanceEntries = async ({
tenantId,
customerId,
trx,
}: ICustomerEventDeletedPayload) => {
await this.customerGLEntries.revertCustomerOpeningBalance(
tenantId,
customerId,
trx
);
};
/**
* Handles the rewrite opening balance entries once opening balnace changed.
* @param {ICustomerOpeningBalanceEditedPayload} payload -
*/
private handleRewriteOpeningEntriesOnChanged = async ({
tenantId,
customer,
trx,
}: ICustomerOpeningBalanceEditedPayload) => {
if (customer.openingBalance) {
await this.customerGLEntries.rewriteCustomerOpeningBalance(
tenantId,
customer.id,
trx
);
} else {
await this.customerGLEntries.revertCustomerOpeningBalance(
tenantId,
customer.id,
trx
);
}
};
}

View File

@@ -0,0 +1,27 @@
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'Overdue',
slug: 'overdue',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'overdue' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Unpaid',
slug: 'unpaid',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const ERRORS = {
CUSTOMER_HAS_TRANSACTIONS: 'CUSTOMER_HAS_TRANSACTIONS',
CUSTOMER_ALREADY_ACTIVE: 'CUSTOMER_ALREADY_ACTIVE',
};

View File

@@ -0,0 +1,67 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { VendorValidators } from './VendorValidators';
import { IVendorActivatedPayload } from '@/interfaces';
@Service()
export class ActivateVendor {
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: VendorValidators;
/**
* Inactive the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} contactId - Contact id.
* @returns {Promise<void>}
*/
public async activateVendor(
tenantId: number,
vendorId: number
): Promise<void> {
const { Contact } = this.tenancy.models(tenantId);
// Retrieves the old vendor or throw not found error.
const oldVendor = await Contact.query()
.findById(vendorId)
.modify('vendor')
.throwIfNotFound();
// Validate whether the vendor is already published.
this.validators.validateNotAlreadyPublished(oldVendor);
// Edits the vendor with associated transactions on unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onVendorActivating` event.
await this.eventPublisher.emitAsync(events.vendors.onActivating, {
tenantId,
trx,
oldVendor,
} as IVendorActivatedPayload);
// Updates the vendor on the storage.
const vendor = await Contact.query(trx).updateAndFetchById(vendorId, {
active: true,
});
// Triggers `onVendorActivated` event.
await this.eventPublisher.emitAsync(events.vendors.onActivated, {
tenantId,
trx,
oldVendor,
vendor,
} as IVendorActivatedPayload);
});
}
}

View File

@@ -0,0 +1,67 @@
import moment from 'moment';
import { defaultTo, isEmpty } from 'lodash';
import { Service } from 'typedi';
import {
ContactService,
IVendor,
IVendorEditDTO,
IVendorNewDTO,
} from '@/interfaces';
import { TenantMetadata } from '@/system/models';
@Service()
export class CreateEditVendorDTO {
/**
*
* @param {IVendorNewDTO | IVendorEditDTO} vendorDTO
* @returns
*/
private transformCommonDTO = (vendorDTO: IVendorNewDTO | IVendorEditDTO) => {
return {
...vendorDTO,
};
};
/**
* Transformes the create vendor DTO.
* @param {IVendorNewDTO} vendorDTO -
* @returns {}
*/
public transformCreateDTO = async (
tenantId: number,
vendorDTO: IVendorNewDTO
) => {
const commonDTO = this.transformCommonDTO(vendorDTO);
// Retrieves the tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
return {
...commonDTO,
currencyCode: vendorDTO.currencyCode || tenantMeta.baseCurrency,
active: defaultTo(vendorDTO.active, true),
contactService: ContactService.Vendor,
...(!isEmpty(vendorDTO.openingBalanceAt)
? {
openingBalanceAt: moment(
vendorDTO?.openingBalanceAt
).toMySqlDateTime(),
}
: {}),
};
};
/**
* Transformes the edit vendor DTO.
* @param {IVendorEditDTO} vendorDTO
* @returns
*/
public transformEditDTO = (vendorDTO: IVendorEditDTO) => {
const commonDTO = this.transformCommonDTO(vendorDTO);
return {
...commonDTO,
};
};
}

View File

@@ -0,0 +1,72 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
ISystemUser,
IVendorEventCreatedPayload,
IVendorEventCreatingPayload,
IVendorNewDTO,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CreateEditVendorDTO } from './CreateEditVendorDTO';
@Service()
export class CreateVendor {
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformDTO: CreateEditVendorDTO;
/**
* Creates a new vendor.
* @param {number} tenantId
* @param {IVendorNewDTO} vendorDTO
* @return {Promise<void>}
*/
public async createVendor(
tenantId: number,
vendorDTO: IVendorNewDTO,
authorizedUser: ISystemUser
) {
const { Contact } = this.tenancy.models(tenantId);
// Transformes create DTO to customer object.
const vendorObject = await this.transformDTO.transformCreateDTO(
tenantId,
vendorDTO
);
// Creates vendor contact under unit-of-work evnirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onVendorCreating` event.
await this.eventPublisher.emitAsync(events.vendors.onCreating, {
tenantId,
vendorDTO,
trx,
} as IVendorEventCreatingPayload);
// Creates a new contact as vendor.
const vendor = await Contact.query(trx).insertAndFetch({
...vendorObject,
});
// Triggers `onVendorCreated` event.
await this.eventPublisher.emitAsync(events.vendors.onCreated, {
tenantId,
vendorId: vendor.id,
vendor,
authorizedUser,
trx,
} as IVendorEventCreatedPayload);
return vendor;
});
}
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ISystemUser,
IVendorEventDeletedPayload,
IVendorEventDeletingPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { ERRORS } from '../constants';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class DeleteVendor {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Deletes the given vendor.
* @param {number} tenantId
* @param {number} vendorId
* @return {Promise<void>}
*/
public async deleteVendor(
tenantId: number,
vendorId: number,
authorizedUser: ISystemUser
) {
const { Contact } = this.tenancy.models(tenantId);
// Retrieves the old vendor or throw not found service error.
const oldVendor = await Contact.query()
.modify('vendor')
.findById(vendorId)
.throwIfNotFound()
.queryAndThrowIfHasRelations({
type: ERRORS.VENDOR_HAS_TRANSACTIONS,
});
// Triggers `onVendorDeleting` event.
await this.eventPublisher.emitAsync(events.vendors.onDeleting, {
tenantId,
vendorId,
oldVendor,
} as IVendorEventDeletingPayload);
// Deletes vendor contact under unit-of-work.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Deletes the vendor contact from the storage.
await Contact.query(trx).findById(vendorId).delete();
// Triggers `onVendorDeleted` event.
await this.eventPublisher.emitAsync(events.vendors.onDeleted, {
tenantId,
vendorId,
authorizedUser,
oldVendor,
trx,
} as IVendorEventDeletedPayload);
});
}
}

View File

@@ -0,0 +1,72 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IVendorOpeningBalanceEditDTO,
IVendorOpeningBalanceEditedPayload,
IVendorOpeningBalanceEditingPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class EditOpeningBalanceVendor {
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
/**
* Changes the opening balance of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {number} openingBalance
* @param {string|Date} openingBalanceAt
* @returns {Promise<IVendor>}
*/
public async editOpeningBalance(
tenantId: number,
vendorId: number,
openingBalanceEditDTO: IVendorOpeningBalanceEditDTO
) {
const { Vendor } = this.tenancy.models(tenantId);
// Retrieves the old vendor or throw not found error.
const oldVendor = await Vendor.query().findById(vendorId).throwIfNotFound();
// Mutates the customer opening balance under unit-of-work.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onVendorOpeingBalanceChanging` event.
await this.eventPublisher.emitAsync(
events.vendors.onOpeningBalanceChanging,
{
tenantId,
oldVendor,
openingBalanceEditDTO,
trx,
} as IVendorOpeningBalanceEditingPayload
);
// Mutates the vendor on the storage.
const vendor = await Vendor.query().patchAndFetchById(vendorId, {
...openingBalanceEditDTO,
});
// Triggers `onVendorOpeingBalanceChanged` event.
await this.eventPublisher.emitAsync(
events.vendors.onOpeningBalanceChanged,
{
tenantId,
vendor,
oldVendor,
openingBalanceEditDTO,
trx,
} as IVendorOpeningBalanceEditedPayload
);
return vendor;
});
}
}

View File

@@ -0,0 +1,78 @@
import {
ISystemUser,
IVendorEditDTO,
IVendorEventEditedPayload,
IVendorEventEditingPayload,
} from '@/interfaces';
import { Knex } from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { Inject, Service } from 'typedi';
import { CreateEditVendorDTO } from './CreateEditVendorDTO';
@Service()
export class EditVendor {
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private transformDTO: CreateEditVendorDTO;
@Inject()
private tenancy: HasTenancyService;
/**
* Edits details of the given vendor.
* @param {number} tenantId -
* @param {number} vendorId -
* @param {IVendorEditDTO} vendorDTO -
* @returns {Promise<IVendor>}
*/
public async editVendor(
tenantId: number,
vendorId: number,
vendorDTO: IVendorEditDTO,
authorizedUser: ISystemUser
) {
const { Contact } = this.tenancy.models(tenantId);
// Retrieve the vendor or throw not found error.
const oldVendor = await Contact.query()
.findById(vendorId)
.modify('vendor')
.throwIfNotFound();
// Transformes vendor DTO to object.
const vendorObj = this.transformDTO.transformEditDTO(vendorDTO);
// Edits vendor contact under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onVendorEditing` event.
await this.eventPublisher.emitAsync(events.vendors.onEditing, {
trx,
tenantId,
vendorDTO,
} as IVendorEventEditingPayload);
// Edits the vendor contact.
const vendor = await Contact.query().updateAndFetchById(vendorId, {
...vendorObj,
});
// Triggers `onVendorEdited` event.
await this.eventPublisher.emitAsync(events.vendors.onEdited, {
tenantId,
vendorId,
vendor,
authorizedUser,
trx,
} as IVendorEventEditedPayload);
return vendor;
});
}
}

View File

@@ -0,0 +1,34 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import VendorTransfromer from '../VendorTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetVendor {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the given vendor details.
* @param {number} tenantId
* @param {number} vendorId
*/
public async getVendor(tenantId: number, vendorId: number) {
const { Contact } = this.tenancy.models(tenantId);
const vendor = await Contact.query()
.findById(vendorId)
.modify('vendor')
.throwIfNotFound();
// Transformes the vendor.
return this.transformer.transform(
tenantId,
vendor,
new VendorTransfromer()
);
}
}

View File

@@ -0,0 +1,80 @@
import * as R from 'ramda';
import { Service, Inject } from 'typedi';
import {
IFilterMeta,
IPaginationMeta,
IVendor,
IVendorsFilter,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import VendorTransfromer from '../VendorTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetVendors {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve vendors datatable list.
* @param {number} tenantId - Tenant id.
* @param {IVendorsFilter} vendorsFilter - Vendors filter.
*/
public async getVendorsList(
tenantId: number,
filterDTO: IVendorsFilter
): Promise<{
vendors: IVendor[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Vendor } = this.tenancy.models(tenantId);
// Parses vendors list filter DTO.
const filter = this.parseVendorsListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Vendor,
filter
);
// Vendors list.
const { results, pagination } = await Vendor.query()
.onBuild((builder) => {
dynamicList.buildQuery()(builder);
// Switches between active/inactive modes.
builder.modify('inactiveMode', filter.inactiveMode);
})
.pagination(filter.page - 1, filter.pageSize);
// Transform the vendors.
const transformedVendors = await this.transformer.transform(
tenantId,
results,
new VendorTransfromer()
);
return {
vendors: transformedVendors,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
/**
*
* @param filterDTO
* @returns
*/
private parseVendorsListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,16 @@
import { ServiceError } from '@/exceptions';
import { Service, Inject } from 'typedi';
import { ERRORS } from '../constants';
@Service()
export class VendorValidators {
/**
* Validates the given vendor is not already activated.
* @param {IVendor} vendor
*/
public validateNotAlreadyPublished = (vendor) => {
if (vendor.active) {
throw new ServiceError(ERRORS.VENDOR_ALREADY_ACTIVE);
}
};
}

View File

@@ -0,0 +1,91 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { VendorGLEntriesStorage } from '../VendorGLEntriesStorage';
import {
IVendorEventCreatedPayload,
IVendorEventDeletedPayload,
IVendorOpeningBalanceEditedPayload,
} from '@/interfaces';
@Service()
export class VendorsWriteGLOpeningSubscriber {
@Inject()
private vendorGLEntriesStorage: VendorGLEntriesStorage;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.vendors.onCreated,
this.handleWriteOpeningBalanceEntries
);
bus.subscribe(
events.vendors.onDeleted,
this.handleRevertOpeningBalanceEntries
);
bus.subscribe(
events.vendors.onOpeningBalanceChanged,
this.handleRewriteOpeningEntriesOnChanged
);
}
/**
* Writes the open balance journal entries once the vendor created.
* @param {IVendorEventCreatedPayload} payload -
*/
private handleWriteOpeningBalanceEntries = async ({
tenantId,
vendor,
trx,
}: IVendorEventCreatedPayload) => {
// Writes the vendor opening balance journal entries.
if (vendor.openingBalance) {
await this.vendorGLEntriesStorage.writeVendorOpeningBalance(
tenantId,
vendor.id,
trx
);
}
};
/**
* Revert the opening balance journal entries once the vendor deleted.
* @param {IVendorEventDeletedPayload} payload -
*/
private handleRevertOpeningBalanceEntries = async ({
tenantId,
vendorId,
trx,
}: IVendorEventDeletedPayload) => {
await this.vendorGLEntriesStorage.revertVendorOpeningBalance(
tenantId,
vendorId,
trx
);
};
/**
* Handles the rewrite opening balance entries once opening balnace changed.
* @param {ICustomerOpeningBalanceEditedPayload} payload -
*/
private handleRewriteOpeningEntriesOnChanged = async ({
tenantId,
vendor,
trx,
}: IVendorOpeningBalanceEditedPayload) => {
if (vendor.openingBalance) {
await this.vendorGLEntriesStorage.rewriteVendorOpeningBalance(
tenantId,
vendor.id,
trx
);
} else {
await this.vendorGLEntriesStorage.revertVendorOpeningBalance(
tenantId,
vendor.id,
trx
);
}
};
}

View File

@@ -0,0 +1,115 @@
import { Service } from 'typedi';
import { IVendor, AccountNormal, ILedgerEntry } from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export class VendorGLEntries {
/**
* Retrieves the opening balance GL common entry.
* @param {IVendor} vendor -
*/
private getOpeningBalanceGLCommonEntry = (vendor: IVendor) => {
return {
exchangeRate: vendor.openingBalanceExchangeRate,
currencyCode: vendor.currencyCode,
transactionType: 'VendorOpeningBalance',
transactionId: vendor.id,
date: vendor.openingBalanceAt,
userId: vendor.userId,
contactId: vendor.id,
credit: 0,
debit: 0,
branchId: vendor.openingBalanceBranchId,
};
};
/**
* Retrieves the opening balance GL debit entry.
* @param {number} costAccountId -
* @param {IVendor} vendor
* @returns {ILedgerEntry}
*/
private getOpeningBalanceGLDebitEntry = (
costAccountId: number,
vendor: IVendor
): ILedgerEntry => {
const commonEntry = this.getOpeningBalanceGLCommonEntry(vendor);
return {
...commonEntry,
accountId: costAccountId,
accountNormal: AccountNormal.DEBIT,
debit: vendor.localOpeningBalance,
credit: 0,
index: 2,
};
};
/**
* Retrieves the opening balance GL credit entry.
* @param {number} APAccountId
* @param {IVendor} vendor
* @returns {ILedgerEntry}
*/
private getOpeningBalanceGLCreditEntry = (
APAccountId: number,
vendor: IVendor
): ILedgerEntry => {
const commonEntry = this.getOpeningBalanceGLCommonEntry(vendor);
return {
...commonEntry,
accountId: APAccountId,
accountNormal: AccountNormal.CREDIT,
credit: vendor.localOpeningBalance,
index: 1,
};
};
/**
* Retrieves the opening balance GL entries.
* @param {number} APAccountId
* @param {number} costAccountId -
* @param {IVendor} vendor
* @returns {ILedgerEntry[]}
*/
public getOpeningBalanceGLEntries = (
APAccountId: number,
costAccountId: number,
vendor: IVendor
): ILedgerEntry[] => {
const debitEntry = this.getOpeningBalanceGLDebitEntry(
costAccountId,
vendor
);
const creditEntry = this.getOpeningBalanceGLCreditEntry(
APAccountId,
vendor
);
return [debitEntry, creditEntry];
};
/**
* Retrieves the opening balance ledger.
* @param {number} APAccountId
* @param {number} costAccountId -
* @param {IVendor} vendor
* @returns {Ledger}
*/
public getOpeningBalanceLedger = (
APAccountId: number,
costAccountId: number,
vendor: IVendor
) => {
const entries = this.getOpeningBalanceGLEntries(
APAccountId,
costAccountId,
vendor
);
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,88 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { VendorGLEntries } from './VendorGLEntries';
@Service()
export class VendorGLEntriesStorage {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private ledegrRepository: LedgerStorageService;
@Inject()
private vendorGLEntries: VendorGLEntries;
/**
* Vendor opening balance journals.
* @param {number} tenantId
* @param {number} vendorId
* @param {Knex.Transaction} trx
*/
public writeVendorOpeningBalance = async (
tenantId: number,
vendorId: number,
trx?: Knex.Transaction
) => {
const { Vendor } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const vendor = await Vendor.query(trx).findById(vendorId);
// Finds the expense account.
const expenseAccount = await accountRepository.findOne({
slug: 'other-expenses',
});
// Find or create the A/P account.
const APAccount = await accountRepository.findOrCreateAccountsPayable(
vendor.currencyCode,
{},
trx
);
// Retrieves the vendor opening balance ledger.
const ledger = this.vendorGLEntries.getOpeningBalanceLedger(
APAccount.id,
expenseAccount.id,
vendor
);
// Commits the ledger entries to the storage.
await this.ledegrRepository.commit(tenantId, ledger, trx);
};
/**
* Reverts the vendor opening balance GL entries.
* @param {number} tenantId
* @param {number} vendorId
* @param {Knex.Transaction} trx
*/
public revertVendorOpeningBalance = async (
tenantId: number,
vendorId: number,
trx?: Knex.Transaction
) => {
await this.ledegrRepository.deleteByReference(
tenantId,
vendorId,
'VendorOpeningBalance',
trx
);
};
/**
* Writes the vendor opening balance GL entries.
* @param {number} tenantId
* @param {number} vendorId
* @param {Knex.Transaction} trx
*/
public rewriteVendorOpeningBalance = async (
tenantId: number,
vendorId: number,
trx?: Knex.Transaction
) => {
await this.writeVendorOpeningBalance(tenantId, vendorId, trx);
await this.revertVendorOpeningBalance(tenantId, vendorId, trx);
};
}

View File

@@ -0,0 +1,16 @@
import { Service } from 'typedi';
import ContactTransfromer from '../ContactTransformer';
export default class VendorTransfromer extends ContactTransfromer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedBalance',
'formattedOpeningBalance',
'formattedOpeningBalanceAt'
];
};
}

View File

@@ -0,0 +1,132 @@
import { Inject, Service } from 'typedi';
import {
ISystemUser,
IVendorEditDTO,
IVendorNewDTO,
IVendorOpeningBalanceEditDTO,
IVendorsFilter,
} from '@/interfaces';
import { CreateVendor } from './CRUD/CreateVendor';
import { DeleteVendor } from './CRUD/DeleteVendor';
import { EditOpeningBalanceVendor } from './CRUD/EditOpeningBalanceVendor';
import { EditVendor } from './CRUD/EditVendor';
import { GetVendor } from './CRUD/GetVendor';
import { GetVendors } from './CRUD/GetVendors';
@Service()
export class VendorsApplication {
@Inject()
private createVendorService: CreateVendor;
@Inject()
private editVendorService: EditVendor;
@Inject()
private deleteVendorService: DeleteVendor;
@Inject()
private editOpeningBalanceService: EditOpeningBalanceVendor;
@Inject()
private getVendorService: GetVendor;
@Inject()
private getVendorsService: GetVendors;
/**
* Creates a new vendor.
* @param {number} tenantId
* @param {IVendorNewDTO} vendorDTO
* @return {Promise<void>}
*/
public createVendor = (
tenantId: number,
vendorDTO: IVendorNewDTO,
authorizedUser: ISystemUser
) => {
return this.createVendorService.createVendor(
tenantId,
vendorDTO,
authorizedUser
);
};
/**
* Edits details of the given vendor.
* @param {number} tenantId -
* @param {number} vendorId -
* @param {IVendorEditDTO} vendorDTO -
* @returns {Promise<IVendor>}
*/
public editVendor = (
tenantId: number,
vendorId: number,
vendorDTO: IVendorEditDTO,
authorizedUser: ISystemUser
) => {
return this.editVendorService.editVendor(
tenantId,
vendorId,
vendorDTO,
authorizedUser
);
};
/**
* Deletes the given vendor.
* @param {number} tenantId
* @param {number} vendorId
* @return {Promise<void>}
*/
public deleteVendor = (
tenantId: number,
vendorId: number,
authorizedUser: ISystemUser
) => {
return this.deleteVendorService.deleteVendor(
tenantId,
vendorId,
authorizedUser
);
};
/**
* Changes the opening balance of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {number} openingBalance
* @param {string|Date} openingBalanceAt
* @returns {Promise<IVendor>}
*/
public editOpeningBalance = (
tenantId: number,
vendorId: number,
openingBalanceEditDTO: IVendorOpeningBalanceEditDTO
) => {
return this.editOpeningBalanceService.editOpeningBalance(
tenantId,
vendorId,
openingBalanceEditDTO
);
};
/**
* Retrieves the vendor details.
* @param {number} tenantId
* @param {number} vendorId
* @returns
*/
public getVendor = (tenantId: number, vendorId: number) => {
return this.getVendorService.getVendor(tenantId, vendorId);
};
/**
* Retrieves the vendors paginated list.
* @param {number} tenantId
* @param {IVendorsFilter} filterDTO
* @returns
*/
public getVendors = (tenantId: number, filterDTO: IVendorsFilter) => {
return this.getVendorsService.getVendorsList(tenantId, filterDTO);
};
}

View File

@@ -0,0 +1,27 @@
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'Overdue',
slug: 'overdue',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'overdue' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Unpaid',
slug: 'unpaid',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const ERRORS = {
VENDOR_HAS_TRANSACTIONS: 'VENDOR_HAS_TRANSACTIONS',
VENDOR_ALREADY_ACTIVE: 'VENDOR_ALREADY_ACTIVE',
};

View File

@@ -0,0 +1,29 @@
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'Overdue',
slug: 'overdue',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'overdue' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Unpaid',
slug: 'unpaid',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const ERRORS = {
OPENING_BALANCE_DATE_REQUIRED: 'OPENING_BALANCE_DATE_REQUIRED',
CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE',
CONTACT_ALREADY_ACTIVE: 'CONTACT_ALREADY_ACTIVE'
};