Merge branch 'develop' into fix-spelling-a-char

This commit is contained in:
Ahmed Bouhuolia
2023-08-22 22:49:39 +02:00
301 changed files with 9345 additions and 7022 deletions

View File

@@ -0,0 +1,103 @@
import { Service, Inject } from 'typedi';
import { omit, sumBy } from 'lodash';
import * as R from 'ramda';
import moment from 'moment';
import composeAsync from 'async/compose';
import {
ISaleInvoice,
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
ICustomer,
ITenantUser,
} from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
import { formatDateFields } from 'utils';
@Service()
export class CommandSaleInvoiceDTOTransformer {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private validators: CommandSaleInvoiceValidators;
@Inject()
private invoiceIncrement: SaleInvoiceIncrement;
/**
* Transformes the create DTO to invoice object model.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
* @param {ISaleInvoice} oldSaleInvoice - Old sale invoice.
* @return {ISaleInvoice}
*/
public async transformDTOToModel(
tenantId: number,
customer: ICustomer,
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
authorizedUser: ITenantUser,
oldSaleInvoice?: ISaleInvoice
): Promise<ISaleInvoice> {
const { ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
// Retreive the next invoice number.
const autoNextNumber = this.invoiceIncrement.getNextInvoiceNumber(tenantId);
// Invoice number.
const invoiceNo =
saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber;
// Validate the invoice is required.
this.validators.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<ISaleInvoice>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
)(initialDTO);
}
}

View File

@@ -0,0 +1,86 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { SaleInvoice } from '@/models';
import { ERRORS } from './constants';
@Service()
export class CommandSaleInvoiceValidators {
@Inject()
private tenancy: HasTenancyService;
/**
* Validates the given invoice is existance.
* @param {SaleInvoice | undefined} invoice
*/
public validateInvoiceExistance(invoice: SaleInvoice | undefined) {
if (!invoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
}
/**
* Validate whether sale invoice number unqiue on the storage.
*/
public 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 invoice amount is bigger than payment amount before edit the invoice.
* @param {number} saleInvoiceAmount
* @param {number} paymentAmount
*/
public validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceAmount: number,
paymentAmount: number
) {
if (saleInvoiceAmount < paymentAmount) {
throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT);
}
}
/**
* Validate the invoice number require.
* @param {ISaleInvoice} saleInvoiceObj
*/
public validateInvoiceNoRequire(invoiceNo: string) {
if (!invoiceNo) {
throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED);
}
}
/**
* 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);
}
}
}

View File

@@ -0,0 +1,147 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ICustomer,
ISaleInvoice,
ISaleInvoiceCreateDTO,
ISaleInvoiceCreatedPayload,
ISaleInvoiceCreatingPaylaod,
ITenantUser,
} from '@/interfaces';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer';
import { SaleEstimateValidators } from '../Estimates/SaleEstimateValidators';
@Service()
export class CreateSaleInvoice {
@Inject()
private tenancy: TenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private validators: CommandSaleInvoiceValidators;
@Inject()
private transformerDTO: CommandSaleInvoiceDTOTransformer;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private commandEstimateValidators: SaleEstimateValidators;
/**
* 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<ISaleInvoice>}
*/
public createSaleInvoice = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
authorizedUser: ITenantUser
): Promise<ISaleInvoice> => {
const { SaleInvoice, SaleEstimate, 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 SaleEstimate.query()
.findById(saleInvoiceDTO.fromEstimateId)
.throwIfNotFound();
// Validate the sale estimate is not already converted to invoice.
this.commandEstimateValidators.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.validators.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;
});
};
/**
* 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.transformerDTO.transformDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
authorizedUser
);
};
}

View File

@@ -0,0 +1,154 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
ISystemUser,
ISaleInvoiceDeletePayload,
ISaleInvoiceDeletedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import { UnlinkConvertedSaleEstimate } from '../Estimates/UnlinkConvertedSaleEstimate';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class DeleteSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Validate the sale invoice has no payment entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
*/
private 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 sale invoice has no applied to credit note transaction.
* @param {number} tenantId
* @param {number} invoiceId
* @returns {Promise<void>}
*/
public validateInvoiceHasNoAppliedToCredit = async (
tenantId: number,
invoiceId: number
): Promise<void> => {
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);
}
};
/**
* Validate whether sale invoice exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private 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;
}
/**
* 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<void> {
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.unlockEstimateFromInvoice.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);
});
}
}

View File

@@ -0,0 +1,80 @@
import { Knex } from 'knex';
import moment from 'moment';
import { ServiceError } from '@/exceptions';
import {
ISaleInvoiceDeliveringPayload,
ISaleInvoiceEventDeliveredPayload,
ISystemUser,
} from '@/interfaces';
import { ERRORS } from './constants';
import { Inject, Service } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
@Service()
export class DeliverSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: CommandSaleInvoiceValidators;
/**
* Deliver the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
* @return {Promise<void>}
*/
public async deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retrieve details of the given sale invoice id.
const oldSaleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(oldSaleInvoice);
// 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)
.patchAndFetchById(saleInvoiceId, {
deliveredAt: moment().toMySqlDateTime(),
})
.withGraphFetched('entries');
// Triggers `onSaleInvoiceDelivered` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDelivered, {
tenantId,
saleInvoiceId,
saleInvoice,
trx,
} as ISaleInvoiceEventDeliveredPayload);
});
}
}

