refactor: wip to nestjs

This commit is contained in:
Ahmed Bouhuolia
2024-12-25 00:43:55 +02:00
parent 336171081e
commit a6932d76f3
249 changed files with 21314 additions and 1616 deletions

View File

@@ -0,0 +1,151 @@
import { Inject, Injectable } from '@nestjs/common';
import { omit, sumBy } from 'lodash';
import * as R from 'ramda';
import moment from 'moment';
import composeAsync from 'async/compose';
import {
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
} from '../SaleInvoice.types';
import { Customer } from '@/modules/Customers/models/Customer';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { ItemEntry } from '@/modules/Items/models/ItemEntry';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement.service';
import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer';
import { SaleInvoice } from '../models/SaleInvoice';
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
import { formatDateFields } from '@/utils/format-date-fields';
import { ItemEntriesTaxTransactions } from '@/modules/TaxRates/ItemEntriesTaxTransactions.service';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class CommandSaleInvoiceDTOTransformer {
constructor(
private branchDTOTransform: BranchTransactionDTOTransformer,
private warehouseDTOTransform: WarehouseTransactionDTOTransform,
private itemsEntriesService: ItemsEntriesService,
private validators: CommandSaleInvoiceValidators,
private invoiceIncrement: SaleInvoiceIncrement,
private taxDTOTransformer: ItemEntriesTaxTransactions,
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
private tenancyContext: TenancyContext,
@Inject(SaleInvoice.name) private saleInvoiceModel: typeof SaleInvoice,
) {}
/**
* 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(
customer: Customer,
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
oldSaleInvoice?: SaleInvoice,
): Promise<SaleInvoice> {
const entriesModels = this.transformDTOEntriesToModels(saleInvoiceDTO);
const amount = this.getDueBalanceItemEntries(entriesModels);
// Retreive the next invoice number.
const autoNextNumber = this.invoiceIncrement.getNextInvoiceNumber();
// Retrieve the authorized user.
const authorizedUser = await this.tenancyContext.getSystemUser();
// 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',
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
...entry,
}));
const asyncEntries = await composeAsync(
// Associate tax rate from tax id to entries.
this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries,
// Associate tax rate id from tax code to entries.
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries,
// Sets default cost and sell account to invoice items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts,
)(initialEntries);
const entries = R.compose(
// Remove tax code from entries.
R.map(R.omit(['taxCode'])),
// Associate the default index for each item entry lin.
assocItemEntriesDefaultIndex,
)(asyncEntries);
const initialDTO = {
...formatDateFields(
omit(saleInvoiceDTO, [
'delivered',
'entries',
'fromEstimateId',
'attachments',
]),
['invoiceDate', 'dueDate'],
),
// Avoid rewrite the deliver date in edit mode when already published.
balance: amount,
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 SaleInvoice;
const initialAsyncDTO = await composeAsync(
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'SaleInvoice',
),
)(initialDTO);
return R.compose(
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
this.branchDTOTransform.transformDTO<SaleInvoice>,
this.warehouseDTOTransform.transformDTO<SaleInvoice>,
)(initialAsyncDTO);
}
/**
* Transforms the DTO entries to invoice entries models.
* @param {ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO} entries
* @returns {IItemEntry[]}
*/
private transformDTOEntriesToModels = (
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
): ItemEntry[] => {
return saleInvoiceDTO.entries.map((entry) => {
return ItemEntry.fromJson({
...entry,
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
});
});
};
/**
* Gets the due balance from the invoice entries.
* @param {IItemEntry[]} entries
* @returns {number}
*/
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
return sumBy(entries, (e) => e.amount);
};
}

View File

