import { Service, Inject } from 'typedi'; import { omit, sumBy } from 'lodash'; import * as R from 'ramda'; import moment from 'moment'; import { Knex } from 'knex'; import composeAsync from 'async/compose'; import { ISaleInvoice, ISaleInvoiceCreateDTO, ISaleInvoiceEditDTO, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta, ISystemUser, ISalesInvoicesService, ISaleInvoiceCreatedPayload, ISaleInvoiceDeletePayload, ISaleInvoiceDeletedPayload, ISaleInvoiceEventDeliveredPayload, ISaleInvoiceEditedPayload, ISaleInvoiceCreatingPaylaod, ISaleInvoiceEditingPayload, ISaleInvoiceDeliveringPayload, ICustomer, ITenantUser, } from '@/interfaces'; import events from '@/subscribers/events'; import InventoryService from '@/services/Inventory/Inventory'; import TenancyService from '@/services/Tenancy/TenancyService'; import { formatDateFields } from 'utils'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import { ServiceError } from '@/exceptions'; import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; import SaleEstimateService from '@/services/Sales/SalesEstimate'; import AutoIncrementOrdersService from './AutoIncrementOrdersService'; import { ERRORS } from './constants'; import { SaleInvoiceTransformer } from './SaleInvoiceTransformer'; import UnitOfWork from '@/services/UnitOfWork'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; /** * Sales invoices service * @service */ @Service('SalesInvoices') export default class SaleInvoicesService implements ISalesInvoicesService { @Inject() tenancy: TenancyService; @Inject() inventoryService: InventoryService; @Inject() itemsEntriesService: ItemsEntriesService; @Inject('logger') logger: any; @Inject() dynamicListService: DynamicListingService; @Inject() private saleEstimatesService: SaleEstimateService; @Inject() private autoIncrementOrdersService: AutoIncrementOrdersService; @Inject() private eventPublisher: EventPublisher; @Inject() private uow: UnitOfWork; @Inject() private branchDTOTransform: BranchTransactionDTOTransform; @Inject() private warehouseDTOTransform: WarehouseTransactionDTOTransform; @Inject() private transformer: TransformerInjectable; /** * Validate whether sale invoice number unqiue on the storage. */ async validateInvoiceNumberUnique( tenantId: number, invoiceNumber: string, notInvoiceId?: number ) { const { SaleInvoice } = this.tenancy.models(tenantId); const saleInvoice = await SaleInvoice.query() .findOne('invoice_no', invoiceNumber) .onBuild((builder) => { if (notInvoiceId) { builder.whereNot('id', notInvoiceId); } }); if (saleInvoice) { throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE); } } /** * Validate the sale invoice has no payment entries. * @param {number} tenantId * @param {number} saleInvoiceId */ async validateInvoiceHasNoPaymentEntries( tenantId: number, saleInvoiceId: number ) { const { PaymentReceiveEntry } = this.tenancy.models(tenantId); // Retrieve the sale invoice associated payment receive entries. const entries = await PaymentReceiveEntry.query().where( 'invoice_id', saleInvoiceId ); if (entries.length > 0) { throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES); } return entries; } /** * Validate the invoice amount is bigger than payment amount before edit the invoice. * @param {number} saleInvoiceAmount * @param {number} paymentAmount */ validateInvoiceAmountBiggerPaymentAmount( saleInvoiceAmount: number, paymentAmount: number ) { if (saleInvoiceAmount < paymentAmount) { throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT); } } /** * Validate whether sale invoice exists on the storage. * @param {Request} req * @param {Response} res * @param {Function} next */ async getInvoiceOrThrowError(tenantId: number, saleInvoiceId: number) { const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); const saleInvoice = await saleInvoiceRepository.findOneById( saleInvoiceId, 'entries' ); if (!saleInvoice) { throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); } return saleInvoice; } /** * Retrieve the next unique invoice number. * @param {number} tenantId - Tenant id. * @return {string} */ getNextInvoiceNumber(tenantId: number): string { return this.autoIncrementOrdersService.getNextTransactionNumber( tenantId, 'sales_invoices' ); } /** * Increment the invoice next number. * @param {number} tenantId - */ incrementNextInvoiceNumber(tenantId: number) { return this.autoIncrementOrdersService.incrementSettingsNextNumber( tenantId, 'sales_invoices' ); } /** * Transformes edit DTO to model. * @param {number} tennatId - * @param {ICustomer} customer - * @param {ISaleInvoiceEditDTO} saleInvoiceDTO - * @param {ISaleInvoice} oldSaleInvoice */ private tranformEditDTOToModel = async ( tenantId: number, customer: ICustomer, saleInvoiceDTO: ISaleInvoiceEditDTO, oldSaleInvoice: ISaleInvoice, authorizedUser: ITenantUser ) => { return this.transformDTOToModel( tenantId, customer, saleInvoiceDTO, authorizedUser, oldSaleInvoice ); }; /** * Transformes create DTO to model. * @param {number} tenantId - * @param {ICustomer} customer - * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - */ private transformCreateDTOToModel = async ( tenantId: number, customer: ICustomer, saleInvoiceDTO: ISaleInvoiceCreateDTO, authorizedUser: ITenantUser ) => { return this.transformDTOToModel( tenantId, customer, saleInvoiceDTO, authorizedUser ); }; /** * Transformes the create DTO to invoice object model. * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. * @param {ISaleInvoice} oldSaleInvoice - Old sale invoice. * @return {ISaleInvoice} */ private async transformDTOToModel( tenantId: number, customer: ICustomer, saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, authorizedUser: ITenantUser, oldSaleInvoice?: ISaleInvoice ): Promise { const { ItemEntry } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, (e) => ItemEntry.calcAmount(e) ); // Retreive the next invoice number. const autoNextNumber = this.getNextInvoiceNumber(tenantId); // Invoice number. const invoiceNo = saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber; // Validate the invoice is required. this.validateInvoiceNoRequire(invoiceNo); const initialEntries = saleInvoiceDTO.entries.map((entry) => ({ referenceType: 'SaleInvoice', ...entry, })); const entries = await composeAsync( // Sets default cost and sell account to invoice items entries. this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId) )(initialEntries); const initialDTO = { ...formatDateFields( omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']), ['invoiceDate', 'dueDate'] ), // Avoid rewrite the deliver date in edit mode when already published. balance, currencyCode: customer.currencyCode, exchangeRate: saleInvoiceDTO.exchangeRate || 1, ...(saleInvoiceDTO.delivered && !oldSaleInvoice?.deliveredAt && { deliveredAt: moment().toMySqlDateTime(), }), // Avoid override payment amount in edit mode. ...(!oldSaleInvoice && { paymentAmount: 0 }), ...(invoiceNo ? { invoiceNo } : {}), entries, userId: authorizedUser.id, } as ISaleInvoice; return R.compose( this.branchDTOTransform.transformDTO(tenantId), this.warehouseDTOTransform.transformDTO(tenantId) )(initialDTO); } /** * Validate the invoice number require. * @param {ISaleInvoice} saleInvoiceObj */ validateInvoiceNoRequire(invoiceNo: string) { if (!invoiceNo) { throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED); } } /** * Creates a new sale invoices and store it to the storage * with associated to entries and journal transactions. * @async * @param {number} tenantId - Tenant id. * @param {ISaleInvoice} saleInvoiceDTO - Sale invoice object DTO. * @return {Promise} */ public createSaleInvoice = async ( tenantId: number, saleInvoiceDTO: ISaleInvoiceCreateDTO, authorizedUser: ITenantUser ): Promise => { const { SaleInvoice, Contact } = this.tenancy.models(tenantId); // Validate customer existance. const customer = await Contact.query() .modify('customer') .findById(saleInvoiceDTO.customerId) .throwIfNotFound(); // Validate the from estimate id exists on the storage. if (saleInvoiceDTO.fromEstimateId) { const fromEstimate = await this.saleEstimatesService.getSaleEstimateOrThrowError( tenantId, saleInvoiceDTO.fromEstimateId ); // Validate the sale estimate is not already converted to invoice. this.saleEstimatesService.validateEstimateNotConverted(fromEstimate); } // Validate items ids existance. await this.itemsEntriesService.validateItemsIdsExistance( tenantId, saleInvoiceDTO.entries ); // Validate items should be sellable items. await this.itemsEntriesService.validateNonSellableEntriesItems( tenantId, saleInvoiceDTO.entries ); // Transform DTO object to model object. const saleInvoiceObj = await this.transformCreateDTOToModel( tenantId, customer, saleInvoiceDTO, authorizedUser ); // Validate sale invoice number uniquiness. if (saleInvoiceObj.invoiceNo) { await this.validateInvoiceNumberUnique( tenantId, saleInvoiceObj.invoiceNo ); } // Creates a new sale invoice and associated transactions under unit of work env. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onSaleInvoiceCreating` event. await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, { saleInvoiceDTO, tenantId, trx, } as ISaleInvoiceCreatingPaylaod); // Create sale invoice graph to the storage. const saleInvoice = await SaleInvoice.query(trx).upsertGraph( saleInvoiceObj ); const eventPayload: ISaleInvoiceCreatedPayload = { tenantId, saleInvoice, saleInvoiceDTO, saleInvoiceId: saleInvoice.id, authorizedUser, trx, }; // Triggers the event `onSaleInvoiceCreated`. await this.eventPublisher.emitAsync( events.saleInvoice.onCreated, eventPayload ); return saleInvoice; }); }; /** * Edit the given sale invoice. * @async * @param {number} tenantId - Tenant id. * @param {Number} saleInvoiceId - Sale invoice id. * @param {ISaleInvoice} saleInvoice - Sale invoice DTO object. * @return {Promise} */ public async editSaleInvoice( tenantId: number, saleInvoiceId: number, saleInvoiceDTO: ISaleInvoiceEditDTO, authorizedUser: ISystemUser ): Promise { const { SaleInvoice, Contact } = this.tenancy.models(tenantId); // Retrieve the sale invoice or throw not found service error. const oldSaleInvoice = await this.getInvoiceOrThrowError( tenantId, saleInvoiceId ); // Validate customer existance. const customer = await Contact.query() .findById(saleInvoiceDTO.customerId) .modify('customer') .throwIfNotFound(); // Validate items ids existance. await this.itemsEntriesService.validateItemsIdsExistance( tenantId, saleInvoiceDTO.entries ); // Validate non-sellable entries items. await this.itemsEntriesService.validateNonSellableEntriesItems( tenantId, saleInvoiceDTO.entries ); // Validate the items entries existance. await this.itemsEntriesService.validateEntriesIdsExistance( tenantId, saleInvoiceId, 'SaleInvoice', saleInvoiceDTO.entries ); // Transform DTO object to model object. const saleInvoiceObj = await this.tranformEditDTOToModel( tenantId, customer, saleInvoiceDTO, oldSaleInvoice, authorizedUser ); // Validate sale invoice number uniquiness. if (saleInvoiceObj.invoiceNo) { await this.validateInvoiceNumberUnique( tenantId, saleInvoiceObj.invoiceNo, saleInvoiceId ); } // Validate the invoice amount is not smaller than the invoice payment amount. this.validateInvoiceAmountBiggerPaymentAmount( saleInvoiceObj.balance, oldSaleInvoice.paymentAmount ); // Edit sale invoice transaction in UOW envirment. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onSaleInvoiceEditing` event. await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, { trx, oldSaleInvoice, tenantId, saleInvoiceDTO, } as ISaleInvoiceEditingPayload); // Upsert the the invoice graph to the storage. const saleInvoice: ISaleInvoice = await SaleInvoice.query().upsertGraphAndFetch({ id: saleInvoiceId, ...saleInvoiceObj, }); // Edit event payload. const editEventPayload: ISaleInvoiceEditedPayload = { tenantId, saleInvoiceId, saleInvoice, saleInvoiceDTO, oldSaleInvoice, authorizedUser, trx, }; // Triggers `onSaleInvoiceEdited` event. await this.eventPublisher.emitAsync( events.saleInvoice.onEdited, editEventPayload ); return saleInvoice; }); } /** * Deliver the given sale invoice. * @param {number} tenantId - Tenant id. * @param {number} saleInvoiceId - Sale invoice id. * @return {Promise} */ public async deliverSaleInvoice( tenantId: number, saleInvoiceId: number, authorizedUser: ISystemUser ): Promise { const { SaleInvoice } = this.tenancy.models(tenantId); // Retrieve details of the given sale invoice id. const oldSaleInvoice = await this.getInvoiceOrThrowError( tenantId, saleInvoiceId ); // Throws error in case the sale invoice already published. if (oldSaleInvoice.isDelivered) { throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED); } // Update sale invoice transaction with assocaite transactions // under unit-of-work envirement. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onSaleInvoiceDelivering` event. await this.eventPublisher.emitAsync(events.saleInvoice.onDelivering, { tenantId, oldSaleInvoice, trx, } as ISaleInvoiceDeliveringPayload); // Record the delivered at on the storage. const saleInvoice = await SaleInvoice.query(trx) .where({ id: saleInvoiceId }) .update({ deliveredAt: moment().toMySqlDateTime() }); // Triggers `onSaleInvoiceDelivered` event. await this.eventPublisher.emitAsync(events.saleInvoice.onDelivered, { tenantId, saleInvoiceId, saleInvoice, } as ISaleInvoiceEventDeliveredPayload); }); } /** * Deletes the given sale invoice with associated entries * and journal transactions. * @param {number} tenantId - Tenant id. * @param {Number} saleInvoiceId - The given sale invoice id. * @param {ISystemUser} authorizedUser - */ public async deleteSaleInvoice( tenantId: number, saleInvoiceId: number, authorizedUser: ISystemUser ): Promise { const { ItemEntry, SaleInvoice } = this.tenancy.models(tenantId); // Retrieve the given sale invoice with associated entries // or throw not found error. const oldSaleInvoice = await this.getInvoiceOrThrowError( tenantId, saleInvoiceId ); // Validate the sale invoice has no associated payment entries. await this.validateInvoiceHasNoPaymentEntries(tenantId, saleInvoiceId); // Validate the sale invoice has applied to credit note transaction. await this.validateInvoiceHasNoAppliedToCredit(tenantId, saleInvoiceId); // Deletes sale invoice transaction and associate transactions with UOW env. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onSaleInvoiceDelete` event. await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, { tenantId, saleInvoice: oldSaleInvoice, saleInvoiceId, trx, } as ISaleInvoiceDeletePayload); // Unlink the converted sale estimates from the given sale invoice. await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice( tenantId, saleInvoiceId, trx ); await ItemEntry.query(trx) .where('reference_id', saleInvoiceId) .where('reference_type', 'SaleInvoice') .delete(); await SaleInvoice.query(trx).findById(saleInvoiceId).delete(); // Triggers `onSaleInvoiceDeleted` event. await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, { tenantId, oldSaleInvoice, saleInvoiceId, authorizedUser, trx, } as ISaleInvoiceDeletedPayload); }); } /** * Records the inventory transactions of the given sale invoice in case * the invoice has inventory entries only. * * @param {number} tenantId - Tenant id. * @param {SaleInvoice} saleInvoice - Sale invoice DTO. * @param {number} saleInvoiceId - Sale invoice id. * @param {boolean} override - Allow to override old transactions. * @return {Promise} */ public async recordInventoryTranscactions( tenantId: number, saleInvoice: ISaleInvoice, override?: boolean, trx?: Knex.Transaction ): Promise { // Loads the inventory items entries of the given sale invoice. const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries( tenantId, saleInvoice.entries ); const transaction = { transactionId: saleInvoice.id, transactionType: 'SaleInvoice', transactionNumber: saleInvoice.invoiceNo, exchangeRate: saleInvoice.exchangeRate, warehouseId: saleInvoice.warehouseId, date: saleInvoice.invoiceDate, direction: 'OUT', entries: inventoryEntries, createdAt: saleInvoice.createdAt, }; await this.inventoryService.recordInventoryTransactionsFromItemsEntries( tenantId, transaction, override, trx ); } /** * Reverting the inventory transactions once the invoice deleted. * @param {number} tenantId - Tenant id. * @param {number} billId - Bill id. * @return {Promise} */ public async revertInventoryTransactions( tenantId: number, saleInvoiceId: number, trx?: Knex.Transaction ): Promise { // Delete the inventory transaction of the given sale invoice. const { oldInventoryTransactions } = await this.inventoryService.deleteInventoryTransactions( tenantId, saleInvoiceId, 'SaleInvoice', trx ); } /** * Retrieve sale invoice with associated entries. * @param {Number} saleInvoiceId - * @param {ISystemUser} authorizedUser - * @return {Promise} */ public async getSaleInvoice( tenantId: number, saleInvoiceId: number, authorizedUser: ISystemUser ): Promise { const { SaleInvoice } = this.tenancy.models(tenantId); const saleInvoice = await SaleInvoice.query() .findById(saleInvoiceId) .withGraphFetched('entries.item') .withGraphFetched('customer') .withGraphFetched('branch'); return this.transformer.transform( tenantId, saleInvoice, new SaleInvoiceTransformer() ); } /** * Parses the sale invoice list filter DTO. * @param filterDTO * @returns */ private parseListFilterDTO(filterDTO) { return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); } /** * Retrieve sales invoices filterable and paginated list. * @param {Request} req * @param {Response} res * @param {NextFunction} next */ public async salesInvoicesList( tenantId: number, filterDTO: ISalesInvoicesFilter ): Promise<{ salesInvoices: ISaleInvoice[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { SaleInvoice } = this.tenancy.models(tenantId); // Parses stringified filter roles. const filter = this.parseListFilterDTO(filterDTO); // Dynamic list service. const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, SaleInvoice, filter ); const { results, pagination } = await SaleInvoice.query() .onBuild((builder) => { builder.withGraphFetched('entries'); builder.withGraphFetched('customer'); dynamicFilter.buildQuery()(builder); }) .pagination(filter.page - 1, filter.pageSize); // Retrieves the transformed sale invoices. const salesInvoices = await this.transformer.transform( tenantId, results, new SaleInvoiceTransformer() ); return { salesInvoices, pagination, filterMeta: dynamicFilter.getResponseMeta(), }; } /** * Retrieve due sales invoices. * @param {number} tenantId * @param {number} customerId */ public async getPayableInvoices( tenantId: number, customerId?: number ): Promise { const { SaleInvoice } = this.tenancy.models(tenantId); const salesInvoices = await SaleInvoice.query().onBuild((query) => { query.modify('dueInvoices'); query.modify('delivered'); if (customerId) { query.where('customer_id', customerId); } }); return salesInvoices; } /** * Validate the given customer has no sales invoices. * @param {number} tenantId * @param {number} customerId - Customer id. */ public async validateCustomerHasNoInvoices( tenantId: number, customerId: number ) { const { SaleInvoice } = this.tenancy.models(tenantId); const invoices = await SaleInvoice.query().where('customer_id', customerId); if (invoices.length > 0) { throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES); } } /** * Validate the sale invoice has no applied to credit note transaction. * @param {number} tenantId * @param {number} invoiceId * @returns {Promise} */ public validateInvoiceHasNoAppliedToCredit = async ( tenantId: number, invoiceId: number ): Promise => { const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId); const appliedTransactions = await CreditNoteAppliedInvoice.query().where( 'invoiceId', invoiceId ); if (appliedTransactions.length > 0) { throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES); } }; }