View File

@@ -0,0 +1,165 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import {
ICustomer,
ISaleInvoice,
ISaleInvoiceEditDTO,
ISaleInvoiceEditedPayload,
ISaleInvoiceEditingPayload,
ISystemUser,
ITenantUser,
} from '@/interfaces';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer';
import events from '@/subscribers/events';
@Service()
export class EditSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private validators: CommandSaleInvoiceValidators;
@Inject()
private transformerDTO: CommandSaleInvoiceDTOTransformer;
@Inject()
private uow: UnitOfWork;
/**
* 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<ISaleInvoice>}
*/
public async editSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceDTO: ISaleInvoiceEditDTO,
authorizedUser: ISystemUser
): Promise<ISaleInvoice> {
const { SaleInvoice, Contact } = this.tenancy.models(tenantId);
// Retrieve the sale invoice or throw not found service error.
const oldSaleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphJoined('entries');
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(oldSaleInvoice);
// 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.validators.validateInvoiceNumberUnique(
tenantId,
saleInvoiceObj.invoiceNo,
saleInvoiceId
);
}
// Validate the invoice amount is not smaller than the invoice payment amount.
this.validators.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;
});
}
/**
* 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.transformerDTO.transformDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
authorizedUser,
oldSaleInvoice
);
};
}

View File

@@ -4,7 +4,7 @@ import { InvoicePaymentTransactionTransformer } from './InvoicePaymentTransactio
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class InvoicePaymentsService {
export class GetInvoicePaymentsService {
@Inject()
private tenancy: HasTenancyService;

View File

@@ -0,0 +1,47 @@
import { Inject, Service } from 'typedi';
import { ISaleInvoice, ISystemUser } from '@/interfaces';
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
@Service()
export class GetSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private validators: CommandSaleInvoiceValidators;
/**
* Retrieve sale invoice with associated entries.
* @param {Number} saleInvoiceId -
* @param {ISystemUser} authorizedUser -
* @return {Promise<ISaleInvoice>}
*/
public async getSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('branch');
// Validates the given sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
return this.transformer.transform(
tenantId,
saleInvoice,
new SaleInvoiceTransformer()
);
}
}

View File

@@ -0,0 +1,80 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import {
IFilterMeta,
IPaginationMeta,
ISaleInvoice,
ISalesInvoicesFilter,
} from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
@Service()
export class GetSaleInvoices {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve sales invoices filterable and paginated list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async getSaleInvoices(
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(),
};
}
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import { ISaleInvoice } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class GetSaleInvoicesPayable {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieve due sales invoices.
* @param {number} tenantId
* @param {number} customerId
*/
public async getPayableInvoices(
tenantId: number,
customerId?: number
): Promise<ISaleInvoice> {
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;
}
}

View File