@@ -0,0 +1,79 @@
import { Injectable } from '@nestjs/common';
import { SaleInvoice } from '../models/SaleInvoice';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../constants';
@Injectable()
export class CommandSaleInvoiceValidators {
constructor(private readonly saleInvoiceModel: typeof SaleInvoice) {}
/**
* 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.
* @param {string} invoiceNumber -
* @param {number} notInvoiceId -
*/
public async validateInvoiceNumberUnique(
invoiceNumber: string,
notInvoiceId?: number,
) {
const saleInvoice = await this.saleInvoiceModel
.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} customerId - Customer id.
*/
public async validateCustomerHasNoInvoices(customerId: number) {
const invoices = await this.saleInvoiceModel
.query()
.where('customer_id', customerId);
if (invoices.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
}

View File

@@ -0,0 +1,133 @@
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Knex } from 'knex';
import {
ISaleInvoiceCreateDTO,
ISaleInvoiceCreatedPayload,
ISaleInvoiceCreatingPaylaod,
} from '../SaleInvoice.types';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { SaleEstimateValidators } from '@/modules/SaleEstimates/commands/SaleEstimateValidators.service';
import { SaleInvoice } from '../models/SaleInvoice';
import { SaleEstimate } from '@/modules/SaleEstimates/models/SaleEstimate';
import { Customer } from '@/modules/Customers/models/Customer';
import { events } from '@/common/events/events';
@Injectable()
export class CreateSaleInvoice {
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly validators: CommandSaleInvoiceValidators,
private readonly transformerDTO: CommandSaleInvoiceDTOTransformer,
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly commandEstimateValidators: SaleEstimateValidators,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: typeof SaleInvoice,
@Inject(SaleEstimate.name)
private readonly saleEstimateModel: typeof SaleEstimate,
@Inject(Customer.name)
private readonly customerModel: typeof Customer,
) {}
/**
* 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 (
saleInvoiceDTO: ISaleInvoiceCreateDTO,
// authorizedUser: ITenantUser,
trx?: Knex.Transaction,
): Promise<SaleInvoice> => {
// Validate customer existance.
const customer = await this.customerModel
.query()
.findById(saleInvoiceDTO.customerId)
.throwIfNotFound();
// Validate the from estimate id exists on the storage.
if (saleInvoiceDTO.fromEstimateId) {
const fromEstimate = await this.saleEstimateModel
.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(
saleInvoiceDTO.entries,
);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
saleInvoiceDTO.entries,
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformCreateDTOToModel(
customer,
saleInvoiceDTO,
// authorizedUser,
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validators.validateInvoiceNumberUnique(
saleInvoiceObj.invoiceNo,
);
}
// Creates a new sale invoice and associated transactions under unit of work env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceCreating` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, {
saleInvoiceDTO,
trx,
} as ISaleInvoiceCreatingPaylaod);
// Create sale invoice graph to the storage.
const saleInvoice = await this.saleInvoiceModel
.query(trx)
.upsertGraph(saleInvoiceObj);
const eventPayload: ISaleInvoiceCreatedPayload = {
saleInvoice,
saleInvoiceDTO,
saleInvoiceId: saleInvoice.id,
trx,
};
// Triggers the event `onSaleInvoiceCreated`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onCreated,
eventPayload,
);
return saleInvoice;
}, trx);
};
/**
* Transformes create DTO to model.
* @param {number} tenantId -
* @param {ICustomer} customer -
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO -
*/
private transformCreateDTOToModel = async (
customer: Customer,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
// authorizedUser: SystemUser,
) => {
return this.transformerDTO.transformDTOToModel(
customer,
saleInvoiceDTO,
// authorizedUser,
);
};
}

View File

