mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
add server to monorepo.
This commit is contained in:
41
packages/server/src/services/Contacts/ContactTransformer.ts
Normal file
41
packages/server/src/services/Contacts/ContactTransformer.ts
Normal 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)
|
||||
: '';
|
||||
};
|
||||
}
|
||||
378
packages/server/src/services/Contacts/ContactsService.ts
Normal file
378
packages/server/src/services/Contacts/ContactsService.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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] || '');
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
27
packages/server/src/services/Contacts/Customers/constants.ts
Normal file
27
packages/server/src/services/Contacts/Customers/constants.ts
Normal 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',
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
115
packages/server/src/services/Contacts/Vendors/VendorGLEntries.ts
Normal file
115
packages/server/src/services/Contacts/Vendors/VendorGLEntries.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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'
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
27
packages/server/src/services/Contacts/Vendors/constants.ts
Normal file
27
packages/server/src/services/Contacts/Vendors/constants.ts
Normal 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',
|
||||
};
|
||||
29
packages/server/src/services/Contacts/constants.ts
Normal file
29
packages/server/src/services/Contacts/constants.ts
Normal 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'
|
||||
};
|
||||
Reference in New Issue
Block a user