@@ -88,8 +88,8 @@ export class SaleInvoiceGLEntries {
/**
* Retrieves the given invoice ledger.
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @returns {ILedger}
*/
public getInvoiceGLedger = (
@@ -103,7 +103,7 @@ export class SaleInvoiceGLEntries {
/**
* Retrieves the invoice GL common entry.
* @param {ISaleInvoice} saleInvoice
* @param {ISaleInvoice} saleInvoice
* @returns {Partial<ILedgerEntry>}
*/
private getInvoiceGLCommonEntry = (
@@ -131,8 +131,8 @@ export class SaleInvoiceGLEntries {
/**
* Retrieve receivable entry of the given invoice.
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @returns {ILedgerEntry}
*/
private getInvoiceReceivableEntry = (
@@ -153,9 +153,9 @@ export class SaleInvoiceGLEntries {
/**
* Retrieve item income entry of the given invoice.
* @param {ISaleInvoice} saleInvoice -
* @param {IItemEntry} entry -
* @param {number} index -
* @param {ISaleInvoice} saleInvoice -
* @param {IItemEntry} entry -
* @param {number} index -
* @returns {ILedgerEntry}
*/
private getInvoiceItemEntry = R.curry(
@@ -183,8 +183,8 @@ export class SaleInvoiceGLEntries {
/**
* Retrieves the invoice GL entries.
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @returns {ILedgerEntry[]}
*/
public getInvoiceGLEntries = (

View File

@@ -0,0 +1,77 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ISaleInvoice } from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import InventoryService from '@/services/Inventory/Inventory';
@Service()
export class InvoiceInventoryTransactions {
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private inventoryService: InventoryService;
/**
* 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<void>}
*/
public async recordInventoryTranscactions(
tenantId: number,
saleInvoice: ISaleInvoice,
override?: boolean,
trx?: Knex.Transaction
): Promise<void> {
// 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<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
saleInvoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
// Delete the inventory transaction of the given sale invoice.
const { oldInventoryTransactions } =
await this.inventoryService.deleteInventoryTransactions(
tenantId,
saleInvoiceId,
'SaleInvoice',
trx
);
}
}

View File

@@ -21,6 +21,11 @@ export class InvoicePaymentTransactionTransformer extends Transformer {
});
};
/**
* Formatted payment date.
* @param entry
* @returns {string}
*/
protected formattedPaymentDate = (entry): string => {
return this.formatDate(entry.payment.paymentDate);
};

View File

@@ -1,7 +1,7 @@
import { Knex } from 'knex';
import async from 'async';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentReceiveGLEntries } from '../PaymentReceives/PaymentReceiveGLEntries';
@Service()

View File

@@ -64,7 +64,7 @@ export class SaleInvoiceCostGLEntries {
/**
*
* @param {IInventoryLotCost} inventoryCostLot
* @param {IInventoryLotCost} inventoryCostLot
* @returns {}
*/
private getInvoiceCostGLCommonEntry = (
@@ -91,7 +91,7 @@ export class SaleInvoiceCostGLEntries {
/**
* Retrieves the inventory cost GL entry.
* @param {IInventoryLotCost} inventoryLotCost
* @param {IInventoryLotCost} inventoryLotCost
* @returns {ILedgerEntry[]}
*/
private getInventoryCostGLEntry = R.curry(
@@ -127,10 +127,10 @@ export class SaleInvoiceCostGLEntries {
/**
* Writes journal entries for given sale invoice.
* -------
* -----
* - Cost of goods sold -> Debit -> YYYY
* - Inventory assets -> Credit -> YYYY
* --------
*-----
* @param {ISaleInvoice} saleInvoice
* @param {JournalPoster} journal
*/

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
@Service()
export class SaleInvoiceIncrement {
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
/**
* Retrieves the next unique invoice number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
public getNextInvoiceNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'sales_invoices'
);
}
/**
* Increment the invoice next number.
* @param {number} tenantId -
*/
public incrementNextInvoiceNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'sales_invoices'
);
}
}

View File

@@ -0,0 +1,260 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import {
ISaleInvoice,
ISaleInvoiceSmsDetailsDTO,
ISaleInvoiceSmsDetails,
SMS_NOTIFICATION_KEY,
InvoiceNotificationType,
ICustomer,
} from '@/interfaces';
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
import { formatSmsMessage, formatNumber } from 'utils';
import { TenantMetadata } from '@/system/models';
import SaleNotifyBySms from '../SaleNotifyBySms';
import { ServiceError } from '@/exceptions';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { ERRORS } from './constants';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
@Service()
export class SaleInvoiceNotifyBySms {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private smsNotificationsSettings: SmsNotificationsSettingsService;
@Inject()
private saleSmsNotification: SaleNotifyBySms;
@Inject()
private validators: CommandSaleInvoiceValidators;
/**
* Notify customer via sms about sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
*/
public notifyBySms = async (
tenantId: number,
saleInvoiceId: number,
invoiceNotificationType: InvoiceNotificationType
) => {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retrieve the sale invoice or throw not found service error.
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('customer');
// Validates the givne invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
// Validate the customer phone number existance and number validation.
this.saleSmsNotification.validateCustomerPhoneNumber(
saleInvoice.customer.personalPhone
);
// Transformes the invoice notification key to sms notification key.
const notificationKey = this.transformDTOKeyToNotificationKey(
invoiceNotificationType
);
// Triggers `onSaleInvoiceNotifySms` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onNotifySms, {
tenantId,
saleInvoice,
});
// Formattes the sms message and sends sms notification.
await this.sendSmsNotification(tenantId, notificationKey, saleInvoice);
// Triggers `onSaleInvoiceNotifySms` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onNotifiedSms, {
tenantId,
saleInvoice,
});
return saleInvoice;
};
/**
* Notify invoice details by sms notification after invoice creation.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns {Promise<void>}
*/
public notifyDetailsBySmsAfterCreation = async (
tenantId: number,
saleInvoiceId: number
): Promise<void> => {
const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
tenantId,
SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
);
// Can't continue if the sms auto-notification is not enabled.
if (!notification.isNotificationEnabled) return;
await this.notifyBySms(tenantId, saleInvoiceId, 'details');
};
/**
* Sends SMS notification.
* @param {ISaleInvoice} invoice
* @param {ICustomer} customer
* @returns {Promise<void>}
*/
private sendSmsNotification = async (
tenantId: number,
notificationType:
| SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
| SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
invoice: ISaleInvoice & { customer: ICustomer }
): Promise<void> => {
const smsClient = this.tenancy.smsClient(tenantId);
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
// Formates the given sms message.
const message = this.formattedInvoiceDetailsMessage(
tenantId,
notificationType,
invoice,
tenantMetadata
);
const phoneNumber = invoice.customer.personalPhone;
// Run the send sms notification message job.
await smsClient.sendMessageJob(phoneNumber, message);
};
/**
* Formates the invoice details sms message.
* @param {number} tenantId
* @param {ISaleInvoice} invoice
* @param {ICustomer} customer
* @returns {string}
*/
private formattedInvoiceDetailsMessage = (
tenantId: number,
notificationKey:
| SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
| SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
invoice: ISaleInvoice,
tenantMetadata: TenantMetadata
): string => {
const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
tenantId,
notificationKey
);
return this.formatInvoiceDetailsMessage(
notification.smsMessage,
invoice,
tenantMetadata
);
};
/**
* Formattees the given invoice details sms message.
* @param {string} smsMessage
* @param {ISaleInvoice} invoice
* @param {ICustomer} customer
* @param {TenantMetadata} tenantMetadata
*/
private formatInvoiceDetailsMessage = (
smsMessage: string,
invoice: ISaleInvoice & { customer: ICustomer },
tenantMetadata: TenantMetadata
) => {
const formattedDueAmount = formatNumber(invoice.dueAmount, {
currencyCode: invoice.currencyCode,
});
const formattedAmount = formatNumber(invoice.balance, {
currencyCode: invoice.currencyCode,
});
return formatSmsMessage(smsMessage, {
InvoiceNumber: invoice.invoiceNo,
ReferenceNumber: invoice.referenceNo,
CustomerName: invoice.customer.displayName,
DueAmount: formattedDueAmount,
DueDate: moment(invoice.dueDate).format('YYYY/MM/DD'),
Amount: formattedAmount,
CompanyName: tenantMetadata.name,
});
};
/**
* Retrieve the SMS details of the given invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
*/
public smsDetails = async (
tenantId: number,
saleInvoiceId: number,
invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO
): Promise<ISaleInvoiceSmsDetails> => {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retrieve the sale invoice or throw not found service error.
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('customer');
// Validates the sale invoice existance.
this.validateSaleInvoiceExistance(saleInvoice);
// Current tenant metadata.
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
// Transformes the invoice notification key to sms notification key.
const notificationKey = this.transformDTOKeyToNotificationKey(
invoiceSmsDetailsDTO.notificationKey
);
// Formates the given sms message.
const smsMessage = this.formattedInvoiceDetailsMessage(
tenantId,
notificationKey,
saleInvoice,
tenantMetadata
);
return {
customerName: saleInvoice.customer.displayName,
customerPhoneNumber: saleInvoice.customer.personalPhone,
smsMessage,
};
};
/**
* Transformes the invoice notification key DTO to notification key.
* @param {string} invoiceNotifKey
* @returns {SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
* | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER}
*/
private transformDTOKeyToNotificationKey = (
invoiceNotifKey: string
):
| SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
| SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER => {
const invoiceNotifKeyPairs = {
details: SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS,
reminder: SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
};
return (
invoiceNotifKeyPairs[invoiceNotifKey] ||
SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
);
};
/**
* Validates the sale invoice existance.
* @param {ISaleInvoice|null} saleInvoice
*/
private validateSaleInvoiceExistance(saleInvoice: ISaleInvoice | null) {
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
}
}

View File

@@ -0,0 +1,37 @@
import { Inject, Service } from 'typedi';
import PdfService from '@/services/PDF/PdfService';
import { templateRender } from 'utils';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Tenant } from '@/system/models';
@Service()
export class SaleInvoicePdf {
@Inject()
pdfService: PdfService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async saleInvoicePdf(tenantId: number, saleInvoice) {
const i18n = this.tenancy.i18n(tenantId);
const organization = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const htmlContent = templateRender('modules/invoice-regular', {
organization,
organizationName: organization.metadata.name,
organizationEmail: organization.metadata.email,
saleInvoice,
...i18n,
});
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
return pdfContent;
}
}

View File

@@ -0,0 +1,91 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class SaleInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedInvoiceDate',
'formattedDueDate',
'formattedAmount',
'formattedDueAmount',
'formattedPaymentAmount',
'formattedBalanceAmount',
'formattedExchangeRate',
];
};
/**
* Retrieve formatted invoice date.
* @param {ISaleInvoice} invoice
* @returns {String}
*/
protected formattedInvoiceDate = (invoice): string => {
return this.formatDate(invoice.invoiceDate);
};
/**
* Retrieve formatted due date.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedDueDate = (invoice): string => {
return this.formatDate(invoice.dueDate);
};
/**
* Retrieve formatted invoice amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedAmount = (invoice): string => {
return formatNumber(invoice.balance, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve formatted invoice due amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedDueAmount = (invoice): string => {
return formatNumber(invoice.dueAmount, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve formatted payment amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedPaymentAmount = (invoice): string => {
return formatNumber(invoice.paymentAmount, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve the formatted invoice balance.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedBalanceAmount = (invoice): string => {
return formatNumber(invoice.balanceAmount, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve the formatted exchange rate.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedExchangeRate = (invoice): string => {
return formatNumber(invoice.exchangeRate, { money: false });
};
}

View File

@@ -0,0 +1,104 @@
import { Service } from 'typedi';
import { ISaleInvoice, AccountNormal, ILedgerEntry, ILedger } from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export class SaleInvoiceWriteoffGLEntries {
/**
* Retrieves the invoice write-off common GL entry.
* @param {ISaleInvoice} saleInvoice
*/
private getInvoiceWriteoffGLCommonEntry = (saleInvoice: ISaleInvoice) => {
return {
date: saleInvoice.invoiceDate,
currencyCode: saleInvoice.currencyCode,
exchangeRate: saleInvoice.exchangeRate,
transactionId: saleInvoice.id,
transactionType: 'InvoiceWriteOff',
transactionNumber: saleInvoice.invoiceNo,
referenceNo: saleInvoice.referenceNo,
branchId: saleInvoice.branchId,
};
};
/**
* Retrieves the invoice write-off receiveable GL entry.
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @returns {ILedgerEntry}
*/
private getInvoiceWriteoffGLReceivableEntry = (
ARAccountId: number,
saleInvoice: ISaleInvoice
): ILedgerEntry => {
const commontEntry = this.getInvoiceWriteoffGLCommonEntry(saleInvoice);
return {
...commontEntry,
credit: saleInvoice.localWrittenoffAmount,
accountId: ARAccountId,
contactId: saleInvoice.customerId,
debit: 0,
index: 1,
indexGroup: 300,
accountNormal: saleInvoice.writtenoffExpenseAccount.accountNormal,
};
};
/**
* Retrieves the invoice write-off expense GL entry.
* @param {ISaleInvoice} saleInvoice
* @returns {ILedgerEntry}
*/
private getInvoiceWriteoffGLExpenseEntry = (
saleInvoice: ISaleInvoice
): ILedgerEntry => {
const commontEntry = this.getInvoiceWriteoffGLCommonEntry(saleInvoice);
return {
...commontEntry,
debit: saleInvoice.localWrittenoffAmount,
accountId: saleInvoice.writtenoffExpenseAccountId,
credit: 0,
index: 2,
indexGroup: 300,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the invoice write-off GL entries.
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @returns {ILedgerEntry[]}
*/
public getInvoiceWriteoffGLEntries = (
ARAccountId: number,
saleInvoice: ISaleInvoice
): ILedgerEntry[] => {
const creditEntry = this.getInvoiceWriteoffGLExpenseEntry(saleInvoice);
const debitEntry = this.getInvoiceWriteoffGLReceivableEntry(
ARAccountId,
saleInvoice
);
return [debitEntry, creditEntry];
};
/**
* Retrieves the invoice write-off ledger.
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @returns {Ledger}
*/
public getInvoiceWriteoffLedger = (
ARAccountId: number,
saleInvoice: ISaleInvoice
): ILedger => {
const entries = this.getInvoiceWriteoffGLEntries(ARAccountId, saleInvoice);
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,88 @@
import { Knex } from 'knex';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Service, Inject } from 'typedi';
import { SaleInvoiceWriteoffGLEntries } from './SaleInvoiceWriteoffGLEntries';
@Service()
export class SaleInvoiceWriteoffGLStorage {
@Inject()
private invoiceWriteoffLedger: SaleInvoiceWriteoffGLEntries;
@Inject()
private ledgerStorage: LedgerStorageService;
@Inject()
private tenancy: HasTenancyService;
/**
* Writes the invoice write-off GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public writeInvoiceWriteoffEntries = async (
tenantId: number,
saleInvoiceId: number,
trx?: Knex.Transaction
) => {
const { SaleInvoice } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Retrieves the sale invoice.
const saleInvoice = await SaleInvoice.query(trx)
.findById(saleInvoiceId)
.withGraphFetched('writtenoffExpenseAccount');
// Find or create the A/R account.
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode,
{},
trx
);
// Retrieves the invoice write-off ledger.
const ledger = this.invoiceWriteoffLedger.getInvoiceWriteoffLedger(
ARAccount.id,
saleInvoice
);
return this.ledgerStorage.commit(tenantId, ledger, trx);
};
/**
* Rewrites the invoice write-off GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {Knex.Transactio} actiontrx
* @returns {Promise<void>}
*/
public rewriteInvoiceWriteoffEntries = async (
tenantId: number,
saleInvoiceId: number,
trx?: Knex.Transaction
) => {
await this.revertInvoiceWriteoffEntries(tenantId, saleInvoiceId, trx);
await this.writeInvoiceWriteoffEntries(tenantId, saleInvoiceId, trx);
};
/**
* Reverts the invoice write-off GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public revertInvoiceWriteoffEntries = async (
tenantId: number,
saleInvoiceId: number,
trx?: Knex.Transaction
) => {
await this.ledgerStorage.deleteByReference(
tenantId,
saleInvoiceId,
'InvoiceWriteOff',
trx
);
};
}

View File

@@ -0,0 +1,58 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ISaleInvoiceWriteoffCreatePayload,
ISaleInvoiceWrittenOffCanceledPayload,
} from '@/interfaces';
import { SaleInvoiceWriteoffGLStorage } from './SaleInvoiceWriteoffGLStorage';
@Service()
export default class SaleInvoiceWriteoffSubscriber {
@Inject()
writeGLStorage: SaleInvoiceWriteoffGLStorage;
/**
* Attaches events.
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onWrittenoff,
this.writeJournalEntriesOnceWriteoffCreate
);
bus.subscribe(
events.saleInvoice.onWrittenoffCanceled,
this.revertJournalEntriesOnce
);
}
/**
* Write the written-off sale invoice journal entries.
* @param {ISaleInvoiceWriteoffCreatePayload}
*/
private writeJournalEntriesOnceWriteoffCreate = async ({
tenantId,
saleInvoice,
trx,
}: ISaleInvoiceWriteoffCreatePayload) => {
await this.writeGLStorage.writeInvoiceWriteoffEntries(
tenantId,
saleInvoice.id,
trx
);
};
/**
* Reverts the written-of sale invoice jounral entries.
* @param {ISaleInvoiceWrittenOffCanceledPayload}
*/
private revertJournalEntriesOnce = async ({
tenantId,
saleInvoice,
trx,
}: ISaleInvoiceWrittenOffCanceledPayload) => {
await this.writeGLStorage.revertInvoiceWriteoffEntries(
tenantId,
saleInvoice.id,
trx
);
};
}

View File

@@ -0,0 +1,282 @@
import {
IFilterMeta,
IPaginationMeta,
ISaleInvoice,
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
ISaleInvoiceSmsDetails,
ISaleInvoiceSmsDetailsDTO,
ISaleInvoiceWriteoffDTO,
ISalesInvoicesFilter,
ISystemUser,
ITenantUser,
InvoiceNotificationType,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { CreateSaleInvoice } from './CreateSaleInvoice';
import { DeleteSaleInvoice } from './DeleteSaleInvoice';
import { GetSaleInvoice } from './GetSaleInvoice';
import { EditSaleInvoice } from './EditSaleInvoice';
import { GetSaleInvoices } from './GetSaleInvoices';
import { DeliverSaleInvoice } from './DeliverSaleInvoice';
import { GetSaleInvoicesPayable } from './GetSaleInvoicesPayable';
import { WriteoffSaleInvoice } from './WriteoffSaleInvoice';
import { SaleInvoicePdf } from './SaleInvoicePdf';
import { GetInvoicePaymentsService } from './GetInvoicePaymentsService';
import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
@Service()
export class SaleInvoiceApplication {
@Inject()
private createSaleInvoiceService: CreateSaleInvoice;
@Inject()
private deleteSaleInvoiceService: DeleteSaleInvoice;
@Inject()
private getSaleInvoiceService: GetSaleInvoice;
@Inject()
private getSaleInvoicesService: GetSaleInvoices;
@Inject()
private editSaleInvoiceService: EditSaleInvoice;
@Inject()
private deliverSaleInvoiceService: DeliverSaleInvoice;
@Inject()
private getReceivableSaleInvoicesService: GetSaleInvoicesPayable;
@Inject()
private writeoffInvoiceService: WriteoffSaleInvoice;
@Inject()
private getInvoicePaymentsService: GetInvoicePaymentsService;
@Inject()
private pdfSaleInvoiceService: SaleInvoicePdf;
@Inject()
private invoiceSms: SaleInvoiceNotifyBySms;
/**
* Creates a new sale invoice with associated GL entries.
* @param {number} tenantId
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO
* @param {ITenantUser} authorizedUser
* @returns {Promise<ISaleInvoice>}
*/
public createSaleInvoice(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
authorizedUser: ITenantUser
): Promise<ISaleInvoice> {
return this.createSaleInvoiceService.createSaleInvoice(
tenantId,
saleInvoiceDTO,
authorizedUser
);
}
/**
* Edits the given sale invoice with associated GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO
* @param {ISystemUser} authorizedUser
* @returns {Promise<ISaleInvoice>}
*/
public editSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceDTO: ISaleInvoiceEditDTO,
authorizedUser: ISystemUser
) {
return this.editSaleInvoiceService.editSaleInvoice(
tenantId,
saleInvoiceId,
saleInvoiceDTO,
authorizedUser
);
}
/**
* Deletes the given sale invoice with given associated GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISystemUser} authorizedUser
* @returns {Promise<void>}
*/
public deleteSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<void> {
return this.deleteSaleInvoiceService.deleteSaleInvoice(
tenantId,
saleInvoiceId,
authorizedUser
);
}
/**
* Retrieves the given sale invoice details.
* @param {number} tenantId
* @param {ISalesInvoicesFilter} filterDTO
* @returns
*/
public getSaleInvoices(
tenantId: number,
filterDTO: ISalesInvoicesFilter
): Promise<{
salesInvoices: ISaleInvoice[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
return this.getSaleInvoicesService.getSaleInvoices(tenantId, filterDTO);
}
/**
* Retrieves sale invoice details.
* @param {number} tenantId -
* @param {number} saleInvoiceId -
* @param {ISystemUser} authorizedUser -
* @return {Promise<ISaleInvoice>}
*/
public getSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
) {
return this.getSaleInvoiceService.getSaleInvoice(
tenantId,
saleInvoiceId,
authorizedUser
);
}
/**
* Mark the given sale invoice as delivered.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISystemUser} authorizedUser
* @returns {}
*/
public deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
) {
return this.deliverSaleInvoiceService.deliverSaleInvoice(
tenantId,
saleInvoiceId,
authorizedUser
);
}
/**
* Retrieves the receivable sale invoices of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @returns
*/
public getReceivableSaleInvoices(tenantId: number, customerId?: number) {
return this.getReceivableSaleInvoicesService.getPayableInvoices(
tenantId,
customerId
);
}
/**
* Writes-off the sale invoice on bad debt expense account.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO
* @return {Promise<ISaleInvoice>}
*/
public writeOff = async (
tenantId: number,
saleInvoiceId: number,
writeoffDTO: ISaleInvoiceWriteoffDTO
): Promise<ISaleInvoice> => {
return this.writeoffInvoiceService.writeOff(
tenantId,
saleInvoiceId,
writeoffDTO
);
};
/**
* Cancels the written-off sale invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns {Promise<ISaleInvoice>}
*/
public cancelWrittenoff = (
tenantId: number,
saleInvoiceId: number
): Promise<ISaleInvoice> => {
return this.writeoffInvoiceService.cancelWrittenoff(
tenantId,
saleInvoiceId
);
};
/**
* Retrieve the invoice assocaited payments transactions.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Invoice id.
*/
public getInvoicePayments = async (tenantId: number, invoiceId: number) => {
return this.getInvoicePaymentsService.getInvoicePayments(
tenantId,
invoiceId
);
};
/**
*
* @param {number} tenantId ]
* @param saleInvoice
* @returns
*/
public saleInvoicePdf(tenantId: number, saleInvoice) {
return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoice);
}
/**
*
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {InvoiceNotificationType} invoiceNotificationType
*/
public notifySaleInvoiceBySms = async (
tenantId: number,
saleInvoiceId: number,
invoiceNotificationType: InvoiceNotificationType
) => {
return this.invoiceSms.notifyBySms(
tenantId,
saleInvoiceId,
invoiceNotificationType
);
};
/**
* Retrieves the SMS details of the given invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
*/
public getSaleInvoiceSmsDetails = async (
tenantId: number,
saleInvoiceId: number,
invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO
): Promise<ISaleInvoiceSmsDetails> => {
return this.invoiceSms.smsDetails(
tenantId,
saleInvoiceId,
invoiceSmsDetailsDTO
);
};
}