@@ -0,0 +1,125 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
ISaleInvoiceDeletePayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceDeletingPayload,
} from '../SaleInvoice.types';
import { ItemEntry } from '@/modules/Items/models/ItemEntry';
import { SaleInvoice } from '../models/SaleInvoice';
import { UnlinkConvertedSaleEstimate } from '@/modules/SaleEstimates/commands/UnlinkConvertedSaleEstimate.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../constants';
import { events } from '@/common/events/events';
import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry';
import CreditNoteAppliedInvoice from '@/modules/CreditNotes/models/CreditNoteAppliedInvoice';
@Injectable()
export class DeleteSaleInvoice {
constructor(
@Inject(PaymentReceivedEntry)
private paymentReceivedEntryModel: typeof PaymentReceivedEntry,
@Inject(CreditNoteAppliedInvoice)
private creditNoteAppliedInvoiceModel: typeof CreditNoteAppliedInvoice,
@Inject(SaleInvoice)
private saleInvoiceModel: typeof SaleInvoice,
private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate,
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
) {}
/**
* Validate the sale invoice has no payment entries.
* @param {number} saleInvoiceId
*/
private async validateInvoiceHasNoPaymentEntries(saleInvoiceId: number) {
// Retrieve the sale invoice associated payment receive entries.
const entries = await this.paymentReceivedEntryModel
.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} invoiceId - Invoice id.
* @returns {Promise<void>}
*/
public validateInvoiceHasNoAppliedToCredit = async (
invoiceId: number,
): Promise<void> => {
const appliedTransactions = await this.creditNoteAppliedInvoiceModel
.query()
.where('invoiceId', invoiceId);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES);
}
};
/**
* Deletes the given sale invoice with associated entries
* and journal transactions.
* @param {Number} saleInvoiceId - The given sale invoice id.
* @param {ISystemUser} authorizedUser -
*/
public async deleteSaleInvoice(saleInvoiceId: number): Promise<void> {
// Retrieve the given sale invoice with associated entries
// or throw not found error.
const oldSaleInvoice = await this.saleInvoiceModel
.query()
.findById(saleInvoiceId)
.withGraphFetched('entries')
.withGraphFetched('paymentMethods')
.throwIfNotFound();
// Validate the sale invoice has no associated payment entries.
await this.validateInvoiceHasNoPaymentEntries(saleInvoiceId);
// Validate the sale invoice has applied to credit note transaction.
await this.validateInvoiceHasNoAppliedToCredit(saleInvoiceId);
// Triggers `onSaleInvoiceDelete` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDelete, {
oldSaleInvoice,
saleInvoiceId,
} as ISaleInvoiceDeletePayload);
// Deletes sale invoice transaction and associate transactions with UOW env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceDeleting` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, {
oldSaleInvoice,
saleInvoiceId,
trx,
} as ISaleInvoiceDeletingPayload);
// Unlink the converted sale estimates from the given sale invoice.
await this.unlockEstimateFromInvoice.unlinkConvertedEstimateFromInvoice(
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, {
oldSaleInvoice,
saleInvoiceId,
trx,
} as ISaleInvoiceDeletedPayload);
});
}
}

View File

@@ -0,0 +1,69 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import moment from 'moment';
import {
ISaleInvoiceDeliveringPayload,
ISaleInvoiceEventDeliveredPayload,
} from '../SaleInvoice.types';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { ERRORS } from '../constants';
import { SaleInvoice } from '../models/SaleInvoice';
import { ServiceError } from '@/modules/Items/ServiceError';
@Injectable()
export class DeliverSaleInvoice {
constructor(
private eventEmitter: EventEmitter2,
private uow: UnitOfWork,
private validators: CommandSaleInvoiceValidators,
@Inject(SaleInvoice.name) private saleInvoiceModel: typeof SaleInvoice,
) {}
/**
* Deliver the given sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @return {Promise<void>}
*/
public async deliverSaleInvoice(saleInvoiceId: number): Promise<void> {
// Retrieve details of the given sale invoice id.
const oldSaleInvoice = await this.saleInvoiceModel
.query()
.findById(saleInvoiceId);
// Validates the given invoice existence.
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 associate transactions
// under unit-of-work environment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceDelivering` event.
await this.eventEmitter.emitAsync(events.saleInvoice.onDelivering, {
oldSaleInvoice,
trx,
} as ISaleInvoiceDeliveringPayload);
// Record the delivered at on the storage.
const saleInvoice = await this.saleInvoiceModel
.query(trx)
.patchAndFetchById(saleInvoiceId, {
deliveredAt: moment().toMySqlDateTime(),
})
.withGraphFetched('entries');
// Triggers `onSaleInvoiceDelivered` event.
await this.eventEmitter.emitAsync(events.saleInvoice.onDelivered, {
saleInvoiceId,
saleInvoice,
trx,
} as ISaleInvoiceEventDeliveredPayload);
});
}
}

View File

