mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateSaleEstimate } from './commands/CreateSaleEstimate.service';
|
||||
import {
|
||||
ISalesEstimatesFilter,
|
||||
SaleEstimateMailOptionsDTO,
|
||||
} from './types/SaleEstimates.types';
|
||||
import { EditSaleEstimate } from './commands/EditSaleEstimate.service';
|
||||
import { DeleteSaleEstimate } from './commands/DeleteSaleEstimate.service';
|
||||
import { GetSaleEstimate } from './queries/GetSaleEstimate.service';
|
||||
import { DeliverSaleEstimateService } from './commands/DeliverSaleEstimate.service';
|
||||
import { ApproveSaleEstimateService } from './commands/ApproveSaleEstimate.service';
|
||||
import { RejectSaleEstimateService } from './commands/RejectSaleEstimate.service';
|
||||
import { SendSaleEstimateMail } from './commands/SendSaleEstimateMail';
|
||||
import { GetSaleEstimateState } from './queries/GetSaleEstimateState.service';
|
||||
import { GetSaleEstimatesService } from './queries/GetSaleEstimates.service';
|
||||
import { GetSaleEstimatePdf } from './queries/GetSaleEstimatePdf';
|
||||
import {
|
||||
CreateSaleEstimateDto,
|
||||
EditSaleEstimateDto,
|
||||
} from './dtos/SaleEstimate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class SaleEstimatesApplication {
|
||||
constructor(
|
||||
private readonly createSaleEstimateService: CreateSaleEstimate,
|
||||
private readonly editSaleEstimateService: EditSaleEstimate,
|
||||
private readonly deleteSaleEstimateService: DeleteSaleEstimate,
|
||||
private readonly getSaleEstimateService: GetSaleEstimate,
|
||||
private readonly getSaleEstimatesService: GetSaleEstimatesService,
|
||||
private readonly deliverSaleEstimateService: DeliverSaleEstimateService,
|
||||
private readonly approveSaleEstimateService: ApproveSaleEstimateService,
|
||||
private readonly rejectSaleEstimateService: RejectSaleEstimateService,
|
||||
private readonly sendEstimateMailService: SendSaleEstimateMail,
|
||||
private readonly getSaleEstimateStateService: GetSaleEstimateState,
|
||||
private readonly saleEstimatesPdfService: GetSaleEstimatePdf,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a sale estimate.
|
||||
* @param {CreateSaleEstimateDto} estimate - Estimate DTO.
|
||||
* @return {Promise<ISaleEstimate>}
|
||||
*/
|
||||
public createSaleEstimate(estimateDTO: CreateSaleEstimateDto) {
|
||||
return this.createSaleEstimateService.createEstimate(estimateDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given sale estimate.
|
||||
* @param {number} estimateId - Sale estimate ID.
|
||||
* @param {EditSaleEstimateDto} estimate - Estimate DTO.
|
||||
* @return {Promise<ISaleEstimate>}
|
||||
*/
|
||||
public editSaleEstimate(
|
||||
estimateId: number,
|
||||
estimateDTO: EditSaleEstimateDto,
|
||||
) {
|
||||
return this.editSaleEstimateService.editEstimate(estimateId, estimateDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given sale estimate.
|
||||
* @param {number} estimateId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public deleteSaleEstimate(estimateId: number) {
|
||||
return this.deleteSaleEstimateService.deleteEstimate(estimateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given sale estimate.
|
||||
* @param {number} estimateId - Sale estimate ID.
|
||||
*/
|
||||
public getSaleEstimate(estimateId: number) {
|
||||
return this.getSaleEstimateService.getEstimate(estimateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sale estimate.
|
||||
* @param {ISalesEstimatesFilter} filterDTO - Sales estimates filter DTO.
|
||||
* @returns
|
||||
*/
|
||||
public getSaleEstimates(filterDTO: ISalesEstimatesFilter) {
|
||||
return this.getSaleEstimatesService.getEstimates(filterDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver the given sale estimate.
|
||||
* @param {number} saleEstimateId - Sale estimate id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deliverSaleEstimate(saleEstimateId: number) {
|
||||
return this.deliverSaleEstimateService.deliverSaleEstimate(saleEstimateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve the given sale estimate.
|
||||
* @param {number} saleEstimateId - Sale estimate ID.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public approveSaleEstimate(saleEstimateId: number) {
|
||||
return this.approveSaleEstimateService.approveSaleEstimate(saleEstimateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the sale estimate as rejected from the customer.
|
||||
* @param {number} saleEstimateId
|
||||
*/
|
||||
public async rejectSaleEstimate(saleEstimateId: number) {
|
||||
return this.rejectSaleEstimateService.rejectSaleEstimate(saleEstimateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the customer of the given sale estimate by SMS.
|
||||
* @param {number} saleEstimateId - Sale estimate ID.
|
||||
* @returns {Promise<ISaleEstimate>}
|
||||
*/
|
||||
public notifySaleEstimateBySms = async (saleEstimateId: number) => {
|
||||
// return this.saleEstimateNotifyBySmsService.notifyBySms(
|
||||
// saleEstimateId,
|
||||
// );
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the SMS details of the given payment receive transaction.
|
||||
* @param {number} saleEstimateId - Sale estimate ID.
|
||||
* @returns {Promise<IPaymentReceivedSmsDetails>}
|
||||
*/
|
||||
public getSaleEstimateSmsDetails = (saleEstimateId: number) => {
|
||||
// return this.saleEstimateNotifyBySmsService.smsDetails(
|
||||
// saleEstimateId,
|
||||
// );
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the PDF content of the given sale estimate.
|
||||
* @param {number} saleEstimateId - Sale estimate ID.
|
||||
* @returns {Promise<[Buffer, string]>}
|
||||
*/
|
||||
public getSaleEstimatePdf(saleEstimateId: number) {
|
||||
return this.saleEstimatesPdfService.getSaleEstimatePdf(saleEstimateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the reminder mail of the given sale estimate.
|
||||
* @param {number} saleEstimateId - Sale estimate ID.
|
||||
* @param {SaleEstimateMailOptionsDTO} saleEstimateMailOpts - Sale estimate mail options.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public sendSaleEstimateMail(
|
||||
saleEstimateId: number,
|
||||
saleEstimateMailOpts: SaleEstimateMailOptionsDTO,
|
||||
) {
|
||||
return this.sendEstimateMailService.triggerMail(
|
||||
saleEstimateId,
|
||||
saleEstimateMailOpts,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale estimate.
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<SaleEstimateMailOptions>}
|
||||
*/
|
||||
public getSaleEstimateMail(saleEstimateId: number) {
|
||||
return this.sendEstimateMailService.getMailOptions(saleEstimateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current state of the sale estimate.
|
||||
* @returns {Promise<ISaleEstimateState>} - A promise resolving to the sale estimate state.
|
||||
*/
|
||||
public getSaleEstimateState() {
|
||||
return this.getSaleEstimateStateService.getSaleEstimateState();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { SaleEstimatesApplication } from './SaleEstimates.application';
|
||||
import {
|
||||
ISalesEstimatesFilter,
|
||||
SaleEstimateMailOptionsDTO,
|
||||
} from './types/SaleEstimates.types';
|
||||
import { SaleEstimate } from './models/SaleEstimate';
|
||||
import {
|
||||
CreateSaleEstimateDto,
|
||||
EditSaleEstimateDto,
|
||||
} from './dtos/SaleEstimate.dto';
|
||||
|
||||
@Controller('sale-estimates')
|
||||
@ApiTags('sale-estimates')
|
||||
export class SaleEstimatesController {
|
||||
/**
|
||||
* @param {SaleEstimatesApplication} saleEstimatesApplication - Sale estimates application.
|
||||
*/
|
||||
constructor(
|
||||
private readonly saleEstimatesApplication: SaleEstimatesApplication,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new sale estimate.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale estimate created successfully',
|
||||
})
|
||||
public createSaleEstimate(
|
||||
@Body() estimateDTO: CreateSaleEstimateDto,
|
||||
): Promise<SaleEstimate> {
|
||||
return this.saleEstimatesApplication.createSaleEstimate(estimateDTO);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Edit the given sale estimate.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale estimate edited successfully',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'Sale estimate not found',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public editSaleEstimate(
|
||||
@Param('id', ParseIntPipe) estimateId: number,
|
||||
@Body() estimateDTO: EditSaleEstimateDto,
|
||||
): Promise<SaleEstimate> {
|
||||
return this.saleEstimatesApplication.editSaleEstimate(
|
||||
estimateId,
|
||||
estimateDTO,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete the given sale estimate.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale estimate deleted successfully',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'Sale estimate not found',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public deleteSaleEstimate(
|
||||
@Param('id', ParseIntPipe) estimateId: number,
|
||||
): Promise<void> {
|
||||
return this.saleEstimatesApplication.deleteSaleEstimate(estimateId);
|
||||
}
|
||||
|
||||
@Get('state')
|
||||
@ApiOperation({ summary: 'Retrieves the sale estimate state.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale estimate state retrieved successfully',
|
||||
})
|
||||
public getSaleEstimateState() {
|
||||
return this.saleEstimatesApplication.getSaleEstimateState();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieves the sale estimates.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale estimates retrieved successfully',
|
||||
})
|
||||
public getSaleEstimates(@Query() filterDTO: ISalesEstimatesFilter) {
|
||||
return this.saleEstimatesApplication.getSaleEstimates(filterDTO);
|
||||
}
|
||||
|
||||
@Post(':id/deliver')
|
||||
@ApiOperation({ summary: 'Deliver the given sale estimate.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale estimate delivered successfully',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public deliverSaleEstimate(
|
||||
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||
): Promise<void> {
|
||||
return this.saleEstimatesApplication.deliverSaleEstimate(saleEstimateId);
|
||||
}
|
||||
|
||||
@Put(':id/approve')
|
||||
@ApiOperation({ summary: 'Approve the given sale estimate.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public approveSaleEstimate(
|
||||
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||
): Promise<void> {
|
||||
return this.saleEstimatesApplication.approveSaleEstimate(saleEstimateId);
|
||||
}
|
||||
|
||||
@Put(':id/reject')
|
||||
@ApiOperation({ summary: 'Reject the given sale estimate.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public rejectSaleEstimate(
|
||||
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||
): Promise<void> {
|
||||
return this.saleEstimatesApplication.rejectSaleEstimate(saleEstimateId);
|
||||
}
|
||||
|
||||
@Post(':id/notify-sms')
|
||||
@ApiOperation({ summary: 'Notify the given sale estimate by SMS.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public notifySaleEstimateBySms(
|
||||
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||
) {
|
||||
return this.saleEstimatesApplication.notifySaleEstimateBySms(
|
||||
saleEstimateId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id/sms-details')
|
||||
@ApiOperation({ summary: 'Retrieves the sale estimate SMS details.' })
|
||||
public getSaleEstimateSmsDetails(
|
||||
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||
) {
|
||||
return this.saleEstimatesApplication.getSaleEstimateSmsDetails(
|
||||
saleEstimateId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id/pdf')
|
||||
@ApiOperation({ summary: 'Retrieves the sale estimate PDF.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public getSaleEstimatePdf(@Param('id', ParseIntPipe) saleEstimateId: number) {
|
||||
return this.saleEstimatesApplication.getSaleEstimatePdf(saleEstimateId);
|
||||
}
|
||||
|
||||
@Post(':id/mail')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Send the given sale estimate by mail.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public sendSaleEstimateMail(
|
||||
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||
@Body() mailOptions: SaleEstimateMailOptionsDTO,
|
||||
) {
|
||||
return this.saleEstimatesApplication.sendSaleEstimateMail(
|
||||
saleEstimateId,
|
||||
mailOptions,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id/mail')
|
||||
@ApiOperation({ summary: 'Retrieves the sale estimate mail details.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public getSaleEstimateMail(
|
||||
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||
) {
|
||||
return this.saleEstimatesApplication.getSaleEstimateMail(saleEstimateId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Retrieves the sale estimate details.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale estimate id',
|
||||
})
|
||||
public getSaleEstimate(@Param('id', ParseIntPipe) estimateId: number) {
|
||||
return this.saleEstimatesApplication.getSaleEstimate(estimateId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { ApproveSaleEstimateService } from './commands/ApproveSaleEstimate.service';
|
||||
import { ConvertSaleEstimate } from './commands/ConvetSaleEstimate.service';
|
||||
import { CreateSaleEstimate } from './commands/CreateSaleEstimate.service';
|
||||
import { DeliverSaleEstimateService } from './commands/DeliverSaleEstimate.service';
|
||||
import { EditSaleEstimate } from './commands/EditSaleEstimate.service';
|
||||
import { RejectSaleEstimateService } from './commands/RejectSaleEstimate.service';
|
||||
import { SaleEstimateValidators } from './commands/SaleEstimateValidators.service';
|
||||
import { SaleEstimatesController } from './SaleEstimates.controller';
|
||||
import { ItemsEntriesService } from '../Items/ItemsEntries.service';
|
||||
import { SaleEstimateDTOTransformer } from './commands/SaleEstimateDTOTransformer.service';
|
||||
import { BranchTransactionDTOTransformer } from '../Branches/integrations/BranchTransactionDTOTransform';
|
||||
import { BranchesSettingsService } from '../Branches/BranchesSettings';
|
||||
import { WarehouseTransactionDTOTransform } from '../Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import { WarehousesSettings } from '../Warehouses/WarehousesSettings';
|
||||
import { SaleEstimateIncrement } from './commands/SaleEstimateIncrement.service';
|
||||
import { AutoIncrementOrdersService } from '../AutoIncrementOrders/AutoIncrementOrders.service';
|
||||
import { BrandingTemplateDTOTransformer } from '../PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
import { SaleEstimatesApplication } from './SaleEstimates.application';
|
||||
import { DeleteSaleEstimate } from './commands/DeleteSaleEstimate.service';
|
||||
import { GetSaleEstimate } from './queries/GetSaleEstimate.service';
|
||||
import { GetSaleEstimateState } from './queries/GetSaleEstimateState.service';
|
||||
import { SendSaleEstimateMail } from './commands/SendSaleEstimateMail';
|
||||
import { GetSaleEstimatesService } from './queries/GetSaleEstimates.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { GetSaleEstimatePdf } from './queries/GetSaleEstimatePdf';
|
||||
import { MailNotificationModule } from '../MailNotification/MailNotification.module';
|
||||
import { MailModule } from '../Mail/Mail.module';
|
||||
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
|
||||
import { TemplateInjectableModule } from '../TemplateInjectable/TemplateInjectable.module';
|
||||
import { SaleEstimatePdfTemplate } from '../SaleInvoices/queries/SaleEstimatePdfTemplate.service';
|
||||
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
|
||||
import { SendSaleEstimateMailQueue } from './types/SaleEstimates.types';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TenancyDatabaseModule,
|
||||
DynamicListModule,
|
||||
MailNotificationModule,
|
||||
MailModule,
|
||||
ChromiumlyTenancyModule,
|
||||
TemplateInjectableModule,
|
||||
PdfTemplatesModule,
|
||||
BullModule.registerQueue({ name: SendSaleEstimateMailQueue }),
|
||||
],
|
||||
controllers: [SaleEstimatesController],
|
||||
providers: [
|
||||
AutoIncrementOrdersService,
|
||||
BrandingTemplateDTOTransformer,
|
||||
SaleEstimateIncrement,
|
||||
CreateSaleEstimate,
|
||||
ConvertSaleEstimate,
|
||||
EditSaleEstimate,
|
||||
DeleteSaleEstimate,
|
||||
GetSaleEstimate,
|
||||
GetSaleEstimatesService,
|
||||
GetSaleEstimateState,
|
||||
ApproveSaleEstimateService,
|
||||
DeliverSaleEstimateService,
|
||||
RejectSaleEstimateService,
|
||||
SaleEstimateValidators,
|
||||
ItemsEntriesService,
|
||||
BranchesSettingsService,
|
||||
WarehousesSettings,
|
||||
BranchTransactionDTOTransformer,
|
||||
WarehouseTransactionDTOTransform,
|
||||
SaleEstimateDTOTransformer,
|
||||
TenancyContext,
|
||||
TransformerInjectable,
|
||||
SaleEstimatesApplication,
|
||||
SendSaleEstimateMail,
|
||||
GetSaleEstimatePdf,
|
||||
SaleEstimatePdfTemplate
|
||||
],
|
||||
})
|
||||
export class SaleEstimatesModule {}
|
||||
@@ -0,0 +1,35 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { ISalesInvoicesFilter } from '@/interfaces';
|
||||
// import { Exportable } from '@/services/Export/Exportable';
|
||||
// import { SaleEstimatesApplication } from './SaleEstimates.application';
|
||||
// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
// @Service()
|
||||
// export class SaleEstimatesExportable extends Exportable {
|
||||
// @Inject()
|
||||
// private saleEstimatesApplication: SaleEstimatesApplication;
|
||||
|
||||
// /**
|
||||
// * Retrieves the accounts data to exportable sheet.
|
||||
// * @param {number} tenantId
|
||||
// * @returns
|
||||
// */
|
||||
// public exportable(tenantId: number, query: ISalesInvoicesFilter) {
|
||||
// const filterQuery = (query) => {
|
||||
// query.withGraphFetched('branch');
|
||||
// query.withGraphFetched('warehouse');
|
||||
// };
|
||||
// const parsedQuery = {
|
||||
// sortOrder: 'desc',
|
||||
// columnSortBy: 'created_at',
|
||||
// ...query,
|
||||
// page: 1,
|
||||
// pageSize: EXPORT_SIZE_LIMIT,
|
||||
// filterQuery,
|
||||
// } as ISalesInvoicesFilter;
|
||||
|
||||
// return this.saleEstimatesApplication
|
||||
// .getSaleEstimates(tenantId, parsedQuery)
|
||||
// .then((output) => output.salesEstimates);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,45 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { Knex } from 'knex';
|
||||
// import { ISaleEstimateDTO } from '@/interfaces';
|
||||
// import { CreateSaleEstimate } from './commands/CreateSaleEstimate.service';
|
||||
// import { Importable } from '@/services/Import/Importable';
|
||||
// import { SaleEstimatesSampleData } from './constants';
|
||||
|
||||
// @Service()
|
||||
// export class SaleEstimatesImportable extends Importable {
|
||||
// @Inject()
|
||||
// private createEstimateService: CreateSaleEstimate;
|
||||
|
||||
// /**
|
||||
// * Importing to account service.
|
||||
// * @param {number} tenantId
|
||||
// * @param {IAccountCreateDTO} createAccountDTO
|
||||
// * @returns
|
||||
// */
|
||||
// public importable(
|
||||
// tenantId: number,
|
||||
// createEstimateDTO: ISaleEstimateDTO,
|
||||
// trx?: Knex.Transaction
|
||||
// ) {
|
||||
// return this.createEstimateService.createEstimate(
|
||||
// tenantId,
|
||||
// createEstimateDTO,
|
||||
// trx
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Concurrrency controlling of the importing process.
|
||||
// * @returns {number}
|
||||
// */
|
||||
// public get concurrency() {
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Retrieves the sample data that used to download accounts sample sheet.
|
||||
// */
|
||||
// public sampleData(): any[] {
|
||||
// return SaleEstimatesSampleData;
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ISaleEstimateApprovedEvent,
|
||||
ISaleEstimateApprovingEvent,
|
||||
} from '../types/SaleEstimates.types';
|
||||
import { ERRORS } from '../constants';
|
||||
import { Knex } from 'knex';
|
||||
import * as moment from 'moment';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class ApproveSaleEstimateService {
|
||||
constructor(
|
||||
@Inject(SaleEstimate.name)
|
||||
private saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
|
||||
private uow: UnitOfWork,
|
||||
private eventPublisher: EventEmitter2,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Mark the sale estimate as approved from the customer.
|
||||
* @param {number} saleEstimateId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async approveSaleEstimate(saleEstimateId: number): Promise<void> {
|
||||
// Retrieve details of the given sale estimate id.
|
||||
const oldSaleEstimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(saleEstimateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Throws error in case the sale estimate still not delivered to customer.
|
||||
if (!oldSaleEstimate.isDelivered) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
|
||||
}
|
||||
// Throws error in case the sale estimate already approved.
|
||||
if (oldSaleEstimate.isApproved) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_APPROVED);
|
||||
}
|
||||
// Triggers `onSaleEstimateApproving` event.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleEstimateApproving` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onApproving, {
|
||||
trx,
|
||||
oldSaleEstimate,
|
||||
} as ISaleEstimateApprovingEvent);
|
||||
|
||||
// Update estimate as approved.
|
||||
const saleEstimate = await this.saleEstimateModel()
|
||||
.query(trx)
|
||||
.patchAndFetchById(saleEstimateId, {
|
||||
approvedAt: moment().toMySqlDateTime(),
|
||||
rejectedAt: null,
|
||||
});
|
||||
// Triggers `onSaleEstimateApproved` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onApproved, {
|
||||
trx,
|
||||
oldSaleEstimate,
|
||||
saleEstimate,
|
||||
} as ISaleEstimateApprovedEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as moment from 'moment';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Knex } from 'knex';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class ConvertSaleEstimate {
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Converts estimate to invoice.
|
||||
* @param {number} estimateId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async convertEstimateToInvoice(
|
||||
estimateId: number,
|
||||
invoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Retrieve details of the given sale estimate.
|
||||
const saleEstimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(estimateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Marks the estimate as converted from the givne invoice.
|
||||
await this.saleEstimateModel().query(trx).where('id', estimateId).patch({
|
||||
convertedToInvoiceId: invoiceId,
|
||||
convertedToInvoiceAt: moment().toMySqlDateTime(),
|
||||
});
|
||||
|
||||
// Triggers `onSaleEstimateConvertedToInvoice` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleEstimate.onConvertedToInvoice,
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ISaleEstimateCreatedPayload,
|
||||
ISaleEstimateCreatingPayload,
|
||||
ISaleEstimateDTO,
|
||||
} from '../types/SaleEstimates.types';
|
||||
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer.service';
|
||||
import { SaleEstimateValidators } from './SaleEstimateValidators.service';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { CreateSaleEstimateDto } from '../dtos/SaleEstimate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CreateSaleEstimate {
|
||||
constructor(
|
||||
@Inject(SaleEstimate.name)
|
||||
private saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
|
||||
@Inject(Customer.name)
|
||||
private customerModel: TenantModelProxy<typeof Customer>,
|
||||
|
||||
private itemsEntriesService: ItemsEntriesService,
|
||||
private eventPublisher: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
private transformerDTO: SaleEstimateDTOTransformer,
|
||||
private validators: SaleEstimateValidators,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new estimate with associated entries.
|
||||
* @param {ISaleEstimateDTO} estimateDTO - Sale estimate DTO object.
|
||||
* @return {Promise<ISaleEstimate>}
|
||||
*/
|
||||
public async createEstimate(
|
||||
estimateDTO: CreateSaleEstimateDto,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<SaleEstimate> {
|
||||
// Retrieve the given customer or throw not found service error.
|
||||
const customer = await this.customerModel()
|
||||
.query()
|
||||
.findById(estimateDTO.customerId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Transform DTO object to model object.
|
||||
const estimateObj = await this.transformerDTO.transformDTOToModel(
|
||||
estimateDTO,
|
||||
customer,
|
||||
);
|
||||
// Validate estimate number uniquiness on the storage.
|
||||
await this.validators.validateEstimateNumberExistance(
|
||||
estimateObj.estimateNumber,
|
||||
);
|
||||
// Validate items IDs existance on the storage.
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||
estimateDTO.entries,
|
||||
);
|
||||
// Validate non-sellable items.
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||
estimateDTO.entries,
|
||||
);
|
||||
// Creates a sale estimate transaction with associated transactions as UOW.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleEstimateCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, {
|
||||
estimateDTO,
|
||||
trx,
|
||||
} as ISaleEstimateCreatingPayload);
|
||||
|
||||
// Upsert the sale estimate graph to the storage.
|
||||
const saleEstimate = await this.saleEstimateModel()
|
||||
.query(trx)
|
||||
.upsertGraphAndFetch({
|
||||
...estimateObj,
|
||||
});
|
||||
// Triggers `onSaleEstimateCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
|
||||
saleEstimate,
|
||||
saleEstimateId: saleEstimate.id,
|
||||
saleEstimateDTO: estimateDTO,
|
||||
trx,
|
||||
} as ISaleEstimateCreatedPayload);
|
||||
|
||||
return saleEstimate;
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ISaleEstimateDeletedPayload,
|
||||
ISaleEstimateDeletingPayload,
|
||||
} from '../types/SaleEstimates.types';
|
||||
import { ERRORS } from '../constants';
|
||||
import { Knex } from 'knex';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteSaleEstimate {
|
||||
constructor(
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
|
||||
@Inject(ItemEntry.name)
|
||||
private readonly itemEntryModel: TenantModelProxy<typeof ItemEntry>,
|
||||
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes the given estimate id with associated entries.
|
||||
* @async
|
||||
* @param {number} estimateId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteEstimate(estimateId: number): Promise<void> {
|
||||
// Retrieve sale estimate or throw not found service error.
|
||||
const oldSaleEstimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(estimateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Throw error if the sale estimate converted to sale invoice.
|
||||
if (oldSaleEstimate.convertedToInvoiceId) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
|
||||
}
|
||||
// Updates the estimate with associated transactions under UOW enivrement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleEstimatedDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onDeleting, {
|
||||
trx,
|
||||
oldSaleEstimate,
|
||||
} as ISaleEstimateDeletingPayload);
|
||||
|
||||
// Delete sale estimate entries.
|
||||
await this.itemEntryModel()
|
||||
.query(trx)
|
||||
.where('reference_id', estimateId)
|
||||
.where('reference_type', 'SaleEstimate')
|
||||
.delete();
|
||||
|
||||
// Delete sale estimate transaction.
|
||||
await this.saleEstimateModel()
|
||||
.query(trx)
|
||||
.where('id', estimateId)
|
||||
.delete();
|
||||
|
||||
// Triggers `onSaleEstimatedDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onDeleted, {
|
||||
saleEstimateId: estimateId,
|
||||
oldSaleEstimate,
|
||||
trx,
|
||||
} as ISaleEstimateDeletedPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Knex } from 'knex';
|
||||
import * as moment from 'moment';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
ISaleEstimateEventDeliveredPayload,
|
||||
ISaleEstimateEventDeliveringPayload,
|
||||
} from '../types/SaleEstimates.types';
|
||||
import { ERRORS } from '../constants';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeliverSaleEstimateService {
|
||||
constructor(
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Mark the sale estimate as delivered.
|
||||
* @param {number} saleEstimateId - Sale estimate id.
|
||||
*/
|
||||
public async deliverSaleEstimate(saleEstimateId: number): Promise<void> {
|
||||
// Retrieve details of the given sale estimate id.
|
||||
const oldSaleEstimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(saleEstimateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Throws error in case the sale estimate already published.
|
||||
if (oldSaleEstimate.isDelivered) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED);
|
||||
}
|
||||
|
||||
// Updates the sale estimate transaction with assocaited transactions
|
||||
// under UOW envirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleEstimateDelivering` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onDelivering, {
|
||||
oldSaleEstimate,
|
||||
trx,
|
||||
} as ISaleEstimateEventDeliveringPayload);
|
||||
|
||||
// Record the delivered at on the storage.
|
||||
const saleEstimate = await this.saleEstimateModel()
|
||||
.query(trx)
|
||||
.patchAndFetchById(saleEstimateId, {
|
||||
deliveredAt: moment().toMySqlDateTime(),
|
||||
});
|
||||
|
||||
// Triggers `onSaleEstimateDelivered` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onDelivered, {
|
||||
saleEstimate,
|
||||
trx,
|
||||
} as ISaleEstimateEventDeliveredPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
ISaleEstimateDTO,
|
||||
ISaleEstimateEditedPayload,
|
||||
ISaleEstimateEditingPayload,
|
||||
} from '../types/SaleEstimates.types';
|
||||
import { SaleEstimateValidators } from './SaleEstimateValidators.service';
|
||||
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer.service';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { EditSaleEstimateDto } from '../dtos/SaleEstimate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class EditSaleEstimate {
|
||||
constructor(
|
||||
private readonly validators: SaleEstimateValidators,
|
||||
private readonly itemsEntriesService: ItemsEntriesService,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly transformerDTO: SaleEstimateDTOTransformer,
|
||||
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
|
||||
@Inject(Customer.name)
|
||||
private readonly customerModel: TenantModelProxy<typeof Customer>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Edit details of the given estimate with associated entries.
|
||||
* @async
|
||||
* @param {Integer} estimateId
|
||||
* @param {EstimateDTO} estimate
|
||||
* @return {Promise<ISaleEstimate>}
|
||||
*/
|
||||
public async editEstimate(
|
||||
estimateId: number,
|
||||
estimateDTO: EditSaleEstimateDto,
|
||||
): Promise<SaleEstimate> {
|
||||
// Retrieve details of the given sale estimate id.
|
||||
const oldSaleEstimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(estimateId);
|
||||
|
||||
// Validates the given estimate existance.
|
||||
this.validators.validateEstimateExistance(oldSaleEstimate);
|
||||
|
||||
// Retrieve the given customer or throw not found service error.
|
||||
const customer = await this.customerModel()
|
||||
.query()
|
||||
.findById(estimateDTO.customerId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Transform DTO object to model object.
|
||||
const estimateObj = await this.transformerDTO.transformDTOToModel(
|
||||
estimateDTO,
|
||||
customer,
|
||||
oldSaleEstimate,
|
||||
);
|
||||
// Validate estimate number uniquiness on the storage.
|
||||
if (estimateDTO.estimateNumber) {
|
||||
await this.validators.validateEstimateNumberExistance(
|
||||
estimateDTO.estimateNumber,
|
||||
estimateId,
|
||||
);
|
||||
}
|
||||
// Validate sale estimate entries existance.
|
||||
await this.itemsEntriesService.validateEntriesIdsExistance(
|
||||
estimateId,
|
||||
'SaleEstimate',
|
||||
estimateDTO.entries,
|
||||
);
|
||||
// Validate items IDs existance on the storage.
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||
estimateDTO.entries,
|
||||
);
|
||||
// Validate non-sellable items.
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||
estimateDTO.entries,
|
||||
);
|
||||
// Edits estimate transaction with associated transactions
|
||||
// under unit-of-work envirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Trigger `onSaleEstimateEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onEditing, {
|
||||
oldSaleEstimate,
|
||||
estimateDTO,
|
||||
trx,
|
||||
} as ISaleEstimateEditingPayload);
|
||||
|
||||
// Upsert the estimate graph to the storage.
|
||||
const saleEstimate = await this.saleEstimateModel()
|
||||
.query(trx)
|
||||
.upsertGraphAndFetch({
|
||||
id: estimateId,
|
||||
...estimateObj,
|
||||
});
|
||||
|
||||
// Trigger `onSaleEstimateEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onEdited, {
|
||||
estimateId,
|
||||
saleEstimate,
|
||||
oldSaleEstimate,
|
||||
estimateDTO,
|
||||
trx,
|
||||
} as ISaleEstimateEditedPayload);
|
||||
|
||||
return saleEstimate;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as moment from 'moment';
|
||||
import { Knex } from 'knex';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
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 { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class RejectSaleEstimateService {
|
||||
constructor(
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Mark the sale estimate as rejected from the customer.
|
||||
* @param {number} saleEstimateId
|
||||
*/
|
||||
public async rejectSaleEstimate(saleEstimateId: number): Promise<void> {
|
||||
// Retrieve details of the given sale estimate id.
|
||||
const saleEstimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(saleEstimateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Throws error in case the sale estimate still not delivered to customer.
|
||||
if (!saleEstimate.isDelivered) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
|
||||
}
|
||||
// Throws error in case the sale estimate already rejected.
|
||||
if (saleEstimate.isRejected) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_REJECTED);
|
||||
}
|
||||
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Mark the sale estimate as reject on the storage.
|
||||
await this.saleEstimateModel()
|
||||
.query(trx)
|
||||
.where('id', saleEstimateId)
|
||||
.patch({
|
||||
rejectedAt: moment().toMySqlDateTime(),
|
||||
approvedAt: null,
|
||||
});
|
||||
// Triggers `onSaleEstimateRejected` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onRejected, {});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import * as R from 'ramda';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import * as composeAsync from 'async/compose';
|
||||
import { SaleEstimateValidators } from './SaleEstimateValidators.service';
|
||||
import { formatDateFields } from '@/utils/format-date-fields';
|
||||
import * as moment from 'moment';
|
||||
import { SaleEstimateIncrement } from './SaleEstimateIncrement.service';
|
||||
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
|
||||
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
|
||||
import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { ISaleEstimateDTO } from '../types/SaleEstimates.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { CommandSaleEstimateDto } from '../dtos/SaleEstimate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class SaleEstimateDTOTransformer {
|
||||
constructor(
|
||||
@Inject(ItemEntry.name)
|
||||
private itemEntryModel: TenantModelProxy<typeof ItemEntry>,
|
||||
|
||||
private readonly validators: SaleEstimateValidators,
|
||||
private readonly branchDTOTransform: BranchTransactionDTOTransformer,
|
||||
private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform,
|
||||
private readonly estimateIncrement: SaleEstimateIncrement,
|
||||
private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Transform create DTO object ot model object.
|
||||
* @param {ISaleEstimateDTO} saleEstimateDTO - Sale estimate DTO.
|
||||
* @param {Customer} paymentCustomer - Payment customer.
|
||||
* @param {SaleEstimate} oldSaleEstimate - Old sale estimate.
|
||||
* @return {ISaleEstimate}
|
||||
*/
|
||||
async transformDTOToModel(
|
||||
estimateDTO: CommandSaleEstimateDto,
|
||||
paymentCustomer: Customer,
|
||||
oldSaleEstimate?: SaleEstimate,
|
||||
): Promise<SaleEstimate> {
|
||||
const amount = sumBy(estimateDTO.entries, (e) =>
|
||||
this.itemEntryModel().calcAmount(e),
|
||||
);
|
||||
// Retrieve the next invoice number.
|
||||
const autoNextNumber = await this.estimateIncrement.getNextEstimateNumber();
|
||||
|
||||
// Retrieve the next estimate number.
|
||||
const estimateNumber =
|
||||
estimateDTO.estimateNumber ||
|
||||
oldSaleEstimate?.estimateNumber ||
|
||||
autoNextNumber;
|
||||
|
||||
// Validate the sale estimate number require.
|
||||
this.validators.validateEstimateNoRequire(estimateNumber);
|
||||
|
||||
const entries = R.compose(
|
||||
// Associate the reference type to item entries.
|
||||
R.map((entry) => R.assoc('reference_type', 'SaleEstimate', entry)),
|
||||
|
||||
// Associate default index to item entries.
|
||||
assocItemEntriesDefaultIndex,
|
||||
)(estimateDTO.entries);
|
||||
|
||||
const initialDTO = {
|
||||
amount,
|
||||
...formatDateFields(
|
||||
omit(estimateDTO, ['delivered', 'entries', 'attachments']),
|
||||
['estimateDate', 'expirationDate'],
|
||||
),
|
||||
currencyCode: paymentCustomer.currencyCode,
|
||||
exchangeRate: estimateDTO.exchangeRate || 1,
|
||||
...(estimateNumber ? { estimateNumber } : {}),
|
||||
entries,
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
...(estimateDTO.delivered &&
|
||||
!oldSaleEstimate?.deliveredAt && {
|
||||
deliveredAt: moment().toMySqlDateTime(),
|
||||
}),
|
||||
};
|
||||
const asyncDto = await composeAsync(
|
||||
this.branchDTOTransform.transformDTO<SaleEstimate>,
|
||||
this.warehouseDTOTransform.transformDTO<SaleEstimate>,
|
||||
|
||||
// Assigns the default branding template id to the invoice DTO.
|
||||
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||
'SaleEstimate',
|
||||
),
|
||||
)(initialDTO);
|
||||
|
||||
return asyncDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve estimate number to object model.
|
||||
* @param {ISaleEstimateDTO} saleEstimateDTO
|
||||
* @param {ISaleEstimate} oldSaleEstimate
|
||||
*/
|
||||
public async transformEstimateNumberToModel(
|
||||
saleEstimateDTO: ISaleEstimateDTO,
|
||||
oldSaleEstimate?: SaleEstimate,
|
||||
): Promise<string> {
|
||||
const autoNextNumber = await this.estimateIncrement.getNextEstimateNumber();
|
||||
|
||||
if (saleEstimateDTO.estimateNumber) {
|
||||
return saleEstimateDTO.estimateNumber;
|
||||
}
|
||||
return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class SaleEstimateIncrement {
|
||||
constructor(
|
||||
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the next unique estimate number.
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
public getNextEstimateNumber(): Promise<string> {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
'sales_estimates',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the estimate next number.
|
||||
*/
|
||||
public incrementNextEstimateNumber() {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
'sales_estimates',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// import { Service, Inject } from 'typedi';
|
||||
// import moment from 'moment';
|
||||
// import events from '@/subscribers/events';
|
||||
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
// import SaleNotifyBySms from '../SaleNotifyBySms';
|
||||
// import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
|
||||
// import {
|
||||
// ICustomer,
|
||||
// IPaymentReceivedSmsDetails,
|
||||
// ISaleEstimate,
|
||||
// SMS_NOTIFICATION_KEY,
|
||||
// } from '@/interfaces';
|
||||
// import { Tenant, TenantMetadata } from '@/system/models';
|
||||
// import { formatNumber, formatSmsMessage } from 'utils';
|
||||
// import { ServiceError } from '@/exceptions';
|
||||
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
// const ERRORS = {
|
||||
// SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||
// };
|
||||
|
||||
// @Service()
|
||||
// export class SaleEstimateNotifyBySms {
|
||||
// @Inject()
|
||||
// private tenancy: HasTenancyService;
|
||||
|
||||
// @Inject()
|
||||
// private saleSmsNotification: SaleNotifyBySms;
|
||||
|
||||
// @Inject()
|
||||
// private eventPublisher: EventPublisher;
|
||||
|
||||
// @Inject()
|
||||
// private smsNotificationsSettings: SmsNotificationsSettingsService;
|
||||
|
||||
// /**
|
||||
// *
|
||||
// * @param {number} tenantId
|
||||
// * @param {number} saleEstimateId
|
||||
// * @returns {Promise<ISaleEstimate>}
|
||||
// */
|
||||
// public notifyBySms = async (
|
||||
// tenantId: number,
|
||||
// saleEstimateId: number
|
||||
// ): Promise<ISaleEstimate> => {
|
||||
// const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
// // Retrieve the sale invoice or throw not found service error.
|
||||
// const saleEstimate = await SaleEstimate.query()
|
||||
// .findById(saleEstimateId)
|
||||
// .withGraphFetched('customer');
|
||||
|
||||
// // Validates the estimate transaction existance.
|
||||
// this.validateEstimateExistance(saleEstimate);
|
||||
|
||||
// // Validate the customer phone number existance and number validation.
|
||||
// this.saleSmsNotification.validateCustomerPhoneNumber(
|
||||
// saleEstimate.customer.personalPhone
|
||||
// );
|
||||
// // Triggers `onSaleEstimateNotifySms` event.
|
||||
// await this.eventPublisher.emitAsync(events.saleEstimate.onNotifySms, {
|
||||
// tenantId,
|
||||
// saleEstimate,
|
||||
// });
|
||||
// await this.sendSmsNotification(tenantId, saleEstimate);
|
||||
|
||||
// // Triggers `onSaleEstimateNotifySms` event.
|
||||
// await this.eventPublisher.emitAsync(events.saleEstimate.onNotifiedSms, {
|
||||
// tenantId,
|
||||
// saleEstimate,
|
||||
// });
|
||||
// return saleEstimate;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// *
|
||||
// * @param {number} tenantId
|
||||
// * @param {ISaleEstimate} saleEstimate
|
||||
// * @returns
|
||||
// */
|
||||
// private sendSmsNotification = async (
|
||||
// tenantId: number,
|
||||
// saleEstimate: ISaleEstimate & { customer: ICustomer }
|
||||
// ) => {
|
||||
// const smsClient = this.tenancy.smsClient(tenantId);
|
||||
// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// // Retrieve the formatted sms notification message for estimate details.
|
||||
// const formattedSmsMessage = this.formattedEstimateDetailsMessage(
|
||||
// tenantId,
|
||||
// saleEstimate,
|
||||
// tenantMetadata
|
||||
// );
|
||||
// const phoneNumber = saleEstimate.customer.personalPhone;
|
||||
|
||||
// // Runs the send message job.
|
||||
// return smsClient.sendMessageJob(phoneNumber, formattedSmsMessage);
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * Notify via SMS message after estimate creation.
|
||||
// * @param {number} tenantId
|
||||
// * @param {number} saleEstimateId
|
||||
// * @returns {Promise<void>}
|
||||
// */
|
||||
// public notifyViaSmsNotificationAfterCreation = async (
|
||||
// tenantId: number,
|
||||
// saleEstimateId: number
|
||||
// ): Promise<void> => {
|
||||
// const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||
// tenantId,
|
||||
// SMS_NOTIFICATION_KEY.SALE_ESTIMATE_DETAILS
|
||||
// );
|
||||
// // Can't continue if the sms auto-notification is not enabled.
|
||||
// if (!notification.isNotificationEnabled) return;
|
||||
|
||||
// await this.notifyBySms(tenantId, saleEstimateId);
|
||||
// };
|
||||
|
||||
// /**
|
||||
// *
|
||||
// * @param {number} tenantId
|
||||
// * @param {ISaleEstimate} saleEstimate
|
||||
// * @param {TenantMetadata} tenantMetadata
|
||||
// * @returns {string}
|
||||
// */
|
||||
// private formattedEstimateDetailsMessage = (
|
||||
// tenantId: number,
|
||||
// saleEstimate: ISaleEstimate,
|
||||
// tenantMetadata: TenantMetadata
|
||||
// ): string => {
|
||||
// const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||
// tenantId,
|
||||
// SMS_NOTIFICATION_KEY.SALE_ESTIMATE_DETAILS
|
||||
// );
|
||||
// return this.formateEstimateDetailsMessage(
|
||||
// notification.smsMessage,
|
||||
// saleEstimate,
|
||||
// tenantMetadata
|
||||
// );
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * Formattes the estimate sms notification details message.
|
||||
// * @param {string} smsMessage
|
||||
// * @param {ISaleEstimate} saleEstimate
|
||||
// * @param {TenantMetadata} tenantMetadata
|
||||
// * @returns {string}
|
||||
// */
|
||||
// private formateEstimateDetailsMessage = (
|
||||
// smsMessage: string,
|
||||
// saleEstimate: ISaleEstimate & { customer: ICustomer },
|
||||
// tenantMetadata: TenantMetadata
|
||||
// ) => {
|
||||
// const formattedAmount = formatNumber(saleEstimate.amount, {
|
||||
// currencyCode: saleEstimate.currencyCode,
|
||||
// });
|
||||
|
||||
// return formatSmsMessage(smsMessage, {
|
||||
// EstimateNumber: saleEstimate.estimateNumber,
|
||||
// ReferenceNumber: saleEstimate.reference,
|
||||
// EstimateDate: moment(saleEstimate.estimateDate).format('YYYY/MM/DD'),
|
||||
// ExpirationDate: saleEstimate.expirationDate
|
||||
// ? moment(saleEstimate.expirationDate).format('YYYY/MM/DD')
|
||||
// : '',
|
||||
// CustomerName: saleEstimate.customer.displayName,
|
||||
// Amount: formattedAmount,
|
||||
// CompanyName: tenantMetadata.name,
|
||||
// });
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * Retrieve the SMS details of the given payment receive transaction.
|
||||
// * @param {number} tenantId
|
||||
// * @param {number} saleEstimateId
|
||||
// * @returns {Promise<IPaymentReceivedSmsDetails>}
|
||||
// */
|
||||
// public smsDetails = async (
|
||||
// tenantId: number,
|
||||
// saleEstimateId: number
|
||||
// ): Promise<IPaymentReceivedSmsDetails> => {
|
||||
// const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
// // Retrieve the sale invoice or throw not found service error.
|
||||
// const saleEstimate = await SaleEstimate.query()
|
||||
// .findById(saleEstimateId)
|
||||
// .withGraphFetched('customer');
|
||||
|
||||
// // Validates the estimate existance.
|
||||
// this.validateEstimateExistance(saleEstimate);
|
||||
|
||||
// // Retrieve the current tenant metadata.
|
||||
// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// // Retrieve the formatted sms message from the given estimate model.
|
||||
// const formattedSmsMessage = this.formattedEstimateDetailsMessage(
|
||||
// tenantId,
|
||||
// saleEstimate,
|
||||
// tenantMetadata
|
||||
// );
|
||||
// return {
|
||||
// customerName: saleEstimate.customer.displayName,
|
||||
// customerPhoneNumber: saleEstimate.customer.personalPhone,
|
||||
// smsMessage: formattedSmsMessage,
|
||||
// };
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * Validates the sale estimate existance.
|
||||
// * @param {ISaleEstimate} saleEstimate -
|
||||
// */
|
||||
// private validateEstimateExistance(saleEstimate: ISaleEstimate) {
|
||||
// if (!saleEstimate) {
|
||||
// throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ERRORS } from '../constants';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class SaleEstimateValidators {
|
||||
constructor(
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the given estimate existance.
|
||||
* @param {SaleEstimate | undefined | null} estimate - The sale estimate.
|
||||
*/
|
||||
public validateEstimateExistance(estimate: SaleEstimate | undefined | null) {
|
||||
if (!estimate) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the estimate number unique on the storage.
|
||||
* @param {string} estimateNumber - The estimate number.
|
||||
* @param {number} notEstimateId - The estimate id to exclude from the search.
|
||||
*/
|
||||
public async validateEstimateNumberExistance(
|
||||
estimateNumber: string,
|
||||
notEstimateId?: number,
|
||||
) {
|
||||
const foundSaleEstimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findOne('estimate_number', estimateNumber)
|
||||
.onBuild((builder) => {
|
||||
if (notEstimateId) {
|
||||
builder.whereNot('id', notEstimateId);
|
||||
}
|
||||
});
|
||||
if (foundSaleEstimate) {
|
||||
throw new ServiceError(
|
||||
ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE,
|
||||
'The given sale estimate is not unique.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given sale estimate not already converted to invoice.
|
||||
* @param {SaleEstimate} saleEstimate -
|
||||
*/
|
||||
public validateEstimateNotConverted(saleEstimate: SaleEstimate) {
|
||||
if (saleEstimate.isConvertedToInvoice) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the sale estimate number require.
|
||||
* @param {string} estimateNumber
|
||||
*/
|
||||
public validateEstimateNoRequire(estimateNumber: string) {
|
||||
if (!estimateNumber) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given customer has no sales estimates.
|
||||
* @param {number} customerId - The customer id.
|
||||
*/
|
||||
public async validateCustomerHasNoEstimates(customerId: number) {
|
||||
const estimates = await this.saleEstimateModel()
|
||||
.query()
|
||||
.where('customer_id', customerId);
|
||||
|
||||
if (estimates.length > 0) {
|
||||
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_ESTIMATES);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bull';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ContactMailNotification } from '@/modules/MailNotification/ContactMailNotification';
|
||||
import {
|
||||
DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT,
|
||||
DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
|
||||
} from '../constants';
|
||||
import { GetSaleEstimate } from '../queries/GetSaleEstimate.service';
|
||||
import { transformEstimateToMailDataArgs } from '../utils';
|
||||
import { GetSaleEstimatePdf } from '../queries/GetSaleEstimatePdf';
|
||||
import { events } from '@/common/events/events';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { mergeAndValidateMailOptions } from '@/modules/MailNotification/utils';
|
||||
import {
|
||||
ISaleEstimateMailPresendEvent,
|
||||
SaleEstimateMailOptionsDTO,
|
||||
SendSaleEstimateMailJob,
|
||||
SendSaleEstimateMailQueue,
|
||||
} from '../types/SaleEstimates.types';
|
||||
import { SaleEstimateMailOptions } from '../types/SaleEstimates.types';
|
||||
import { Mail } from '@/modules/Mail/Mail';
|
||||
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class SendSaleEstimateMail {
|
||||
/**
|
||||
* @param {GetSaleEstimatePdf} estimatePdf - Estimate pdf service.
|
||||
* @param {GetSaleEstimate} getSaleEstimateService - Get sale estimate service.
|
||||
* @param {ContactMailNotification} contactMailNotification - Contact mail notification service.
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {MailTransporter} mailTransporter - Mail transporter service.
|
||||
* @param {typeof SaleEstimate} saleEstimateModel - Sale estimate model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly estimatePdf: GetSaleEstimatePdf,
|
||||
private readonly getSaleEstimateService: GetSaleEstimate,
|
||||
private readonly contactMailNotification: ContactMailNotification,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly mailTransporter: MailTransporter,
|
||||
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
|
||||
@InjectQueue(SendSaleEstimateMailQueue)
|
||||
private readonly sendEstimateMailQueue: Queue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Triggers the reminder mail of the given sale estimate.
|
||||
* @param {number} saleEstimateId - Sale estimate id.
|
||||
* @param {SaleEstimateMailOptionsDTO} messageOptions - Sale estimate mail options.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async triggerMail(
|
||||
saleEstimateId: number,
|
||||
messageOptions: SaleEstimateMailOptionsDTO,
|
||||
): Promise<void> {
|
||||
const payload = {
|
||||
saleEstimateId,
|
||||
messageOptions,
|
||||
};
|
||||
await this.sendEstimateMailQueue.add(SendSaleEstimateMailJob, payload);
|
||||
|
||||
// Triggers `onSaleEstimatePreMailSend` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onPreMailSend, {
|
||||
saleEstimateId,
|
||||
messageOptions,
|
||||
} as ISaleEstimateMailPresendEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate the text of the mail.
|
||||
* @param {number} estimateId - Estimate id.
|
||||
* @returns {Promise<Record<string, any>>}
|
||||
*/
|
||||
public formatterArgs = async (estimateId: number) => {
|
||||
const estimate = await this.getSaleEstimateService.getEstimate(estimateId);
|
||||
return transformEstimateToMailDataArgs(estimate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the mail options.
|
||||
* @param {number} saleEstimateId - Sale estimate id.
|
||||
* @param {string} defaultSubject - Default subject.
|
||||
* @param {string} defaultMessage - Default message.
|
||||
* @returns {Promise<SaleEstimateMailOptions>}
|
||||
*/
|
||||
public getMailOptions = async (
|
||||
saleEstimateId: number,
|
||||
defaultSubject: string = DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
|
||||
defaultMessage: string = DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT,
|
||||
): Promise<SaleEstimateMailOptions> => {
|
||||
const saleEstimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(saleEstimateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const formatArgs = await this.formatterArgs(saleEstimateId);
|
||||
|
||||
const mailOptions =
|
||||
await this.contactMailNotification.getDefaultMailOptions(
|
||||
saleEstimate.customerId,
|
||||
);
|
||||
return {
|
||||
...mailOptions,
|
||||
message: defaultMessage,
|
||||
subject: defaultSubject,
|
||||
attachEstimate: true,
|
||||
formatArgs,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the given mail options.
|
||||
* @param {number} saleEstimateId - Sale estimate id.
|
||||
* @param {SaleEstimateMailOptions} mailOptions - Sale estimate mail options.
|
||||
* @returns {Promise<SaleEstimateMailOptions>}
|
||||
*/
|
||||
public formatMailOptions = async (
|
||||
saleEstimateId: number,
|
||||
mailOptions: SaleEstimateMailOptions,
|
||||
): Promise<SaleEstimateMailOptions> => {
|
||||
const formatterArgs = await this.formatterArgs(saleEstimateId);
|
||||
const formattedOptions =
|
||||
await this.contactMailNotification.formatMailOptions(
|
||||
mailOptions,
|
||||
formatterArgs,
|
||||
);
|
||||
return { ...formattedOptions };
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends the mail notification of the given sale estimate.
|
||||
* @param {number} saleEstimateId - Sale estimate id.
|
||||
* @param {SaleEstimateMailOptions} messageOptions - Sale estimate mail options.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async sendMail(
|
||||
saleEstimateId: number,
|
||||
messageOptions: SaleEstimateMailOptionsDTO,
|
||||
): Promise<void> {
|
||||
const localMessageOpts = await this.getMailOptions(saleEstimateId);
|
||||
// Overrides and validates the given mail options.
|
||||
const parsedMessageOptions = mergeAndValidateMailOptions(
|
||||
localMessageOpts,
|
||||
messageOptions,
|
||||
) as SaleEstimateMailOptions;
|
||||
|
||||
const formattedOptions = await this.formatMailOptions(
|
||||
saleEstimateId,
|
||||
parsedMessageOptions,
|
||||
);
|
||||
const mail = new Mail()
|
||||
.setSubject(formattedOptions.subject)
|
||||
.setTo(formattedOptions.to)
|
||||
.setCC(formattedOptions.cc)
|
||||
.setBCC(formattedOptions.bcc)
|
||||
.setContent(formattedOptions.message);
|
||||
|
||||
// Attaches the estimate pdf to the mail.
|
||||
if (formattedOptions.attachEstimate) {
|
||||
// Retrieves the estimate pdf and attaches it to the mail.
|
||||
const [estimatePdfBuffer, estimateFilename] =
|
||||
await this.estimatePdf.getSaleEstimatePdf(saleEstimateId);
|
||||
|
||||
mail.setAttachments([
|
||||
{
|
||||
filename: `${estimateFilename}.pdf`,
|
||||
content: estimatePdfBuffer,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const eventPayload = {
|
||||
saleEstimateId,
|
||||
messageOptions,
|
||||
formattedOptions,
|
||||
};
|
||||
// Triggers `onSaleEstimateMailSend` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleEstimate.onMailSend,
|
||||
eventPayload as ISaleEstimateMailPresendEvent,
|
||||
);
|
||||
await this.mailTransporter.send(mail);
|
||||
|
||||
// Triggers `onSaleEstimateMailSent` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleEstimate.onMailSent,
|
||||
eventPayload as ISaleEstimateMailPresendEvent,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { SendSaleEstimateMail } from './SendSaleEstimateMail';
|
||||
|
||||
// @Service()
|
||||
// export class SendSaleEstimateMailJob {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define(
|
||||
// 'sale-estimate-mail-send',
|
||||
// { priority: 'high', concurrency: 2 },
|
||||
// this.handler
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const { tenantId, saleEstimateId, messageOptions } = job.attrs.data;
|
||||
// const sendSaleEstimateMail = Container.get(SendSaleEstimateMail);
|
||||
|
||||
// try {
|
||||
// await sendSaleEstimateMail.sendMail(
|
||||
// tenantId,
|
||||
// saleEstimateId,
|
||||
// messageOptions
|
||||
// );
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class UnlinkConvertedSaleEstimate {
|
||||
constructor(
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Unlink the converted sale estimates from the given sale invoice.
|
||||
* @param {number} invoiceId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async unlinkConvertedEstimateFromInvoice(
|
||||
invoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
await this.saleEstimateModel()
|
||||
.query(trx)
|
||||
.where({
|
||||
convertedToInvoiceId: invoiceId,
|
||||
})
|
||||
.patch({
|
||||
convertedToInvoiceId: null,
|
||||
convertedToInvoiceAt: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
287
packages/server/src/modules/SaleEstimates/constants.ts
Normal file
287
packages/server/src/modules/SaleEstimates/constants.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
|
||||
'Estimate {Estimate Number} is awaiting your approval';
|
||||
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {Customer Name}</p>
|
||||
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
|
||||
<p>
|
||||
Estimate <strong>#{Estimate Number}</strong><br />
|
||||
Expiration Date : <strong>{Estimate Expiration Date}</strong><br />
|
||||
Amount : <strong>{Estimate Amount}</strong></br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{Company Name}</i>
|
||||
</p>
|
||||
`;
|
||||
|
||||
export const ERRORS = {
|
||||
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
|
||||
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE',
|
||||
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
|
||||
SALE_ESTIMATE_ALREADY_REJECTED: 'SALE_ESTIMATE_ALREADY_REJECTED',
|
||||
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
|
||||
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
|
||||
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
|
||||
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
name: 'Draft',
|
||||
slug: 'draft',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Delivered',
|
||||
slug: 'delivered',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'delivered',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Approved',
|
||||
slug: 'approved',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'approved',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Rejected',
|
||||
slug: 'rejected',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'rejected',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Invoiced',
|
||||
slug: 'invoiced',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'invoiced',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Expired',
|
||||
slug: 'expired',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'expired',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Closed',
|
||||
slug: 'closed',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'closed',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
|
||||
export const SaleEstimatesSampleData = [
|
||||
{
|
||||
Customer: 'Ambrose Olson',
|
||||
'Estimate Date': '2024-01-01',
|
||||
'Expiration Date': '2025-01-01',
|
||||
'Estimate No.': 'EST-0001',
|
||||
'Reference No.': 'REF-0001',
|
||||
Currency: '',
|
||||
'Exchange Rate': '',
|
||||
Note: 'Vel autem quis aut ab.',
|
||||
'Terms & Conditions': 'Provident illo architecto sit iste in.',
|
||||
Delivered: 'T',
|
||||
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||
Quantity: 1000,
|
||||
Rate: 20,
|
||||
'Line Description': 'Rem esse doloremque praesentium harum maiores.',
|
||||
},
|
||||
{
|
||||
Customer: 'Ambrose Olson',
|
||||
'Estimate Date': '2024-01-02',
|
||||
'Expiration Date': '2025-01-02',
|
||||
'Estimate No.': 'EST-0002',
|
||||
'Reference No.': 'REF-0002',
|
||||
Currency: '',
|
||||
'Exchange Rate': '',
|
||||
Note: 'Tempora voluptas odio deleniti rerum vitae consequatur nihil quis sunt.',
|
||||
'Terms & Conditions': 'Ut eum incidunt quibusdam rerum vero.',
|
||||
Delivered: 'T',
|
||||
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||
Quantity: 1000,
|
||||
Rate: 20,
|
||||
'Line Description': 'Qui voluptate aliquam maxime aliquam.',
|
||||
},
|
||||
{
|
||||
Customer: 'Ambrose Olson',
|
||||
'Estimate Date': '2024-01-03',
|
||||
'Expiration Date': '2025-01-03',
|
||||
'Estimate No.': 'EST-0003',
|
||||
'Reference No.': 'REF-0003',
|
||||
Currency: '',
|
||||
'Exchange Rate': '',
|
||||
Note: 'Quia voluptatem delectus doloremque.',
|
||||
'Terms & Conditions': 'Facilis porro vitae ratione.',
|
||||
Delivered: 'T',
|
||||
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||
Quantity: 1000,
|
||||
Rate: 20,
|
||||
'Line Description': 'Qui suscipit ducimus qui qui.',
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultEstimatePdfBrandingAttributes = {
|
||||
primaryColor: '#000',
|
||||
secondaryColor: '#000',
|
||||
|
||||
// # Company logo
|
||||
showCompanyLogo: true,
|
||||
companyLogoUri: '',
|
||||
companyLogoKey: '',
|
||||
|
||||
companyName: '',
|
||||
|
||||
customerAddress: '',
|
||||
companyAddress: '',
|
||||
showCustomerAddress: true,
|
||||
showCompanyAddress: true,
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
total: '$1000.00',
|
||||
totalLabel: 'Total',
|
||||
showTotal: true,
|
||||
|
||||
subtotal: '1000/00',
|
||||
subtotalLabel: 'Subtotal',
|
||||
showSubtotal: true,
|
||||
|
||||
showCustomerNote: true,
|
||||
customerNote:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
customerNoteLabel: 'Customer Note',
|
||||
|
||||
showTermsConditions: true,
|
||||
termsConditions:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
termsConditionsLabel: 'Terms & Conditions',
|
||||
|
||||
lines: [
|
||||
{
|
||||
item: 'Simply dummy text',
|
||||
description: 'Simply dummy text of the printing and typesetting',
|
||||
rate: '1',
|
||||
quantity: '1000',
|
||||
total: '$1000.00',
|
||||
},
|
||||
],
|
||||
showEstimateNumber: true,
|
||||
estimateNumberLabel: 'Estimate Number',
|
||||
estimateNumebr: '346D3D40-0001',
|
||||
|
||||
estimateDate: 'September 3, 2024',
|
||||
showEstimateDate: true,
|
||||
estimateDateLabel: 'Estimate Date',
|
||||
|
||||
expirationDateLabel: 'Expiration Date',
|
||||
showExpirationDate: true,
|
||||
expirationDate: 'September 3, 2024',
|
||||
};
|
||||
|
||||
interface EstimatePdfBrandingLineItem {
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export interface EstimatePdfBrandingAttributes {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
showCompanyLogo: boolean;
|
||||
companyLogo: string;
|
||||
companyName: string;
|
||||
|
||||
// Customer Address
|
||||
showCustomerAddress: boolean;
|
||||
customerAddress: string;
|
||||
|
||||
// Company Address
|
||||
showCompanyAddress: boolean;
|
||||
companyAddress: string;
|
||||
billedToLabel: string;
|
||||
|
||||
total: string;
|
||||
totalLabel: string;
|
||||
showTotal: boolean;
|
||||
|
||||
subtotal: string;
|
||||
subtotalLabel: string;
|
||||
showSubtotal: boolean;
|
||||
|
||||
showCustomerNote: boolean;
|
||||
customerNote: string;
|
||||
customerNoteLabel: string;
|
||||
|
||||
showTermsConditions: boolean;
|
||||
termsConditions: string;
|
||||
termsConditionsLabel: string;
|
||||
|
||||
lines: EstimatePdfBrandingLineItem[];
|
||||
|
||||
showEstimateNumber: boolean;
|
||||
estimateNumberLabel: string;
|
||||
estimateNumebr: string;
|
||||
|
||||
estimateDate: string;
|
||||
showEstimateDate: boolean;
|
||||
estimateDateLabel: string;
|
||||
|
||||
expirationDateLabel: string;
|
||||
showExpirationDate: boolean;
|
||||
expirationDate: string;
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
MinLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
enum DiscountType {
|
||||
Percentage = 'percentage',
|
||||
Amount = 'amount',
|
||||
}
|
||||
|
||||
class SaleEstimateEntryDto extends ItemEntryDto {}
|
||||
|
||||
class AttachmentDto {
|
||||
@IsString()
|
||||
key: string;
|
||||
}
|
||||
export class CommandSaleEstimateDto {
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
description: 'The id of the customer',
|
||||
example: 1,
|
||||
})
|
||||
customerId: number;
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiProperty({
|
||||
description: 'The date of the estimate',
|
||||
example: '2021-01-01',
|
||||
})
|
||||
estimateDate: Date;
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiProperty({
|
||||
description: 'The expiration date of the estimate',
|
||||
example: '2021-01-01',
|
||||
})
|
||||
expirationDate: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The reference of the estimate',
|
||||
example: '123456',
|
||||
})
|
||||
reference?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
estimateNumber?: string;
|
||||
|
||||
@IsBoolean()
|
||||
delivered: boolean = false;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0.01)
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The exchange rate of the estimate',
|
||||
example: 1,
|
||||
})
|
||||
exchangeRate?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The id of the warehouse',
|
||||
example: 1,
|
||||
})
|
||||
warehouseId?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The id of the branch',
|
||||
example: 1,
|
||||
})
|
||||
branchId?: number;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SaleEstimateEntryDto)
|
||||
@ApiProperty({
|
||||
description: 'The entries of the estimate',
|
||||
example: [
|
||||
{
|
||||
index: 1,
|
||||
itemId: 1,
|
||||
description: 'This is a description',
|
||||
quantity: 100,
|
||||
cost: 100,
|
||||
},
|
||||
],
|
||||
})
|
||||
entries: SaleEstimateEntryDto[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The note of the estimate',
|
||||
example: 'This is a note',
|
||||
})
|
||||
note?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The terms and conditions of the estimate',
|
||||
example: 'This is a terms and conditions',
|
||||
})
|
||||
termsConditions?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The email to send the estimate to',
|
||||
example: 'test@test.com',
|
||||
})
|
||||
sendToEmail?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AttachmentDto)
|
||||
@ApiProperty({
|
||||
description: 'The attachments of the estimate',
|
||||
example: [
|
||||
{
|
||||
key: '123456',
|
||||
},
|
||||
],
|
||||
})
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The id of the pdf template',
|
||||
example: 1,
|
||||
})
|
||||
pdfTemplateId?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The discount of the estimate',
|
||||
example: 1,
|
||||
})
|
||||
discount?: number;
|
||||
|
||||
@IsEnum(DiscountType)
|
||||
@ApiProperty({
|
||||
description: 'The type of the discount',
|
||||
example: DiscountType.Amount,
|
||||
})
|
||||
discountType: DiscountType = DiscountType.Amount;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The adjustment of the estimate',
|
||||
example: 1,
|
||||
})
|
||||
adjustment?: number;
|
||||
}
|
||||
|
||||
export class CreateSaleEstimateDto extends CommandSaleEstimateDto {}
|
||||
export class EditSaleEstimateDto extends CommandSaleEstimateDto {}
|
||||
328
packages/server/src/modules/SaleEstimates/models/SaleEstimate.ts
Normal file
328
packages/server/src/modules/SaleEstimates/models/SaleEstimate.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
import * as moment from 'moment';
|
||||
import { Model } from 'objection';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class SaleEstimate extends TenantBaseModel {
|
||||
exchangeRate!: number;
|
||||
amount!: number;
|
||||
|
||||
currencyCode!: string;
|
||||
|
||||
customerId!: number;
|
||||
estimateDate!: Date | string;
|
||||
expirationDate!: Date | string;
|
||||
reference!: string;
|
||||
estimateNumber!: string;
|
||||
note!: string;
|
||||
termsConditions!: string;
|
||||
sendToEmail!: string;
|
||||
|
||||
deliveredAt!: Date | string;
|
||||
approvedAt!: Date | string;
|
||||
rejectedAt!: Date | string;
|
||||
|
||||
userId!: number;
|
||||
|
||||
convertedToInvoiceId!: number;
|
||||
convertedToInvoiceAt!: Date | string;
|
||||
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date | null;
|
||||
|
||||
branchId?: number;
|
||||
warehouseId?: number;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'sales_estimates';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'localAmount',
|
||||
'isDelivered',
|
||||
'isExpired',
|
||||
'isConvertedToInvoice',
|
||||
'isApproved',
|
||||
'isRejected',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localAmount() {
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the sale estimate converted to sale invoice.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isConvertedToInvoice() {
|
||||
return !!(this.convertedToInvoiceId && this.convertedToInvoiceAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the estimate is delivered.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isDelivered() {
|
||||
return !!this.deliveredAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the estimate is expired.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isExpired() {
|
||||
// return defaultToTransform(
|
||||
// this.expirationDate,
|
||||
// moment().isAfter(this.expirationDate, 'day'),
|
||||
// false
|
||||
// );i
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the estimate is approved.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isApproved() {
|
||||
return !!this.approvedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the estimate is reject.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isRejected() {
|
||||
return !!this.rejectedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to mark model as resourceable to viewable and filterable.
|
||||
*/
|
||||
static get resourceable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
/**
|
||||
* Filters the drafted estimates transactions.
|
||||
*/
|
||||
draft(query) {
|
||||
query.where('delivered_at', null);
|
||||
},
|
||||
/**
|
||||
* Filters the delivered estimates transactions.
|
||||
*/
|
||||
delivered(query) {
|
||||
query.whereNot('delivered_at', null);
|
||||
},
|
||||
/**
|
||||
* Filters the expired estimates transactions.
|
||||
*/
|
||||
expired(query) {
|
||||
query.where('expiration_date', '<', moment().format('YYYY-MM-DD'));
|
||||
},
|
||||
/**
|
||||
* Filters the rejected estimates transactions.
|
||||
*/
|
||||
rejected(query) {
|
||||
query.whereNot('rejected_at', null);
|
||||
},
|
||||
/**
|
||||
* Filters the invoiced estimates transactions.
|
||||
*/
|
||||
invoiced(query) {
|
||||
query.whereNot('converted_to_invoice_at', null);
|
||||
},
|
||||
/**
|
||||
* Filters the approved estimates transactions.
|
||||
*/
|
||||
approved(query) {
|
||||
query.whereNot('approved_at', null);
|
||||
},
|
||||
/**
|
||||
* Sorting the estimates orders by delivery status.
|
||||
*/
|
||||
orderByStatus(query, order) {
|
||||
query.orderByRaw(`delivered_at is null ${order}`);
|
||||
},
|
||||
/**
|
||||
* Filtering the estimates oreders by status field.
|
||||
*/
|
||||
filterByStatus(query, filterType) {
|
||||
switch (filterType) {
|
||||
case 'draft':
|
||||
query.modify('draft');
|
||||
break;
|
||||
case 'delivered':
|
||||
query.modify('delivered');
|
||||
break;
|
||||
case 'approved':
|
||||
query.modify('approved');
|
||||
break;
|
||||
case 'rejected':
|
||||
query.modify('rejected');
|
||||
break;
|
||||
case 'invoiced':
|
||||
query.modify('invoiced');
|
||||
break;
|
||||
case 'expired':
|
||||
query.modify('expired');
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const {
|
||||
ItemEntry,
|
||||
} = require('../../TransactionItemEntry/models/ItemEntry');
|
||||
const { Customer } = require('../../Customers/models/Customer');
|
||||
const { Branch } = require('../../Branches/models/Branch.model');
|
||||
const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
|
||||
const { Document } = require('../../ChromiumlyTenancy/models/Document');
|
||||
const {
|
||||
PdfTemplateModel,
|
||||
} = require('../../PdfTemplate/models/PdfTemplate');
|
||||
|
||||
return {
|
||||
customer: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Customer,
|
||||
join: {
|
||||
from: 'sales_estimates.customerId',
|
||||
to: 'contacts.id',
|
||||
},
|
||||
filter(query) {
|
||||
query.where('contact_service', 'customer');
|
||||
},
|
||||
},
|
||||
entries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: ItemEntry,
|
||||
join: {
|
||||
from: 'sales_estimates.id',
|
||||
to: 'items_entries.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'SaleEstimate');
|
||||
builder.orderBy('index', 'ASC');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale estimate may belongs to branch.
|
||||
*/
|
||||
branch: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Branch,
|
||||
join: {
|
||||
from: 'sales_estimates.branchId',
|
||||
to: 'branches.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale estimate may has associated warehouse.
|
||||
*/
|
||||
warehouse: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Warehouse,
|
||||
join: {
|
||||
from: 'sales_estimates.warehouseId',
|
||||
to: 'warehouses.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale estimate transaction may has many attached attachments.
|
||||
*/
|
||||
attachments: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Document,
|
||||
join: {
|
||||
from: 'sales_estimates.id',
|
||||
through: {
|
||||
from: 'document_links.modelId',
|
||||
to: 'document_links.documentId',
|
||||
},
|
||||
to: 'documents.id',
|
||||
},
|
||||
filter(query) {
|
||||
query.where('model_ref', 'SaleEstimate');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale estimate may belongs to pdf branding template.
|
||||
*/
|
||||
pdfTemplate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: PdfTemplateModel,
|
||||
join: {
|
||||
from: 'sales_estimates.pdfTemplateId',
|
||||
to: 'pdf_templates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Model settings.
|
||||
*/
|
||||
// static get meta() {
|
||||
// return SaleEstimateSettings;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Retrieve the default custom views, roles and columns.
|
||||
*/
|
||||
// static get defaultViews() {
|
||||
// return DEFAULT_VIEWS;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Model search roles.
|
||||
*/
|
||||
static get searchRoles() {
|
||||
return [
|
||||
{ fieldKey: 'amount', comparator: 'equals' },
|
||||
{ condition: 'or', fieldKey: 'estimate_number', comparator: 'contains' },
|
||||
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents mutate base currency since the model is not empty.
|
||||
*/
|
||||
static get preventMutateBaseCurrency() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import {
|
||||
SendSaleEstimateMailJob,
|
||||
SendSaleEstimateMailQueue,
|
||||
} from '../types/SaleEstimates.types';
|
||||
import { SendSaleEstimateMail } from '../commands/SendSaleEstimateMail';
|
||||
|
||||
@Processor(SendSaleEstimateMailQueue)
|
||||
export class SendSaleEstimateMailProcess {
|
||||
constructor(private readonly sendEstimateMailService: SendSaleEstimateMail) {}
|
||||
|
||||
@Process(SendSaleEstimateMailJob)
|
||||
async handleSendMail(job: Job) {
|
||||
const { saleEstimateId, messageOptions } = job.data;
|
||||
|
||||
await this.sendEstimateMailService.sendMail(saleEstimateId, messageOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SaleEstimateTransfromer } from './SaleEstimate.transformer';
|
||||
import { SaleEstimateValidators } from '../commands/SaleEstimateValidators.service';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleEstimate {
|
||||
constructor(
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
|
||||
private readonly transformer: TransformerInjectable,
|
||||
private readonly validators: SaleEstimateValidators,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the estimate details with associated entries.
|
||||
* @async
|
||||
* @param {Integer} estimateId
|
||||
*/
|
||||
public async getEstimate(estimateId: number) {
|
||||
const estimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(estimateId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('branch')
|
||||
.withGraphFetched('attachments');
|
||||
|
||||
// Validates the estimate existance.
|
||||
this.validators.validateEstimateExistance(estimate);
|
||||
|
||||
// Transformes sale estimate model to POJO.
|
||||
const transformed = await this.transformer.transform(
|
||||
estimate,
|
||||
new SaleEstimateTransfromer(),
|
||||
);
|
||||
const eventPayload = { saleEstimateId: estimateId };
|
||||
|
||||
// Triggers `onSaleEstimateViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleEstimate.onViewed,
|
||||
eventPayload,
|
||||
);
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { GetSaleEstimate } from './GetSaleEstimate.service';
|
||||
import { transformEstimateToPdfTemplate } from '../utils';
|
||||
import { EstimatePdfBrandingAttributes } from '../constants';
|
||||
import { SaleEstimatePdfTemplate } from '@/modules/SaleInvoices/queries/SaleEstimatePdfTemplate.service';
|
||||
import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service';
|
||||
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
|
||||
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
||||
import { events } from '@/common/events/events';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleEstimatePdf {
|
||||
constructor(
|
||||
private readonly chromiumlyTenancy: ChromiumlyTenancy,
|
||||
private readonly templateInjectable: TemplateInjectable,
|
||||
private readonly getSaleEstimate: GetSaleEstimate,
|
||||
private readonly estimatePdfTemplate: SaleEstimatePdfTemplate,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(PdfTemplateModel.name)
|
||||
private readonly pdfTemplateModel: TenantModelProxy<
|
||||
typeof PdfTemplateModel
|
||||
>,
|
||||
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
*/
|
||||
public async getSaleEstimatePdf(
|
||||
saleEstimateId: number,
|
||||
): Promise<[Buffer, string]> {
|
||||
const filename = await this.getSaleEstimateFilename(saleEstimateId);
|
||||
const brandingAttributes =
|
||||
await this.getEstimateBrandingAttributes(saleEstimateId);
|
||||
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
'modules/estimate-regular',
|
||||
brandingAttributes,
|
||||
);
|
||||
const content =
|
||||
await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
|
||||
const eventPayload = { saleEstimateId };
|
||||
|
||||
// Triggers the `onSaleEstimatePdfViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleEstimate.onPdfViewed,
|
||||
eventPayload,
|
||||
);
|
||||
return [content, filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename file document of the given estimate.
|
||||
* @param {number} estimateId - Estimate id.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
private async getSaleEstimateFilename(estimateId: number) {
|
||||
const estimate = await this.saleEstimateModel()
|
||||
.query()
|
||||
.findById(estimateId);
|
||||
|
||||
return `Estimate-${estimate.estimateNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given estimate branding attributes.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} estimateId - Estimate id.
|
||||
* @returns {Promise<EstimatePdfBrandingAttributes>}
|
||||
*/
|
||||
async getEstimateBrandingAttributes(
|
||||
estimateId: number,
|
||||
): Promise<EstimatePdfBrandingAttributes> {
|
||||
const saleEstimate = await this.getSaleEstimate.getEstimate(estimateId);
|
||||
// Retrieve the invoice template id of not found get the default template id.
|
||||
const templateId =
|
||||
saleEstimate.pdfTemplateId ??
|
||||
(
|
||||
await this.pdfTemplateModel().query().findOne({
|
||||
resource: 'SaleEstimate',
|
||||
default: true,
|
||||
})
|
||||
)?.id;
|
||||
const brandingTemplate =
|
||||
await this.estimatePdfTemplate.getEstimatePdfTemplate(templateId);
|
||||
return {
|
||||
...brandingTemplate.attributes,
|
||||
...transformEstimateToPdfTemplate(saleEstimate),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
||||
import { ISaleEstimateState } from '../types/SaleEstimates.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleEstimateState {
|
||||
constructor(
|
||||
@Inject(PdfTemplateModel.name)
|
||||
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the create/edit sale estimate state.
|
||||
* @return {Promise<ISaleEstimateState>}
|
||||
*/
|
||||
public async getSaleEstimateState(): Promise<ISaleEstimateState> {
|
||||
const defaultPdfTemplate = await this.pdfTemplateModel()
|
||||
.query()
|
||||
.findOne({ resource: 'SaleEstimate' })
|
||||
.modify('default');
|
||||
|
||||
return {
|
||||
defaultTemplateId: defaultPdfTemplate?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as R from 'ramda';
|
||||
import { SaleEstimateTransfromer } from './SaleEstimate.transformer';
|
||||
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
|
||||
import { ISalesEstimatesFilter } from '../types/SaleEstimates.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleEstimatesService {
|
||||
constructor(
|
||||
private readonly dynamicListService: DynamicListService,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves estimates filterable and paginated list.
|
||||
* @param {IEstimatesFilter} estimatesFilter -
|
||||
*/
|
||||
public async getEstimates(filterDTO: ISalesEstimatesFilter): Promise<{
|
||||
salesEstimates: SaleEstimate[];
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}> {
|
||||
// Parses filter DTO.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(
|
||||
SaleEstimate,
|
||||
filter,
|
||||
);
|
||||
const { results, pagination } = await this.saleEstimateModel()
|
||||
.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('customer');
|
||||
builder.withGraphFetched('entries');
|
||||
builder.withGraphFetched('entries.item');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
const transformedEstimates = await this.transformer.transform(
|
||||
results,
|
||||
new SaleEstimateTransfromer(),
|
||||
);
|
||||
return {
|
||||
salesEstimates: transformedEstimates,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the sale receipts list filter DTO.
|
||||
* @param filterDTO
|
||||
*/
|
||||
private parseListFilterDTO(filterDTO) {
|
||||
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
// import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
|
||||
// import { AttachmentTransformer } from '@/services/Attachments/AttachmentTransformer';
|
||||
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
|
||||
|
||||
export class SaleEstimateTransfromer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedSubtotal',
|
||||
'formattedAmount',
|
||||
'formattedEstimateDate',
|
||||
'formattedExpirationDate',
|
||||
'formattedDeliveredAtDate',
|
||||
'formattedApprovedAtDate',
|
||||
'formattedRejectedAtDate',
|
||||
'formattedCreatedAt',
|
||||
'entries',
|
||||
'attachments',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedEstimateDate = (estimate: SaleEstimate): string => {
|
||||
return this.formatDate(estimate.estimateDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedExpirationDate = (estimate: SaleEstimate): string => {
|
||||
return this.formatDate(estimate.expirationDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted estimate created at.
|
||||
* @param {ISaleEstimate} estimate -
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreatedAt = (estimate: SaleEstimate): string => {
|
||||
return this.formatDate(estimate.createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedDeliveredAtDate = (estimate: SaleEstimate): string => {
|
||||
return this.formatDate(estimate.deliveredAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedApprovedAtDate = (estimate: SaleEstimate): string => {
|
||||
return this.formatDate(estimate.approvedAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedRejectedAtDate = (estimate: SaleEstimate): string => {
|
||||
return this.formatDate(estimate.rejectedAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice amount.
|
||||
* @param {ISaleEstimate} estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (estimate: SaleEstimate): string => {
|
||||
return this.formatNumber(estimate.amount, {
|
||||
currencyCode: estimate.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted invoice subtotal.
|
||||
* @param {ISaleEstimate} estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedSubtotal = (estimate: SaleEstimate): string => {
|
||||
return this.formatNumber(estimate.amount, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the sale estimate.
|
||||
* @param {ISaleEstimate} estimate
|
||||
* @returns {}
|
||||
*/
|
||||
protected entries = (estimate: SaleEstimate) => {
|
||||
// return this.item(estimate.entries, new ItemEntryTransformer(), {
|
||||
// currencyCode: estimate.currencyCode,
|
||||
// });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the sale estimate attachments.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns
|
||||
*/
|
||||
protected attachments = (estimate: SaleEstimate) => {
|
||||
// return this.item(estimate.attachments, new AttachmentTransformer());
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// import { ERRORS } from '../constants';
|
||||
// import { OnEvent } from '@nestjs/event-emitter';
|
||||
// import { Injectable } from '@nestjs/common';
|
||||
// import { DeliverSaleEstimateService } from '../commands/DeliverSaleEstimate.service';
|
||||
// import { events } from '@/common/events/events';
|
||||
// import { ISaleEstimateMailPresendEvent } from '../types/SaleEstimates.types';
|
||||
// import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
|
||||
// @Injectable()
|
||||
// export class SaleEstimateMarkApprovedOnMailSentSubscriber {
|
||||
// constructor(
|
||||
// private readonly deliverEstimateService: DeliverSaleEstimateService,
|
||||
// ) {}
|
||||
|
||||
// /**
|
||||
// * Marks the given estimate approved on submitting mail.
|
||||
// * @param {ISaleEstimateMailPresendEvent}
|
||||
// */
|
||||
// @OnEvent(events.saleEstimate.onPreMailSend)
|
||||
// public async markEstimateApproved({
|
||||
// saleEstimateId,
|
||||
// }: ISaleEstimateMailPresendEvent) {
|
||||
// try {
|
||||
// await this.deliverEstimateService.deliverSaleEstimate(saleEstimateId);
|
||||
// } catch (error) {
|
||||
// if (
|
||||
// error instanceof ServiceError &&
|
||||
// error.errorType === ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED
|
||||
// ) {
|
||||
// } else {
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Knex } from 'knex';
|
||||
// import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||
// import { AttachmentLinkDTO } from './Attachments';
|
||||
import { SaleEstimate } from '../models/SaleEstimate';
|
||||
import { IItemEntryDTO } from '@/modules/TransactionItemEntry/ItemEntry.types';
|
||||
import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types';
|
||||
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { CommonMailOptionsDTO } from '@/modules/MailNotification/MailNotification.types';
|
||||
import { CommonMailOptions } from '@/modules/MailNotification/MailNotification.types';
|
||||
import { EditSaleEstimateDto } from '../dtos/SaleEstimate.dto';
|
||||
|
||||
export const SendSaleEstimateMailQueue = 'SendSaleEstimateMailProcessor';
|
||||
export const SendSaleEstimateMailJob = 'SendSaleEstimateMailProcess';
|
||||
|
||||
export interface ISaleEstimateDTO {
|
||||
customerId: number;
|
||||
exchangeRate?: number;
|
||||
estimateDate?: Date;
|
||||
reference?: string;
|
||||
estimateNumber?: string;
|
||||
entries: IItemEntryDTO[];
|
||||
note: string;
|
||||
termsConditions: string;
|
||||
sendToEmail: string;
|
||||
delivered: boolean;
|
||||
|
||||
branchId?: number;
|
||||
warehouseId?: number;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
}
|
||||
|
||||
export interface ISalesEstimatesFilter extends IDynamicListFilter {
|
||||
stringifiedFilterRoles?: string;
|
||||
filterQuery?: (q: any) => void;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateCreatedPayload {
|
||||
saleEstimate: SaleEstimate;
|
||||
saleEstimateId: number;
|
||||
saleEstimateDTO: ISaleEstimateDTO;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateCreatingPayload {
|
||||
estimateDTO: ISaleEstimateDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateEditedPayload {
|
||||
estimateId: number;
|
||||
saleEstimate: SaleEstimate;
|
||||
oldSaleEstimate: SaleEstimate;
|
||||
estimateDTO: ISaleEstimateDTO;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateEditingPayload {
|
||||
oldSaleEstimate: SaleEstimate;
|
||||
estimateDTO: EditSaleEstimateDto;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateDeletedPayload {
|
||||
saleEstimateId: number;
|
||||
oldSaleEstimate: SaleEstimate;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateDeletingPayload {
|
||||
oldSaleEstimate: SaleEstimate;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateEventDeliveredPayload {
|
||||
saleEstimate: SaleEstimate;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateEventDeliveringPayload {
|
||||
oldSaleEstimate: SaleEstimate;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export enum SaleEstimateAction {
|
||||
Create = 'Create',
|
||||
Edit = 'Edit',
|
||||
Delete = 'Delete',
|
||||
View = 'View',
|
||||
NotifyBySms = 'NotifyBySms',
|
||||
}
|
||||
|
||||
export interface ISaleEstimateApprovingEvent {
|
||||
oldSaleEstimate: SaleEstimate;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateApprovedEvent {
|
||||
oldSaleEstimate: SaleEstimate;
|
||||
saleEstimate: SaleEstimate;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface SaleEstimateMailOptions extends CommonMailOptions {
|
||||
attachEstimate?: boolean;
|
||||
}
|
||||
|
||||
export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO {
|
||||
attachEstimate?: boolean;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateMailPresendEvent {
|
||||
saleEstimateId: number;
|
||||
messageOptions: SaleEstimateMailOptionsDTO;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
|
||||
export interface ISendSaleEstimateMailProcessData {
|
||||
saleEstimateId: number;
|
||||
messageOptions: SaleEstimateMailOptionsDTO;
|
||||
}
|
||||
34
packages/server/src/modules/SaleEstimates/utils.ts
Normal file
34
packages/server/src/modules/SaleEstimates/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { contactAddressTextFormat } from '@/utils/address-text-format';
|
||||
import { EstimatePdfBrandingAttributes } from './constants';
|
||||
|
||||
export const transformEstimateToPdfTemplate = (
|
||||
estimate
|
||||
): Partial<EstimatePdfBrandingAttributes> => {
|
||||
return {
|
||||
expirationDate: estimate.formattedExpirationDate,
|
||||
estimateNumebr: estimate.estimateNumber,
|
||||
estimateDate: estimate.formattedEstimateDate,
|
||||
lines: estimate.entries.map((entry) => ({
|
||||
item: entry.item.name,
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
total: estimate.formattedSubtotal,
|
||||
subtotal: estimate.formattedSubtotal,
|
||||
customerNote: estimate.note,
|
||||
termsConditions: estimate.termsConditions,
|
||||
customerAddress: contactAddressTextFormat(estimate.customer),
|
||||
};
|
||||
};
|
||||
|
||||
export const transformEstimateToMailDataArgs = (estimate: any) => {
|
||||
return {
|
||||
'Customer Name': estimate.customer.displayName,
|
||||
'Estimate Number': estimate.estimateNumber,
|
||||
'Estimate Date': estimate.formattedEstimateDate,
|
||||
'Estimate Amount': estimate.formattedAmount,
|
||||
'Estimate Expiration Date': estimate.formattedExpirationDate,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user