View File

@@ -0,0 +1,146 @@
import { Container, Service, Inject } from 'typedi';
import { chain } from 'lodash';
import moment from 'moment';
import { Knex } from 'knex';
import InventoryService from '@/services/Inventory/Inventory';
import {
IInventoryCostLotsGLEntriesWriteEvent,
IInventoryTransaction,
} from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class SaleInvoicesCost {
@Inject()
private inventoryService: InventoryService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Schedule sale invoice re-compute based on the item
* cost method and starting date.
* @param {number[]} itemIds - Inventory items ids.
* @param {Date} startingDate - Starting compute cost date.
* @return {Promise<Agenda>}
*/
async scheduleComputeCostByItemsIds(
tenantId: number,
inventoryItemsIds: number[],
startingDate: Date
): Promise<void> {
const asyncOpers: Promise<[]>[] = [];
inventoryItemsIds.forEach((inventoryItemId: number) => {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
tenantId,
inventoryItemId,
startingDate
);
asyncOpers.push(oper);
});
await Promise.all([...asyncOpers]);
}
/**
* Retrieve the max dated inventory transactions in the transactions that
* have the same item id.
* @param {IInventoryTransaction[]} inventoryTransactions
* @return {IInventoryTransaction[]}
*/
getMaxDateInventoryTransactions(
inventoryTransactions: IInventoryTransaction[]
): IInventoryTransaction[] {
return chain(inventoryTransactions)
.reduce((acc: any, transaction) => {
const compatatorDate = acc[transaction.itemId];
if (
!compatatorDate ||
moment(compatatorDate.date).isBefore(transaction.date)
) {
return {
...acc,
[transaction.itemId]: {
...transaction,
},
};
}
return acc;
}, {})
.values()
.value();
}
/**
* Computes items costs by the given inventory transaction.
* @param {number} tenantId
* @param {IInventoryTransaction[]} inventoryTransactions
*/
async computeItemsCostByInventoryTransactions(
tenantId: number,
inventoryTransactions: IInventoryTransaction[]
) {
const asyncOpers: Promise<[]>[] = [];
const reducedTransactions = this.getMaxDateInventoryTransactions(
inventoryTransactions
);
reducedTransactions.forEach((transaction) => {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
tenantId,
transaction.itemId,
transaction.date
);
asyncOpers.push(oper);
});
await Promise.all([...asyncOpers]);
}
/**
* Schedule writing journal entries.
* @param {Date} startingDate
* @return {Promise<agenda>}
*/
scheduleWriteJournalEntries(tenantId: number, startingDate?: Date) {
const agenda = Container.get('agenda');
return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
startingDate,
tenantId,
});
}
/**
* Writes cost GL entries from the inventory cost lots.
* @param {number} tenantId -
* @param {Date} startingDate -
* @returns {Promise<void>}
*/
public writeCostLotsGLEntries = (tenantId: number, startingDate: Date) => {
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers event `onInventoryCostLotsGLEntriesBeforeWrite`.
await this.eventPublisher.emitAsync(
events.inventory.onCostLotsGLEntriesBeforeWrite,
{
tenantId,
startingDate,
trx,
} as IInventoryCostLotsGLEntriesWriteEvent
);
// Triggers event `onInventoryCostLotsGLEntriesWrite`.
await this.eventPublisher.emitAsync(
events.inventory.onCostLotsGLEntriesWrite,
{
tenantId,
startingDate,
trx,
} as IInventoryCostLotsGLEntriesWriteEvent
);
});
};
}