@@ -0,0 +1,142 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import {
ISaleInvoiceEditDTO,
ISaleInvoiceEditedPayload,
ISaleInvoiceEditingPayload,
} from '../SaleInvoice.types';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { events } from '@/common/events/events';
import { SaleInvoice } from '../models/SaleInvoice';
import { Customer } from '@/modules/Customers/models/Customer';
@Injectable()
export class EditSaleInvoice {
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly eventPublisher: EventEmitter2,
private readonly validators: CommandSaleInvoiceValidators,
private readonly transformerDTO: CommandSaleInvoiceDTOTransformer,
private readonly uow: UnitOfWork,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: typeof SaleInvoice,
@Inject(Customer.name) private readonly customerModel: typeof Customer,
) {}
/**
* Edit the given sale invoice.
* @async
* @param {Number} saleInvoiceId - Sale invoice id.
* @param {ISaleInvoice} saleInvoice - Sale invoice DTO object.
* @return {Promise<ISaleInvoice>}
*/
public async editSaleInvoice(
saleInvoiceId: number,
saleInvoiceDTO: ISaleInvoiceEditDTO,
): Promise<SaleInvoice> {
// Retrieve the sale invoice or throw not found service error.
const oldSaleInvoice = await this.saleInvoiceModel
.query()
.findById(saleInvoiceId)
.withGraphJoined('entries');
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(oldSaleInvoice);
// Validate customer existance.
const customer = await this.customerModel
.query()
.findById(saleInvoiceDTO.customerId)
.throwIfNotFound();
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
saleInvoiceDTO.entries,
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
saleInvoiceDTO.entries,
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
saleInvoiceId,
'SaleInvoice',
saleInvoiceDTO.entries,
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.tranformEditDTOToModel(
customer,
saleInvoiceDTO,
oldSaleInvoice,
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validators.validateInvoiceNumberUnique(
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(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceEditing` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, {
trx,
oldSaleInvoice,
saleInvoiceDTO,
} as ISaleInvoiceEditingPayload);
// Upsert the the invoice graph to the storage.
const saleInvoice = await this.saleInvoiceModel
.query()
.upsertGraphAndFetch({
id: saleInvoiceId,
...saleInvoiceObj,
});
// Edit event payload.
const editEventPayload: ISaleInvoiceEditedPayload = {
saleInvoiceId,
saleInvoice,
saleInvoiceDTO,
oldSaleInvoice,
trx,
};
// Triggers `onSaleInvoiceEdited` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onEdited,
editEventPayload,
);
return saleInvoice;
});
}
/**
* Transformes edit DTO to model.
* @param {ICustomer} customer -
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO -
* @param {ISaleInvoice} oldSaleInvoice
*/
private tranformEditDTOToModel = async (
customer: Customer,
saleInvoiceDTO: ISaleInvoiceEditDTO,
oldSaleInvoice: SaleInvoice,
// authorizedUser: ITenantUser,
) => {
return this.transformerDTO.transformDTOToModel(
customer,
saleInvoiceDTO,
oldSaleInvoice,
// authorizedUser,
);
};
}

View File

@@ -0,0 +1,73 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { v4 as uuidv4 } from 'uuid';
import { GeneratePaymentLinkTransformer } from './GeneratePaymentLink.transformer';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { events } from '@/common/events/events';
import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink';
import { SaleInvoice } from '../models/SaleInvoice';
@Injectable()
export class GenerateShareLink {
constructor(
private uow: UnitOfWork,
private eventPublisher: EventEmitter2,
private transformer: TransformerInjectable,
@Inject(SaleInvoice) private saleInvoiceModel: typeof SaleInvoice,
@Inject(PaymentLink) private paymentLinkModel: typeof PaymentLink,
) {}
/**
* Generates private or public payment link for the given sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {string} publicity - Public or private.
* @param {string} expiryTime - Expiry time.
*/
async generatePaymentLink(
saleInvoiceId: number,
publicity: string = 'private',
expiryTime: string = ''
) {
const foundInvoice = await this.saleInvoiceModel.query()
.findById(saleInvoiceId)
.throwIfNotFound();
// Generate unique uuid for sharable link.
const linkId = uuidv4() as string;
const commonEventPayload = {
saleInvoiceId,
publicity,
expiryTime,
};
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onPublicSharableLinkGenerating` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onPublicLinkGenerating,
{ ...commonEventPayload, trx }
);
const paymentLink = await this.paymentLinkModel.query().insert({
linkId,
publicity,
resourceId: foundInvoice.id,
resourceType: 'SaleInvoice',
});
// Triggers `onPublicSharableLinkGenerated` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onPublicLinkGenerated,
{
...commonEventPayload,
paymentLink,
trx,
}
);
return this.transformer.transform(
paymentLink,
new GeneratePaymentLinkTransformer()
);
});
}
}

