import { Inject, Service } from 'typedi'; import { intersection, defaultTo } from 'lodash'; import { EventDispatcher, EventDispatcherInterface, } from 'decorators/eventDispatcher'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalCommands from 'services/Accounting/JournalCommands'; import ContactsService from 'services/Contacts/ContactsService'; import { IVendorNewDTO, IVendorEditDTO, IVendor, IVendorsFilter, IPaginationMeta, IFilterMeta, ISystemUser, } from 'interfaces'; import { ServiceError } from 'exceptions'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; import events from 'subscribers/events'; @Service() export default class VendorsService { @Inject() contactService: ContactsService; @Inject() tenancy: TenancyService; @Inject() dynamicListService: DynamicListingService; @EventDispatcher() eventDispatcher: EventDispatcherInterface; @Inject('logger') logger: any; /** * Converts vendor to contact DTO. * @param {IVendorNewDTO|IVendorEditDTO} vendorDTO * @returns {IContactDTO} */ private vendorToContactDTO(vendorDTO: IVendorNewDTO | IVendorEditDTO) { return { ...vendorDTO, active: defaultTo(vendorDTO.active, true), }; } /** * Creates a new vendor. * @param {number} tenantId * @param {IVendorNewDTO} vendorDTO * @return {Promise} */ public async newVendor( tenantId: number, vendorDTO: IVendorNewDTO, authorizedUser: ISystemUser ) { this.logger.info('[vendor] trying create a new vendor.', { tenantId, vendorDTO, }); const contactDTO = this.vendorToContactDTO(vendorDTO); const vendor = await this.contactService.newContact( tenantId, contactDTO, 'vendor' ); // Triggers `onVendorCreated` event. await this.eventDispatcher.dispatch(events.vendors.onCreated, { tenantId, vendorId: vendor.id, vendor, authorizedUser, }); return vendor; } /** * Edits details of the given vendor. * @param {number} tenantId * @param {IVendorEditDTO} vendorDTO */ public async editVendor( tenantId: number, vendorId: number, vendorDTO: IVendorEditDTO, authorizedUser: ISystemUser ) { const contactDTO = this.vendorToContactDTO(vendorDTO); const vendor = await this.contactService.editContact( tenantId, vendorId, contactDTO, 'vendor' ); // Triggers `onVendorEdited` event. await this.eventDispatcher.dispatch(events.vendors.onEdited, { tenantId, vendorId, vendor, authorizedUser, }); return vendor; } /** * Retrieve the given vendor details by id or throw not found. * @param {number} tenantId * @param {number} customerId */ private getVendorByIdOrThrowError(tenantId: number, customerId: number) { return this.contactService.getContactByIdOrThrowError( tenantId, customerId, 'vendor' ); } /** * Deletes the given vendor from the storage. * @param {number} tenantId * @param {number} vendorId * @return {Promise} */ public async deleteVendor( tenantId: number, vendorId: number, authorizedUser: ISystemUser ) { // Validate the vendor existance on the storage. await this.getVendorByIdOrThrowError(tenantId, vendorId); // Validate the vendor has no associated bills. await this.vendorHasNoBillsOrThrowError(tenantId, vendorId); this.logger.info('[vendor] trying to delete vendor.', { tenantId, vendorId, }); await this.contactService.deleteContact(tenantId, vendorId, 'vendor'); // Triggers `onVendorDeleted` event. await this.eventDispatcher.dispatch(events.vendors.onDeleted, { tenantId, vendorId, authorizedUser, }); this.logger.info('[vendor] deleted successfully.', { tenantId, vendorId }); } /** * Retrieve the given vendor details. * @param {number} tenantId * @param {number} vendorId */ public async getVendor(tenantId: number, vendorId: number) { return this.contactService.getContact(tenantId, vendorId, 'vendor'); } /** * Writes vendor opening balance journal entries. * @param {number} tenantId * @param {number} vendorId * @param {number} openingBalance * @return {Promise} */ public async writeVendorOpeningBalanceJournal( tenantId: number, vendorId: number, openingBalance: number, openingBalanceAt: Date | string, user: ISystemUser ) { const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); this.logger.info('[vendor] writing opening balance journal entries.', { tenantId, vendorId, }); await journalCommands.vendorOpeningBalance( vendorId, openingBalance, openingBalanceAt, user ); await Promise.all([journal.saveBalance(), journal.saveEntries()]); } /** * Reverts vendor opening balance journal entries. * @param {number} tenantId - * @param {number} vendorId - * @return {Promise} */ public async revertOpeningBalanceEntries( tenantId: number, vendorId: number | number[] ) { const id = Array.isArray(vendorId) ? vendorId : [vendorId]; this.logger.info( '[customer] trying to revert opening balance journal entries.', { tenantId, vendorId } ); await this.contactService.revertJEntriesContactsOpeningBalance( tenantId, id, 'vendor' ); } /** * Retrieve the given vendors or throw error if one of them not found. * @param {numebr} tenantId * @param {number[]} vendorsIds */ private getVendorsOrThrowErrorNotFound( tenantId: number, vendorsIds: number[] ) { return this.contactService.getContactsOrThrowErrorNotFound( tenantId, vendorsIds, 'vendor' ); } /** * Deletes the given vendors from the storage. * @param {number} tenantId * @param {number[]} vendorsIds * @return {Promise} */ public async deleteBulkVendors( tenantId: number, vendorsIds: number[], authorizedUser: ISystemUser ): Promise { const { Contact } = this.tenancy.models(tenantId); // Validate the given vendors exists on the storage. await this.getVendorsOrThrowErrorNotFound(tenantId, vendorsIds); // Validate the given vendors have no assocaited bills. await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds); await Contact.query().whereIn('id', vendorsIds).delete(); // Triggers `onVendorsBulkDeleted` event. await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, { tenantId, vendorsIds, authorizedUser, }); this.logger.info('[vendor] bulk deleted successfully.', { tenantId, vendorsIds, }); } /** * Validates the vendor has no associated bills or throw service error. * @param {number} tenantId * @param {number} vendorId */ private async vendorHasNoBillsOrThrowError( tenantId: number, vendorId: number ) { const { billRepository } = this.tenancy.repositories(tenantId); // Retrieve the bill that associated to the given vendor id. const bills = await billRepository.find({ vendor_id: vendorId }); if (bills.length > 0) { throw new ServiceError('vendor_has_bills'); } } /** * Throws error in case one of vendors have associated bills. * @param {number} tenantId * @param {number[]} customersIds * @throws {ServiceError} */ private async vendorsHaveNoBillsOrThrowError( tenantId: number, vendorsIds: number[] ) { const { billRepository } = this.tenancy.repositories(tenantId); // Retrieves bills that assocaited to the given vendors. const vendorsBills = await billRepository.findWhereIn( 'vendor_id', vendorsIds ); const billsVendorsIds = vendorsBills.map((bill) => bill.vendorId); // The intersection between vendors and vendors that have bills. const vendorsHaveInvoices = intersection(vendorsIds, billsVendorsIds); if (vendorsHaveInvoices.length > 0) { throw new ServiceError('some_vendors_have_bills'); } } /** * Retrieve vendors datatable list. * @param {number} tenantId - Tenant id. * @param {IVendorsFilter} vendorsFilter - Vendors filter. */ public async getVendorsList( tenantId: number, vendorsFilter: IVendorsFilter ): Promise<{ vendors: IVendor[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { Vendor } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, Vendor, vendorsFilter ); const { results, pagination } = await Vendor.query() .onBuild((builder) => { dynamicFilter.buildQuery()(builder); }) .pagination(vendorsFilter.page - 1, vendorsFilter.pageSize); return { vendors: results, pagination, filterMeta: dynamicFilter.getResponseMeta(), }; } /** * Changes the opeing balance of the given vendor. * @param {number} tenantId * @param {number} vendorId * @param {number} openingBalance * @param {Date|string} openingBalanceAt */ public async changeOpeningBalance( tenantId: number, vendorId: number, openingBalance: number, openingBalanceAt: Date | string ): Promise { await this.contactService.changeOpeningBalance( tenantId, vendorId, 'vendor', openingBalance, openingBalanceAt ); // Triggers `onOpeingBalanceChanged` event. await this.eventDispatcher.dispatch( events.vendors.onOpeningBalanceChanged, { tenantId, vendorId, openingBalance, openingBalanceAt, } ); } }