mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
refactor: wip to nestjs
This commit is contained in:
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
// };
|
||||
// };
|
||||
// }
|
||||
@@ -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
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -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);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
@@ -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);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user