View File

@@ -0,0 +1,28 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { PUBLIC_PAYMENT_LINK } from '../constants';
export class GeneratePaymentLinkTransformer extends Transformer {
/**
* Exclude these attributes from payment link object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['linkId'];
};
/**
* Included attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['link'];
};
/**
* Retrieves the public/private payment linl
* @returns {string}
*/
public link(link) {
return PUBLIC_PAYMENT_LINK?.replace('{PAYMENT_LINK_ID}', link.linkId);
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { AutoIncrementOrdersService } from '../../AutoIncrementOrders/AutoIncrementOrders.service';
@Injectable()
export class SaleInvoiceIncrement {
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
) {}
/**
* Retrieves the next unique invoice number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
public getNextInvoiceNumber(): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
'sales_invoices',
);
}
/**
* Increment the invoice next number.
* @param {number} tenantId -
*/
public incrementNextInvoiceNumber() {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'sales_invoices',
);
}
}

View File

@@ -0,0 +1,129 @@
// import { Inject, Service } from 'typedi';
// import { SaleInvoiceMailOptions } from '@/interfaces';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import { GetSaleInvoice } from '../queries/GetSaleInvoice.service';
// import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
// import {
// DEFAULT_INVOICE_MAIL_CONTENT,
// DEFAULT_INVOICE_MAIL_SUBJECT,
// } from '../constants';
// import { GetInvoicePaymentMail } from '../queries/GetInvoicePaymentMail.service';
// import { GenerateShareLink } from './GenerateInvoicePaymentLink.service';
// @Service()
// export class SendSaleInvoiceMailCommon {
// constructor(
// private getSaleInvoiceService: GetSaleInvoice,
// private contactMailNotification: ContactMailNotification,
// private getInvoicePaymentMail: GetInvoicePaymentMail,
// private generatePaymentLinkService: GenerateShareLink,
// ) {}
// /**
// * Retrieves the mail options.
// * @param {number} tenantId - Tenant id.
// * @param {number} invoiceId - Invoice id.
// * @param {string} defaultSubject - Subject text.
// * @param {string} defaultBody - Subject body.
// * @returns {Promise<SaleInvoiceMailOptions>}
// */
// public async getInvoiceMailOptions(
// invoiceId: number,
// defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
// defaultMessage: string = DEFAULT_INVOICE_MAIL_CONTENT,
// ): Promise<SaleInvoiceMailOptions> {
// const { SaleInvoice } = this.tenancy.models(tenantId);
// const saleInvoice = await SaleInvoice.query()
// .findById(invoiceId)
// .throwIfNotFound();
// const contactMailDefaultOptions =
// await this.contactMailNotification.getDefaultMailOptions(
// tenantId,
// saleInvoice.customerId,
// );
// const formatArgs = await this.getInvoiceFormatterArgs(tenantId, invoiceId);
// return {
// ...contactMailDefaultOptions,
// subject: defaultSubject,
// message: defaultMessage,
// attachInvoice: true,
// formatArgs,
// };
// }
// /**
// * Formats the given invoice mail options.
// * @param {number} tenantId
// * @param {number} invoiceId
// * @param {SaleInvoiceMailOptions} mailOptions
// * @returns {Promise<SaleInvoiceMailOptions>}
// */
// public async formatInvoiceMailOptions(
// tenantId: number,
// invoiceId: number,
// mailOptions: SaleInvoiceMailOptions,
// ): Promise<SaleInvoiceMailOptions> {
// const formatterArgs = await this.getInvoiceFormatterArgs(
// tenantId,
// invoiceId,
// );
// const formattedOptions =
// await this.contactMailNotification.formatMailOptions(
// tenantId,
// mailOptions,
// formatterArgs,
// );
// // Generates the a new payment link for the given invoice.
// const paymentLink =
// await this.generatePaymentLinkService.generatePaymentLink(
// tenantId,
// invoiceId,
// 'public',
// );
// const message = await this.getInvoicePaymentMail.getMailTemplate(
// tenantId,
// invoiceId,
// {
// // # Invoice message
// invoiceMessage: formattedOptions.message,
// preview: formattedOptions.message,
// // # Payment link
// viewInvoiceButtonUrl: paymentLink.link,
// },
// );
// return { ...formattedOptions, message };
// }
// /**
// * Retrieves the formatted text of the given sale invoice.
// * @param {number} tenantId - Tenant id.
// * @param {number} invoiceId - Sale invoice id.
// * @param {string} text - The given text.
// * @returns {Promise<string>}
// */
// public getInvoiceFormatterArgs = async (
// tenantId: number,
// invoiceId: number,
// ): Promise<Record<string, string | number>> => {
// const invoice = await this.getSaleInvoiceService.getSaleInvoice(
// tenantId,
// invoiceId,
// );
// const commonArgs =
// await this.contactMailNotification.getCommonFormatArgs(tenantId);
// return {
// ...commonArgs,
// 'Customer Name': invoice.customer.displayName,
// 'Invoice Number': invoice.invoiceNo,
// 'Invoice Due Amount': invoice.dueAmountFormatted,
// 'Invoice Due Date': invoice.dueDateFormatted,
// 'Invoice Date': invoice.invoiceDateFormatted,
// 'Invoice Amount': invoice.totalFormatted,
// 'Overdue Days': invoice.overdueDays,
// };
// };
// }