View File

@@ -0,0 +1,161 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
ISaleInvoice,
ISaleInvoiceWriteoffCreatePayload,
ISaleInvoiceWriteoffDTO,
ISaleInvoiceWrittenOffCanceledPayload,
ISaleInvoiceWrittenOffCancelPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import { ServiceError } from '@/exceptions';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { ERRORS } from './constants';
@Service()
export class WriteoffSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: CommandSaleInvoiceValidators;
/**
* Writes-off the sale invoice on bad debt expense account.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO
* @return {Promise<ISaleInvoice>}
*/
public writeOff = async (
tenantId: number,
saleInvoiceId: number,
writeoffDTO: ISaleInvoiceWriteoffDTO
): Promise<ISaleInvoice> => {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
// Validate the sale invoice whether already written-off.
this.validateSaleInvoiceAlreadyWrittenoff(saleInvoice);
// Saves the invoice write-off transaction with associated transactions
// under unit-of-work envirmenet.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
const eventPayload = {
tenantId,
saleInvoiceId,
saleInvoice,
writeoffDTO,
trx,
} as ISaleInvoiceWriteoffCreatePayload;
// Triggers `onSaleInvoiceWriteoff` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWriteoff,
eventPayload
);
// Mark the sale invoice as written-off.
const newSaleInvoice = await SaleInvoice.query(trx)
.patch({
writtenoffExpenseAccountId: writeoffDTO.expenseAccountId,
writtenoffAmount: saleInvoice.dueAmount,
writtenoffAt: new Date(),
})
.findById(saleInvoiceId);
// Triggers `onSaleInvoiceWrittenoff` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWrittenoff,
eventPayload
);
return newSaleInvoice;
});
};
/**
* Cancels the written-off sale invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns {Promise<ISaleInvoice>}
*/
public cancelWrittenoff = async (
tenantId: number,
saleInvoiceId: number
): Promise<ISaleInvoice> => {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Validate the sale invoice existance.
// Retrieve the sale invoice or throw not found service error.
const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
// Validate the sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
// Validate the sale invoice whether already written-off.
this.validateSaleInvoiceNotWrittenoff(saleInvoice);
// Cancels the invoice written-off and removes the associated transactions.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceWrittenoffCancel` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWrittenoffCancel,
{
tenantId,
saleInvoice,
trx,
} as ISaleInvoiceWrittenOffCancelPayload
);
// Mark the sale invoice as written-off.
const newSaleInvoice = await SaleInvoice.query(trx)
.patch({
writtenoffAmount: null,
writtenoffAt: null,
})
.findById(saleInvoiceId);
// Triggers `onSaleInvoiceWrittenoffCanceled`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWrittenoffCanceled,
{
tenantId,
saleInvoice,
trx,
} as ISaleInvoiceWrittenOffCanceledPayload
);
return newSaleInvoice;
});
};
/**
* Should sale invoice not be written-off.
* @param {ISaleInvoice} saleInvoice
*/
private validateSaleInvoiceNotWrittenoff(saleInvoice: ISaleInvoice) {
if (!saleInvoice.isWrittenoff) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_WRITTEN_OFF);
}
}
/**
* Should sale invoice already written-off.
* @param {ISaleInvoice} saleInvoice
*/
private validateSaleInvoiceAlreadyWrittenoff(saleInvoice: ISaleInvoice) {
if (saleInvoice.isWrittenoff) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_WRITTEN_OFF);
}
}
}

View File

@@ -0,0 +1,78 @@
export const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT:
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES:
'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES',
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF',
};
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'Draft',
slug: 'draft',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Delivered',
slug: 'delivered',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'delivered',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Unpaid',
slug: 'unpaid',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Partially paid',
slug: 'partially-paid',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'partially-paid',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Paid',
slug: 'paid',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'paid' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
];