View File

@@ -0,0 +1,136 @@
// import { Inject, Service } from 'typedi';
// import Mail from '@/lib/Mail';
// import {
// ISaleInvoiceMailSend,
// SaleInvoiceMailOptions,
// SendInvoiceMailDTO,
// } from '@/interfaces';
// import { SaleInvoicePdf } from '../queries/SaleInvoicePdf.service';
// import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon.service';
// import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
// import events from '@/subscribers/events';
// @Service()
// export class SendSaleInvoiceMail {
// @Inject()
// private invoicePdf: SaleInvoicePdf;
// @Inject()
// private invoiceMail: SendSaleInvoiceMailCommon;
// @Inject()
// private eventPublisher: EventPublisher;
// @Inject('agenda')
// private agenda: any;
// /**
// * Sends the invoice mail of the given sale invoice.
// * @param {number} tenantId
// * @param {number} saleInvoiceId
// * @param {SendInvoiceMailDTO} messageDTO
// */
// public async triggerMail(
// tenantId: number,
// saleInvoiceId: number,
// messageOptions: SendInvoiceMailDTO
// ) {
// const payload = {
// tenantId,
// saleInvoiceId,
// messageOptions,
// };
// await this.agenda.now('sale-invoice-mail-send', payload);
// // Triggers the event `onSaleInvoicePreMailSend`.
// await this.eventPublisher.emitAsync(events.saleInvoice.onPreMailSend, {
// tenantId,
// saleInvoiceId,
// messageOptions,
// } as ISaleInvoiceMailSend);
// }
// /**
// * Retrieves the formatted mail options.
// * @param {number} tenantId
// * @param {number} saleInvoiceId
// * @param {SendInvoiceMailDTO} messageOptions
// * @returns {Promise<SaleInvoiceMailOptions>}
// */
// async getFormattedMailOptions(
// tenantId: number,
// saleInvoiceId: number,
// messageOptions: SendInvoiceMailDTO
// ): Promise<SaleInvoiceMailOptions> {
// const defaultMessageOptions = await this.invoiceMail.getInvoiceMailOptions(
// tenantId,
// saleInvoiceId
// );
// // Merges message options with default options and parses the options values.
// const parsedMessageOptions = mergeAndValidateMailOptions(
// defaultMessageOptions,
// messageOptions
// );
// return this.invoiceMail.formatInvoiceMailOptions(
// tenantId,
// saleInvoiceId,
// parsedMessageOptions
// );
// }
// /**
// * Triggers the mail invoice.
// * @param {number} tenantId
// * @param {number} saleInvoiceId
// * @param {SendInvoiceMailDTO} messageDTO
// * @returns {Promise<void>}
// */
// public async sendMail(
// tenantId: number,
// saleInvoiceId: number,
// messageOptions: SendInvoiceMailDTO
// ) {
// const formattedMessageOptions = await this.getFormattedMailOptions(
// tenantId,
// saleInvoiceId,
// messageOptions
// );
// const mail = new Mail()
// .setSubject(formattedMessageOptions.subject)
// .setTo(formattedMessageOptions.to)
// .setCC(formattedMessageOptions.cc)
// .setBCC(formattedMessageOptions.bcc)
// .setContent(formattedMessageOptions.message);
// // Attach invoice document.
// if (formattedMessageOptions.attachInvoice) {
// // Retrieves document buffer of the invoice pdf document.
// const [invoicePdfBuffer, invoiceFilename] =
// await this.invoicePdf.saleInvoicePdf(tenantId, saleInvoiceId);
// mail.setAttachments([
// { filename: `${invoiceFilename}.pdf`, content: invoicePdfBuffer },
// ]);
// }
// const eventPayload = {
// tenantId,
// saleInvoiceId,
// messageOptions,
// formattedMessageOptions,
// } as ISaleInvoiceMailSend;
// // Triggers the event `onSaleInvoiceSend`.
// await this.eventPublisher.emitAsync(
// events.saleInvoice.onMailSend,
// eventPayload
// );
// await mail.send();
// // Triggers the event `onSaleInvoiceSend`.
// await this.eventPublisher.emitAsync(
// events.saleInvoice.onMailSent,
// eventPayload
// );
// }
// }

View File

@@ -0,0 +1,33 @@
// import Container, { Service } from 'typedi';
// import events from '@/subscribers/events';
// import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
// @Service()
// export class SendSaleInvoiceMailJob {
// /**
// * Constructor method.
// */
// constructor(agenda) {
// agenda.define(
// 'sale-invoice-mail-send',
// { priority: 'high', concurrency: 2 },
// this.handler
// );
// }
// /**
// * Triggers sending invoice mail.
// */
// private handler = async (job, done: Function) => {
// const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
// const sendInvoiceMail = Container.get(SendSaleInvoiceMail);
// try {
// await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
// done();
// } catch (error) {
// console.log(error);
// done(error);
// }
// };
// }

View File

@@ -0,0 +1,112 @@
// import { Inject, Service } from 'typedi';
// import {
// ISaleInvoiceMailSend,
// ISaleInvoiceMailSent,
// SendInvoiceMailDTO,
// } from '@/interfaces';
// import Mail from '@/lib/Mail';
// import { SaleInvoicePdf } from '../queries/SaleInvoicePdf';
// import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
// import {
// DEFAULT_INVOICE_REMINDER_MAIL_CONTENT,
// DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT,
// } from '../constants';
// import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
// import events from '@/subscribers/events';
// @Service()
// export class SendInvoiceMailReminder {
// @Inject('agenda')
// private agenda: any;
// @Inject()
// private invoicePdf: SaleInvoicePdf;
// @Inject()
// private invoiceCommonMail: SendSaleInvoiceMailCommon;
// @Inject()
// private eventPublisher: EventPublisher;
// /**
// * Triggers the reminder mail of the given sale invoice.
// * @param {number} tenantId
// * @param {number} saleInvoiceId
// */
// public async triggerMail(
// tenantId: number,
// saleInvoiceId: number,
// messageOptions: SendInvoiceMailDTO
// ) {
// const payload = {
// tenantId,
// saleInvoiceId,
// messageOptions,
// };
// await this.agenda.now('sale-invoice-reminder-mail-send', payload);
// }
// /**
// * Retrieves the mail options of the given sale invoice.
// * @param {number} tenantId
// * @param {number} saleInvoiceId
// * @returns {Promise<SaleInvoiceMailOptions>}
// */
// public async getMailOption(tenantId: number, saleInvoiceId: number) {
// return this.invoiceCommonMail.getMailOption(
// tenantId,
// saleInvoiceId,
// DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT,
// DEFAULT_INVOICE_REMINDER_MAIL_CONTENT
// );
// }
// /**
// * Triggers the mail invoice.
// * @param {number} tenantId
// * @param {number} saleInvoiceId
// * @param {SendInvoiceMailDTO} messageOptions
// * @returns {Promise<void>}
// */
// public async sendMail(
// tenantId: number,
// saleInvoiceId: number,
// messageOptions: SendInvoiceMailDTO
// ) {
// const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId);
// const messageOpts = parseAndValidateMailOptions(
// localMessageOpts,
// messageOptions
// );
// const mail = new Mail()
// .setSubject(messageOpts.subject)
// .setTo(messageOpts.to)
// .setContent(messageOpts.body);
// if (messageOpts.attachInvoice) {
// // Retrieves document buffer of the invoice pdf document.
// const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf(
// tenantId,
// saleInvoiceId
// );
// mail.setAttachments([
// { filename: 'invoice.pdf', content: invoicePdfBuffer },
// ]);
// }
// // Triggers the event `onSaleInvoiceSend`.
// await this.eventPublisher.emitAsync(events.saleInvoice.onMailReminderSend, {
// saleInvoiceId,
// messageOptions,
// } as ISaleInvoiceMailSend);
// await mail.send();
// // Triggers the event `onSaleInvoiceSent`.
// await this.eventPublisher.emitAsync(events.saleInvoice.onMailReminderSent, {
// saleInvoiceId,
// messageOptions,
// } as ISaleInvoiceMailSent);
// }
// }

View File

@@ -0,0 +1,32 @@
// import Container, { Service } from 'typedi';
// import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
// @Service()
// export class SendSaleInvoiceReminderMailJob {
// /**
// * Constructor method.
// */
// constructor(agenda) {
// agenda.define(
// 'sale-invoice-reminder-mail-send',
// { priority: 'high', concurrency: 1 },
// this.handler
// );
// }
// /**
// * Triggers sending invoice mail.
// */
// private handler = async (job, done: Function) => {
// const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
// const sendInvoiceMail = Container.get(SendInvoiceMailReminder);
// try {
// await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
// done();
// } catch (error) {
// console.log(error);
// done(error);
// }
// };
// }

View File

@@ -0,0 +1,155 @@
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import {
ISaleInvoiceWriteoffCreatePayload,
ISaleInvoiceWriteoffDTO,
ISaleInvoiceWrittenOffCanceledPayload,
ISaleInvoiceWrittenOffCancelPayload,
} from '../SaleInvoice.types';
import { ERRORS } from '../constants';
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { SaleInvoice } from '../models/SaleInvoice';
import { events } from '@/common/events/events';
import { ServiceError } from '../../Items/ServiceError';
@Injectable()
export class WriteoffSaleInvoice {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly validators: CommandSaleInvoiceValidators,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: typeof SaleInvoice,
) {}
/**
* Writes-off the sale invoice on bad debt expense account.
* @param {number} saleInvoiceId
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO
* @return {Promise<ISaleInvoice>}
*/
public writeOff = async (
saleInvoiceId: number,
writeoffDTO: ISaleInvoiceWriteoffDTO,
): Promise<SaleInvoice> => {
const saleInvoice = await this.saleInvoiceModel
.query()
.findById(saleInvoiceId)
.throwIfNotFound();
// 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(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 this.saleInvoiceModel
.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 (
saleInvoiceId: number,
): Promise<SaleInvoice> => {
// Validate the sale invoice existance.
// Retrieve the sale invoice or throw not found service error.
const saleInvoice = await this.saleInvoiceModel
.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(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceWrittenoffCancel` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWrittenoffCancel,
{
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,
{
saleInvoice,
trx,
} as ISaleInvoiceWrittenOffCanceledPayload,
);
return newSaleInvoice;
});
};
/**
* Should sale invoice not be written-off.
* @param {SaleInvoice} saleInvoice
*/
private validateSaleInvoiceNotWrittenoff(saleInvoice: SaleInvoice) {
if (!saleInvoice.isWrittenoff) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_WRITTEN_OFF);
}
}
/**
* Should sale invoice already written-off.
* @param {SaleInvoice} saleInvoice
*/
private validateSaleInvoiceAlreadyWrittenoff(saleInvoice: SaleInvoice) {
if (saleInvoice.isWrittenoff) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_WRITTEN_OFF);
}
}
}