feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,177 @@
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service';
import { GetSaleReceiptState } from './queries/GetSaleReceiptState.service';
import { SaleReceiptsPdfService } from './queries/SaleReceiptsPdf.service';
import { CloseSaleReceipt } from './commands/CloseSaleReceipt.service';
import { DeleteSaleReceipt } from './commands/DeleteSaleReceipt.service';
import { GetSaleReceipt } from './queries/GetSaleReceipt.service';
import { EditSaleReceipt } from './commands/EditSaleReceipt.service';
import {
ISaleReceiptState,
ISalesReceiptsFilter,
SaleReceiptMailOpts,
SaleReceiptMailOptsDTO,
} from './types/SaleReceipts.types';
import { GetSaleReceiptsService } from './queries/GetSaleReceipts.service';
import { SaleReceipt } from './models/SaleReceipt';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { SaleReceiptMailNotification } from './commands/SaleReceiptMailNotification';
import { CreateSaleReceiptDto, EditSaleReceiptDto } from './dtos/SaleReceipt.dto';
@Injectable()
export class SaleReceiptApplication {
constructor(
private createSaleReceiptService: CreateSaleReceipt,
private editSaleReceiptService: EditSaleReceipt,
private getSaleReceiptService: GetSaleReceipt,
private deleteSaleReceiptService: DeleteSaleReceipt,
private getSaleReceiptsService: GetSaleReceiptsService,
private closeSaleReceiptService: CloseSaleReceipt,
private getSaleReceiptPdfService: SaleReceiptsPdfService,
private getSaleReceiptStateService: GetSaleReceiptState,
private saleReceiptNotifyByMailService: SaleReceiptMailNotification,
) {}
/**
* Creates a new sale receipt with associated entries.
* @param {ISaleReceiptDTO} saleReceiptDTO
* @returns {Promise<ISaleReceipt>}
*/
public async createSaleReceipt(
saleReceiptDTO: CreateSaleReceiptDto,
trx?: Knex.Transaction,
) {
return this.createSaleReceiptService.createSaleReceipt(saleReceiptDTO, trx);
}
/**
* Edit details sale receipt with associated entries.
* @param {number} tenantId
* @param {number} saleReceiptId
* @param {} saleReceiptDTO
* @returns
*/
public async editSaleReceipt(
saleReceiptId: number,
saleReceiptDTO: EditSaleReceiptDto,
) {
return this.editSaleReceiptService.editSaleReceipt(
saleReceiptId,
saleReceiptDTO,
);
}
/**
* Retrieve sale receipt with associated entries.
* @param {number} saleReceiptId - Sale receipt identifier.
* @returns {Promise<ISaleReceipt>}
*/
public async getSaleReceipt(saleReceiptId: number) {
return this.getSaleReceiptService.getSaleReceipt(saleReceiptId);
}
/**
* Deletes the sale receipt with associated entries.
* @param {number} saleReceiptId - Sale receipt identifier.
* @returns {Promise<void>}
*/
public async deleteSaleReceipt(saleReceiptId: number) {
return this.deleteSaleReceiptService.deleteSaleReceipt(saleReceiptId);
}
/**
* Retrieve sales receipts paginated and filterable list.
* @param {ISalesReceiptsFilter} filterDTO
* @returns
*/
public async getSaleReceipts(filterDTO: ISalesReceiptsFilter): Promise<{
data: SaleReceipt[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
return this.getSaleReceiptsService.getSaleReceipts(filterDTO);
}
/**
* Closes the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns {Promise<void>}
*/
public async closeSaleReceipt(saleReceiptId: number) {
return this.closeSaleReceiptService.closeSaleReceipt(saleReceiptId);
}
/**
* Retrieves the given sale receipt pdf.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
public getSaleReceiptPdf(tenantId: number, saleReceiptId: number) {
return this.getSaleReceiptPdfService.saleReceiptPdf(saleReceiptId);
}
/**
* Notify receipt customer by SMS of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
// public saleReceiptNotifyBySms(tenantId: number, saleReceiptId: number) {
// return this.saleReceiptNotifyBySmsService.notifyBySms(
// tenantId,
// saleReceiptId,
// );
// }
/**
* Retrieves sms details of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
// public getSaleReceiptSmsDetails(tenantId: number, saleReceiptId: number) {
// return this.saleReceiptNotifyBySmsService.smsDetails(
// tenantId,
// saleReceiptId,
// );
// }
/**
* Sends the receipt mail of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @param {SaleReceiptMailOptsDTO} messageOpts
* @returns {Promise<void>}
*/
public sendSaleReceiptMail(
saleReceiptId: number,
messageOpts: SaleReceiptMailOptsDTO,
): Promise<void> {
return this.saleReceiptNotifyByMailService.triggerMail(
saleReceiptId,
messageOpts,
);
}
/**
* Retrieves the default mail options of the given sale receipt.
* @param {number} saleReceiptId - Sale receipt identifier.
* @returns {Promise<SaleReceiptMailOpts>}
*/
public getSaleReceiptMail(
saleReceiptId: number,
): Promise<SaleReceiptMailOpts> {
return this.saleReceiptNotifyByMailService.getMailOptions(saleReceiptId);
}
/**
* Retrieves the current state of the sale receipt.
* @returns {Promise<ISaleReceiptState>} - A promise resolving to the sale receipt state.
*/
public getSaleReceiptState(): Promise<ISaleReceiptState> {
return this.getSaleReceiptStateService.getSaleReceiptState();
}
}

View File

@@ -0,0 +1,124 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
ParseIntPipe,
Post,
Put,
} from '@nestjs/common';
import { SaleReceiptApplication } from './SaleReceiptApplication.service';
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import {
CreateSaleReceiptDto,
EditSaleReceiptDto,
} from './dtos/SaleReceipt.dto';
@Controller('sale-receipts')
@ApiTags('sale-receipts')
export class SaleReceiptsController {
constructor(private saleReceiptApplication: SaleReceiptApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new sale receipt.' })
createSaleReceipt(@Body() saleReceiptDTO: CreateSaleReceiptDto) {
return this.saleReceiptApplication.createSaleReceipt(saleReceiptDTO);
}
@Put(':id/mail')
@HttpCode(200)
@ApiOperation({ summary: 'Send the sale receipt mail.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale receipt id',
})
sendSaleReceiptMail(@Param('id', ParseIntPipe) id: number) {
return this.saleReceiptApplication.getSaleReceiptMail(id);
}
@Get(':id/mail')
@HttpCode(200)
@ApiOperation({ summary: 'Retrieves the sale receipt mail.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale receipt id',
})
getSaleReceiptMail(@Param('id', ParseIntPipe) id: number) {
return this.saleReceiptApplication.getSaleReceiptMail(id);
}
@Put(':id')
@ApiOperation({ summary: 'Edit the given sale receipt.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale receipt id',
})
editSaleReceipt(
@Param('id', ParseIntPipe) id: number,
@Body() saleReceiptDTO: EditSaleReceiptDto,
) {
return this.saleReceiptApplication.editSaleReceipt(id, saleReceiptDTO);
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the sale receipt details.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale receipt id',
})
getSaleReceipt(@Param('id', ParseIntPipe) id: number) {
return this.saleReceiptApplication.getSaleReceipt(id);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete the given sale receipt.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale receipt id',
})
deleteSaleReceipt(@Param('id', ParseIntPipe) id: number) {
return this.saleReceiptApplication.deleteSaleReceipt(id);
}
@Post(':id/close')
@ApiOperation({ summary: 'Close the given sale receipt.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale receipt id',
})
closeSaleReceipt(@Param('id', ParseIntPipe) id: number) {
return this.saleReceiptApplication.closeSaleReceipt(id);
}
@Get(':id/pdf')
@ApiOperation({ summary: 'Retrieves the sale receipt PDF.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale receipt id',
})
getSaleReceiptPdf(@Param('id', ParseIntPipe) id: number) {
return this.saleReceiptApplication.getSaleReceiptPdf(0, id);
}
@Get('state')
@ApiOperation({ summary: 'Retrieves the sale receipt state.' })
getSaleReceiptState() {
return this.saleReceiptApplication.getSaleReceiptState();
}
}

View File

@@ -0,0 +1,80 @@
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { SaleReceiptApplication } from './SaleReceiptApplication.service';
import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service';
import { EditSaleReceipt } from './commands/EditSaleReceipt.service';
import { GetSaleReceipt } from './queries/GetSaleReceipt.service';
import { DeleteSaleReceipt } from './commands/DeleteSaleReceipt.service';
import { CloseSaleReceipt } from './commands/CloseSaleReceipt.service';
import { SaleReceiptsPdfService } from './queries/SaleReceiptsPdf.service';
import { GetSaleReceiptState } from './queries/GetSaleReceiptState.service';
import { ItemsModule } from '../Items/items.module';
import { SaleReceiptDTOTransformer } from './commands/SaleReceiptDTOTransformer.service';
import { SaleReceiptValidators } from './commands/SaleReceiptValidators.service';
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
import { TemplateInjectableModule } from '../TemplateInjectable/TemplateInjectable.module';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { SaleReceiptBrandingTemplate } from './queries/SaleReceiptBrandingTemplate.service';
import { BranchesModule } from '../Branches/Branches.module';
import { WarehousesModule } from '../Warehouses/Warehouses.module';
import { SaleReceiptIncrement } from './commands/SaleReceiptIncrement.service';
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
import { SaleReceiptsController } from './SaleReceipts.controller';
import { SaleReceiptGLEntriesSubscriber } from './subscribers/SaleReceiptGLEntriesSubscriber';
import { SaleReceiptGLEntries } from './ledger/SaleReceiptGLEntries';
import { LedgerModule } from '../Ledger/Ledger.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { SaleReceiptInventoryTransactionsSubscriber } from './inventory/SaleReceiptWriteInventoryTransactions';
import { GetSaleReceiptsService } from './queries/GetSaleReceipts.service';
import { SaleReceiptMailNotification } from './commands/SaleReceiptMailNotification';
import { SaleReceiptInventoryTransactions } from './inventory/SaleReceiptInventoryTransactions';
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { MailNotificationModule } from '../MailNotification/MailNotification.module';
import { SendSaleReceiptMailProcess } from './processes/SendSaleReceiptMail.process';
import { MailModule } from '../Mail/Mail.module';
import { SendSaleReceiptMailQueue } from './constants';
@Module({
controllers: [SaleReceiptsController],
imports: [
ItemsModule,
ChromiumlyTenancyModule,
TemplateInjectableModule,
BranchesModule,
WarehousesModule,
PdfTemplatesModule,
AutoIncrementOrdersModule,
LedgerModule,
AccountsModule,
InventoryCostModule,
DynamicListModule,
MailModule,
MailNotificationModule,
BullModule.registerQueue({ name: SendSaleReceiptMailQueue }),
],
providers: [
TenancyContext,
SaleReceiptValidators,
SaleReceiptApplication,
CreateSaleReceipt,
EditSaleReceipt,
GetSaleReceipt,
DeleteSaleReceipt,
CloseSaleReceipt,
SaleReceiptsPdfService,
GetSaleReceiptState,
SaleReceiptDTOTransformer,
SaleReceiptBrandingTemplate,
SaleReceiptIncrement,
SaleReceiptGLEntries,
SaleReceiptGLEntriesSubscriber,
GetSaleReceiptsService,
SaleReceiptMailNotification,
SaleReceiptInventoryTransactions,
SaleReceiptInventoryTransactionsSubscriber,
SendSaleReceiptMailProcess,
],
})
export class SaleReceiptsModule {}

View File

@@ -0,0 +1,71 @@
import { Inject, Injectable } from '@nestjs/common';
import * as moment from 'moment';
import { Knex } from 'knex';
import {
ISaleReceiptEventClosedPayload,
ISaleReceiptEventClosingPayload,
} from '../types/SaleReceipts.types';
import { SaleReceiptValidators } from './SaleReceiptValidators.service';
import { SaleReceipt } from '../models/SaleReceipt';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class CloseSaleReceipt {
/**
* @param {EventEmitter2} eventEmitter - Event emitter.
* @param {UnitOfWork} uow - Unit of work.
* @param {SaleReceiptValidators} validators - Sale receipt validators.
* @param {TenantModelProxy<typeof SaleReceipt>} saleReceiptModel - Sale receipt model.
*/
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly validators: SaleReceiptValidators,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
) {}
/**
* Mark the given sale receipt as closed.
* @param {number} saleReceiptId - Sale receipt identifier.
* @return {Promise<void>}
*/
public async closeSaleReceipt(saleReceiptId: number): Promise<void> {
// Retrieve sale receipt or throw not found service error.
const oldSaleReceipt = await this.saleReceiptModel()
.query()
.findById(saleReceiptId)
.withGraphFetched('entries')
.throwIfNotFound();
// Throw service error if the sale receipt already closed.
this.validators.validateReceiptNotClosed(oldSaleReceipt);
// Updates the sale receipt transaction under unit-of-work environment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptClosing` event.
await this.eventEmitter.emitAsync(events.saleReceipt.onClosing, {
oldSaleReceipt,
trx,
} as ISaleReceiptEventClosingPayload);
// Mark the sale receipt as closed on the storage.
const saleReceipt = await this.saleReceiptModel()
.query(trx)
.patchAndFetchById(saleReceiptId, {
closedAt: moment().toMySqlDateTime(),
});
// Triggers `onSaleReceiptClosed` event.
await this.eventEmitter.emitAsync(events.saleReceipt.onClosed, {
saleReceiptId,
saleReceipt,
trx,
} as ISaleReceiptEventClosedPayload);
});
}
}

View File

@@ -0,0 +1,108 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
ISaleReceiptCreatedPayload,
ISaleReceiptCreatingPayload,
} from '../types/SaleReceipts.types';
import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer.service';
import { SaleReceiptValidators } from './SaleReceiptValidators.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { SaleReceipt } from '../models/SaleReceipt';
import { Customer } from '@/modules/Customers/models/Customer';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { CreateSaleReceiptDto } from '../dtos/SaleReceipt.dto';
@Injectable()
export class CreateSaleReceipt {
/**
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {UnitOfWork} uow - Unit of work.
* @param {SaleReceiptDTOTransformer} transformer - Sale receipt DTO transformer.
* @param {SaleReceiptValidators} validators - Sale receipt validators.
* @param {typeof SaleReceipt} saleReceiptModel - Sale receipt model.
* @param {typeof Customer} customerModel - Customer model.
*/
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly transformer: SaleReceiptDTOTransformer,
private readonly validators: SaleReceiptValidators,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
@Inject(Customer.name)
private readonly customerModel: TenantModelProxy<typeof Customer>,
) {}
/**
* Creates a new sale receipt with associated entries.
* @async
* @param {ISaleReceiptDTO} saleReceiptDTO
* @return {Promise<ISaleReceipt>}
*/
public async createSaleReceipt(
saleReceiptDTO: CreateSaleReceiptDto,
trx?: Knex.Transaction,
): Promise<SaleReceipt> {
// Retrieves the payment customer model.
const paymentCustomer = await this.customerModel()
.query()
.findById(saleReceiptDTO.customerId)
.throwIfNotFound();
// Transform sale receipt DTO to model.
const saleReceiptObj = await this.transformer.transformDTOToModel(
saleReceiptDTO,
paymentCustomer,
);
// Validate receipt deposit account existence and type.
await this.validators.validateReceiptDepositAccountExistence(
saleReceiptDTO.depositAccountId,
);
// Validate items IDs existence on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
saleReceiptDTO.entries,
);
// Validate the sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
saleReceiptDTO.entries,
);
// Validate sale receipt number uniqueness.
if (saleReceiptDTO.receiptNumber) {
await this.validators.validateReceiptNumberUnique(
saleReceiptDTO.receiptNumber,
);
}
// Creates a sale receipt transaction and associated transactions under UOW env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptCreating` event.
await this.eventEmitter.emitAsync(events.saleReceipt.onCreating, {
saleReceiptDTO,
trx,
} as ISaleReceiptCreatingPayload);
// Inserts the sale receipt graph to the storage.
const saleReceipt = await this.saleReceiptModel()
.query()
.upsertGraph({
...saleReceiptObj,
});
// Triggers `onSaleReceiptCreated` event.
await this.eventEmitter.emitAsync(events.saleReceipt.onCreated, {
saleReceipt,
saleReceiptId: saleReceipt.id,
saleReceiptDTO,
trx,
} as ISaleReceiptCreatedPayload);
return saleReceipt;
}, trx);
}
}

View File

@@ -0,0 +1,70 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
ISaleReceiptDeletingPayload,
ISaleReceiptEventDeletedPayload,
} from '../types/SaleReceipts.types';
import { SaleReceiptValidators } from './SaleReceiptValidators.service';
import { SaleReceipt } from '../models/SaleReceipt';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DeleteSaleReceipt {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly validators: SaleReceiptValidators,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
@Inject(ItemEntry.name)
private readonly itemEntryModel: TenantModelProxy<typeof ItemEntry>,
) {}
/**
* Deletes the sale receipt with associated entries.
* @param {Integer} saleReceiptId - Sale receipt identifier.
* @return {void}
*/
public async deleteSaleReceipt(saleReceiptId: number) {
const oldSaleReceipt = await this.saleReceiptModel()
.query()
.findById(saleReceiptId)
.withGraphFetched('entries');
// Validates the sale receipt existence.
this.validators.validateReceiptExistence(oldSaleReceipt);
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptsDeleting` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onDeleting, {
trx,
oldSaleReceipt,
} as ISaleReceiptDeletingPayload);
await this.itemEntryModel()
.query(trx)
.where('reference_id', saleReceiptId)
.where('reference_type', 'SaleReceipt')
.delete();
// Delete the sale receipt transaction.
await this.saleReceiptModel()
.query(trx)
.where('id', saleReceiptId)
.delete();
// Triggers `onSaleReceiptsDeleted` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onDeleted, {
saleReceiptId,
oldSaleReceipt,
trx,
} as ISaleReceiptEventDeletedPayload);
});
}
}

View File

@@ -0,0 +1,110 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
ISaleReceiptEditedPayload,
ISaleReceiptEditingPayload,
} from '../types/SaleReceipts.types';
import { SaleReceiptValidators } from './SaleReceiptValidators.service';
import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer.service';
import { SaleReceipt } from '../models/SaleReceipt';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { Contact } from '@/modules/Contacts/models/Contact';
import { events } from '@/common/events/events';
import { Customer } from '@/modules/Customers/models/Customer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { EditSaleReceiptDto } from '../dtos/SaleReceipt.dto';
@Injectable()
export class EditSaleReceipt {
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly validators: SaleReceiptValidators,
private readonly dtoTransformer: SaleReceiptDTOTransformer,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
@Inject(Customer.name)
private readonly customerModel: TenantModelProxy<typeof Customer>,
) {}
/**
* Edit details sale receipt with associated entries.
* @param {Integer} saleReceiptId
* @param {ISaleReceipt} saleReceipt
* @return {void}
*/
public async editSaleReceipt(
saleReceiptId: number,
saleReceiptDTO: EditSaleReceiptDto,
) {
// Retrieve sale receipt or throw not found service error.
const oldSaleReceipt = await this.saleReceiptModel()
.query()
.findById(saleReceiptId)
.withGraphFetched('entries')
.throwIfNotFound();
// Retrieves the payment customer model.
const paymentCustomer = await this.customerModel()
.query()
.findById(saleReceiptDTO.customerId)
.throwIfNotFound();
// Transform sale receipt DTO to model.
const saleReceiptObj = await this.dtoTransformer.transformDTOToModel(
saleReceiptDTO,
paymentCustomer,
oldSaleReceipt,
);
// Validate receipt deposit account existance and type.
await this.validators.validateReceiptDepositAccountExistence(
saleReceiptDTO.depositAccountId,
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
saleReceiptDTO.entries,
);
// Validate the sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
saleReceiptDTO.entries,
);
// Validate sale receipt number uniuqiness.
if (saleReceiptDTO.receiptNumber) {
await this.validators.validateReceiptNumberUnique(
saleReceiptDTO.receiptNumber,
saleReceiptId,
);
}
// Edits the sale receipt tranasctions with associated transactions under UOW env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptsEditing` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onEditing, {
oldSaleReceipt,
saleReceiptDTO,
trx,
} as ISaleReceiptEditingPayload);
// Upsert the receipt graph to the storage.
const saleReceipt = await this.saleReceiptModel()
.query(trx)
.upsertGraphAndFetch({
id: saleReceiptId,
...saleReceiptObj,
});
// Triggers `onSaleReceiptEdited` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onEdited, {
oldSaleReceipt,
saleReceipt,
saleReceiptDTO,
trx,
} as ISaleReceiptEditedPayload);
return saleReceipt;
});
}
}

View File

@@ -0,0 +1,148 @@
// import { Service, Inject } from 'typedi';
// import * as R from 'ramda';
// import { Knex } from 'knex';
// import { AccountNormal, IInventoryLotCost, ILedgerEntry } from '@/interfaces';
// import { increment } from 'utils';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import Ledger from '@/services/Accounting/Ledger';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import { groupInventoryTransactionsByTypeId } from '../../Inventory/utils';
// @Service()
// export class SaleReceiptCostGLEntries {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private ledgerStorage: LedgerStorageService;
// /**
// * Writes journal entries from sales invoices.
// * @param {number} tenantId - The tenant id.
// * @param {Date} startingDate - Starting date.
// * @param {boolean} override
// */
// public writeInventoryCostJournalEntries = async (
// tenantId: number,
// startingDate: Date,
// trx?: Knex.Transaction
// ): Promise<void> => {
// const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
// const inventoryCostLotTrans = await InventoryCostLotTracker.query()
// .where('direction', 'OUT')
// .where('transaction_type', 'SaleReceipt')
// .where('cost', '>', 0)
// .modify('filterDateRange', startingDate)
// .orderBy('date', 'ASC')
// .withGraphFetched('receipt')
// .withGraphFetched('item');
// const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
// // Commit the ledger to the storage.
// await this.ledgerStorage.commit(tenantId, ledger, trx);
// };
// /**
// * Retrieves the inventory cost lots ledger.
// * @param {} inventoryCostLots
// * @returns {Ledger}
// */
// private getInventoryCostLotsLedger = (
// inventoryCostLots: IInventoryLotCost[]
// ) => {
// // Groups the inventory cost lots transactions.
// const inventoryTransactions =
// groupInventoryTransactionsByTypeId(inventoryCostLots);
// //
// const entries = inventoryTransactions
// .map(this.getSaleInvoiceCostGLEntries)
// .flat();
// return new Ledger(entries);
// };
// /**
// *
// * @param {IInventoryLotCost} inventoryCostLot
// * @returns {}
// */
// private getInvoiceCostGLCommonEntry = (
// inventoryCostLot: IInventoryLotCost
// ) => {
// return {
// currencyCode: inventoryCostLot.receipt.currencyCode,
// exchangeRate: inventoryCostLot.receipt.exchangeRate,
// transactionType: inventoryCostLot.transactionType,
// transactionId: inventoryCostLot.transactionId,
// date: inventoryCostLot.date,
// indexGroup: 20,
// costable: true,
// createdAt: inventoryCostLot.createdAt,
// debit: 0,
// credit: 0,
// branchId: inventoryCostLot.receipt.branchId,
// };
// };
// /**
// * Retrieves the inventory cost GL entry.
// * @param {IInventoryLotCost} inventoryLotCost
// * @returns {ILedgerEntry[]}
// */
// private getInventoryCostGLEntry = R.curry(
// (
// getIndexIncrement,
// inventoryCostLot: IInventoryLotCost
// ): ILedgerEntry[] => {
// const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot);
// const costAccountId =
// inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
// // XXX Debit - Cost account.
// const costEntry = {
// ...commonEntry,
// debit: inventoryCostLot.cost,
// accountId: costAccountId,
// accountNormal: AccountNormal.DEBIT,
// itemId: inventoryCostLot.itemId,
// index: getIndexIncrement(),
// };
// // XXX Credit - Inventory account.
// const inventoryEntry = {
// ...commonEntry,
// credit: inventoryCostLot.cost,
// accountId: inventoryCostLot.item.inventoryAccountId,
// accountNormal: AccountNormal.DEBIT,
// itemId: inventoryCostLot.itemId,
// index: getIndexIncrement(),
// };
// return [costEntry, inventoryEntry];
// }
// );
// /**
// * Writes journal entries for given sale invoice.
// * -------
// * - Cost of goods sold -> Debit -> YYYY
// * - Inventory assets -> Credit -> YYYY
// * --------
// * @param {ISaleInvoice} saleInvoice
// * @param {JournalPoster} journal
// */
// public getSaleInvoiceCostGLEntries = (
// inventoryCostLots: IInventoryLotCost[]
// ): ILedgerEntry[] => {
// const getIndexIncrement = increment(0);
// const getInventoryLotEntry =
// this.getInventoryCostGLEntry(getIndexIncrement);
// return inventoryCostLots.map(getInventoryLotEntry).flat();
// };
// }

View File

@@ -0,0 +1,114 @@
import { Inject, Injectable } from '@nestjs/common';
import * as R from 'ramda';
import { sumBy, omit } from 'lodash';
import * as composeAsync from 'async/compose';
import * as moment from 'moment';
import { SaleReceiptIncrement } from './SaleReceiptIncrement.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { SaleReceiptValidators } from './SaleReceiptValidators.service';
import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { formatDateFields } from '@/utils/format-date-fields';
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
import { SaleReceipt } from '../models/SaleReceipt';
import { Customer } from '@/modules/Customers/models/Customer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import {
CreateSaleReceiptDto,
EditSaleReceiptDto,
} from '../dtos/SaleReceipt.dto';
@Injectable()
export class SaleReceiptDTOTransformer {
/**
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
* @param {BranchTransactionDTOTransformer} branchDTOTransform - Branch transaction DTO transformer.
* @param {WarehouseTransactionDTOTransform} warehouseDTOTransform - Warehouse transaction DTO transformer.
* @param {SaleReceiptValidators} validators - Sale receipt validators.
* @param {SaleReceiptIncrement} receiptIncrement - Sale receipt increment.
* @param {BrandingTemplateDTOTransformer} brandingTemplatesTransformer - Branding template DTO transformer.
* @param {typeof ItemEntry} itemEntryModel - Item entry model.
*/
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly branchDTOTransform: BranchTransactionDTOTransformer,
private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform,
private readonly validators: SaleReceiptValidators,
private readonly receiptIncrement: SaleReceiptIncrement,
private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
@Inject(ItemEntry.name)
private readonly itemEntryModel: TenantModelProxy<typeof ItemEntry>,
) {}
/**
* Transform create DTO object to model object.
* @param {ISaleReceiptDTO} saleReceiptDTO -
* @param {ISaleReceipt} oldSaleReceipt -
* @returns {ISaleReceipt}
*/
async transformDTOToModel(
saleReceiptDTO: CreateSaleReceiptDto | EditSaleReceiptDto,
paymentCustomer: Customer,
oldSaleReceipt?: SaleReceipt,
): Promise<SaleReceipt> {
const amount = sumBy(saleReceiptDTO.entries, (e) =>
this.itemEntryModel().calcAmount(e),
);
// Retrieve the next invoice number.
const autoNextNumber = await this.receiptIncrement.getNextReceiptNumber();
// Retrieve the receipt number.
const receiptNumber =
saleReceiptDTO.receiptNumber ||
oldSaleReceipt?.receiptNumber ||
autoNextNumber;
// Validate receipt number require.
this.validators.validateReceiptNoRequire(receiptNumber);
const initialEntries = saleReceiptDTO.entries.map((entry) => ({
reference_type: 'SaleReceipt',
...entry,
}));
const asyncEntries = await composeAsync(
// Sets default cost and sell account to receipt items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts,
)(initialEntries);
const entries = R.compose(
// Associate the default index for each item entry.
assocItemEntriesDefaultIndex,
)(asyncEntries);
const initialDTO = {
amount,
...formatDateFields(
omit(saleReceiptDTO, ['closed', 'entries', 'attachments']),
['receiptDate'],
),
currencyCode: paymentCustomer.currencyCode,
exchangeRate: saleReceiptDTO.exchangeRate || 1,
receiptNumber,
// Avoid rewrite the deliver date in edit mode when already published.
...(saleReceiptDTO.closed &&
!oldSaleReceipt?.closedAt && {
closedAt: moment().toMySqlDateTime(),
}),
entries,
};
const asyncDto = await composeAsync(
this.branchDTOTransform.transformDTO<SaleReceipt>,
this.warehouseDTOTransform.transformDTO<SaleReceipt>,
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'SaleReceipt',
),
)(initialDTO);
return asyncDto;
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
@Injectable()
export class SaleReceiptIncrement {
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
) {}
/**
* Retrieve the next unique receipt number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
public getNextReceiptNumber(): Promise<string> {
return this.autoIncrementOrdersService.getNextTransactionNumber(
'sales_receipts',
);
}
/**
* Increment the receipt next number.
* @param {number} tenantId -
*/
public incrementNextReceiptNumber() {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'sales_receipts',
);
}
}

View File

@@ -0,0 +1,72 @@
// import { Knex } from 'knex';
// import { Inject, Service } from 'typedi';
// import { ISaleReceipt } from '@/interfaces';
// import InventoryService from '@/services/Inventory/Inventory';
// import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
// @Service()
// export class SaleReceiptInventoryTransactions {
// @Inject()
// private inventoryService: InventoryService;
// @Inject()
// private itemsEntriesService: ItemsEntriesService;
// /**
// * Records the inventory transactions from the given bill input.
// * @param {Bill} bill - Bill model object.
// * @param {number} billId - Bill id.
// * @return {Promise<void>}
// */
// public async recordInventoryTransactions(
// tenantId: number,
// saleReceipt: ISaleReceipt,
// override?: boolean,
// trx?: Knex.Transaction
// ): Promise<void> {
// // Loads the inventory items entries of the given sale invoice.
// const inventoryEntries =
// await this.itemsEntriesService.filterInventoryEntries(
// tenantId,
// saleReceipt.entries
// );
// const transaction = {
// transactionId: saleReceipt.id,
// transactionType: 'SaleReceipt',
// transactionNumber: saleReceipt.receiptNumber,
// exchangeRate: saleReceipt.exchangeRate,
// date: saleReceipt.receiptDate,
// direction: 'OUT',
// entries: inventoryEntries,
// createdAt: saleReceipt.createdAt,
// warehouseId: saleReceipt.warehouseId,
// };
// return this.inventoryService.recordInventoryTransactionsFromItemsEntries(
// tenantId,
// transaction,
// override,
// trx
// );
// }
// /**
// * Reverts the inventory transactions of the given bill id.
// * @param {number} tenantId - Tenant id.
// * @param {number} billId - Bill id.
// * @return {Promise<void>}
// */
// public async revertInventoryTransactions(
// tenantId: number,
// receiptId: number,
// trx?: Knex.Transaction
// ) {
// return this.inventoryService.deleteInventoryTransactions(
// tenantId,
// receiptId,
// 'SaleReceipt',
// trx
// );
// }
// }

View File

@@ -0,0 +1,213 @@
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bullmq';
import {
DEFAULT_RECEIPT_MAIL_CONTENT,
DEFAULT_RECEIPT_MAIL_SUBJECT,
SendSaleReceiptMailJob,
SendSaleReceiptMailQueue,
} from '../constants';
import { mergeAndValidateMailOptions } from '@/modules/MailNotification/utils';
import { transformReceiptToMailDataArgs } from '../utils';
import { Inject, Injectable } from '@nestjs/common';
import { GetSaleReceipt } from '../queries/GetSaleReceipt.service';
import { SaleReceiptsPdfService } from '../queries/SaleReceiptsPdf.service';
import { ContactMailNotification } from '@/modules/MailNotification/ContactMailNotification';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import {
ISaleReceiptMailPresend,
SaleReceiptMailOpts,
SaleReceiptMailOptsDTO,
SaleReceiptSendMailPayload,
} from '../types/SaleReceipts.types';
import { SaleReceipt } from '../models/SaleReceipt';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { Mail } from '@/modules/Mail/Mail';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class SaleReceiptMailNotification {
/**
* @param {GetSaleReceipt} getSaleReceiptService - Get sale receipt service.
* @param {SaleReceiptsPdfService} receiptPdfService - Sale receipt pdf service.
* @param {ContactMailNotification} contactMailNotification - Contact mail notification service.
* @param {EventEmitter2} eventEmitter - Event emitter.
* @param {MailTransporter} mailTransporter - Mail transporter service.
*/
constructor(
private readonly getSaleReceiptService: GetSaleReceipt,
private readonly receiptPdfService: SaleReceiptsPdfService,
private readonly contactMailNotification: ContactMailNotification,
private readonly eventEmitter: EventEmitter2,
private readonly mailTransporter: MailTransporter,
private readonly tenancyContext: TenancyContext,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
@InjectQueue(SendSaleReceiptMailQueue)
private readonly sendSaleReceiptMailProcess: Queue,
) {}
/**
* Sends the receipt mail of the given sale receipt.
* @param {number} saleReceiptId - Sale receipt id.
* @param {SaleReceiptMailOptsDTO} messageDTO - Message DTOs.
*/
public async triggerMail(
saleReceiptId: number,
messageOptions: SaleReceiptMailOptsDTO,
) {
const tenant = await this.tenancyContext.getTenant();
const user = await this.tenancyContext.getSystemUser();
const organizationId = tenant.organizationId;
const userId = user.id;
const payload = {
saleReceiptId,
messageOpts: messageOptions,
userId,
organizationId,
} as SaleReceiptSendMailPayload;
await this.sendSaleReceiptMailProcess.add(SendSaleReceiptMailJob, {
...payload,
});
// Triggers the event `onSaleReceiptPreMailSend`.
await this.eventEmitter.emitAsync(events.saleReceipt.onPreMailSend, {
saleReceiptId,
messageOptions,
} as ISaleReceiptMailPresend);
}
/**
* Retrieves the mail options of the given sale receipt.
* @param {number} saleReceiptId - Sale receipt id.
* @returns {Promise<SaleReceiptMailOptsDTO>}
*/
public async getMailOptions(
saleReceiptId: number,
): Promise<SaleReceiptMailOpts> {
const saleReceipt = await this.saleReceiptModel()
.query()
.findById(saleReceiptId)
.throwIfNotFound();
const formatArgs = await this.textFormatterArgs(saleReceiptId);
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
saleReceipt.customerId,
);
return {
...mailOptions,
message: DEFAULT_RECEIPT_MAIL_CONTENT,
subject: DEFAULT_RECEIPT_MAIL_SUBJECT,
attachReceipt: true,
formatArgs,
};
}
/**
* Retrieves the formatted text of the given sale receipt.
* @param {number} receiptId - Sale receipt id.
* @param {string} text - The given text.
* @returns {Promise<string>}
*/
public textFormatterArgs = async (
receiptId: number,
): Promise<Record<string, string>> => {
const receipt = await this.getSaleReceiptService.getSaleReceipt(receiptId);
return transformReceiptToMailDataArgs(receipt);
};
/**
* Formats the mail options of the given sale receipt.
* @param {number} receiptId
* @param {SaleReceiptMailOpts} mailOptions
* @returns {Promise<SaleReceiptMailOpts>}
*/
public async formatEstimateMailOptions(
receiptId: number,
mailOptions: SaleReceiptMailOpts,
): Promise<SaleReceiptMailOpts> {
const formatterArgs = await this.textFormatterArgs(receiptId);
const formattedOptions =
(await this.contactMailNotification.formatMailOptions(
mailOptions,
formatterArgs,
)) as SaleReceiptMailOpts;
return formattedOptions;
}
/**
* Retrieves the formatted mail options of the given sale receipt.
* @param {number} saleReceiptId
* @param {SaleReceiptMailOptsDTO} messageOpts
* @returns {Promise<SaleReceiptMailOpts>}
*/
public getFormatMailOptions = async (
saleReceiptId: number,
messageOpts: SaleReceiptMailOptsDTO,
): Promise<SaleReceiptMailOpts> => {
const defaultMessageOptions = await this.getMailOptions(saleReceiptId);
// Merges message opts with default options.
const parsedMessageOpts = mergeAndValidateMailOptions(
defaultMessageOptions,
messageOpts,
) as SaleReceiptMailOpts;
// Formats the message options.
return this.formatEstimateMailOptions(saleReceiptId, parsedMessageOpts);
};
/**
* Triggers the mail notification of the given sale receipt.
* @param {number} saleReceiptId - Sale receipt id.
* @param {SaleReceiptMailOpts} messageDTO - message options.
* @returns {Promise<void>}
*/
public async sendMail(
saleReceiptId: number,
messageOpts: SaleReceiptMailOptsDTO,
) {
// Formats the message options.
const formattedMessageOptions = await this.getFormatMailOptions(
saleReceiptId,
messageOpts,
);
const mail = new Mail()
.setSubject(formattedMessageOptions.subject)
.setTo(formattedMessageOptions.to)
.setCC(formattedMessageOptions.cc)
.setBCC(formattedMessageOptions.bcc)
.setContent(formattedMessageOptions.message);
// Attaches the receipt pdf document.
if (formattedMessageOptions.attachReceipt) {
// Retrieves document buffer of the receipt pdf document.
const [receiptPdfBuffer, filename] =
await this.receiptPdfService.saleReceiptPdf(saleReceiptId);
mail.setAttachments([
{ filename: `${filename}.pdf`, content: receiptPdfBuffer },
]);
}
const eventPayload = {
saleReceiptId,
messageOptions: {},
};
await this.eventEmitter.emitAsync(
events.saleReceipt.onMailSend,
eventPayload,
);
await this.mailTransporter.send(mail);
await this.eventEmitter.emitAsync(
events.saleReceipt.onMailSent,
eventPayload,
);
}
}

View File

@@ -0,0 +1,36 @@
// import Container, { Service } from 'typedi';
// import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
// @Service()
// export class SaleReceiptMailNotificationJob {
// /**
// * Constructor method.
// */
// constructor(agenda) {
// agenda.define(
// 'sale-receipt-mail-send',
// { priority: 'high', concurrency: 2 },
// this.handler
// );
// }
// /**
// * Triggers sending invoice mail.
// */
// private handler = async (job, done: Function) => {
// const { tenantId, saleReceiptId, messageOpts } = job.attrs.data;
// const receiveMailNotification = Container.get(SaleReceiptMailNotification);
// try {
// await receiveMailNotification.sendMail(
// tenantId,
// saleReceiptId,
// messageOpts
// );
// done();
// } catch (error) {
// console.log(error);
// done(error);
// }
// };
// }

View File

@@ -0,0 +1,206 @@
// import { Service, Inject } from 'typedi';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import events from '@/subscribers/events';
// import {
// ISaleReceiptSmsDetails,
// ISaleReceipt,
// SMS_NOTIFICATION_KEY,
// ICustomer,
// } from '@/interfaces';
// import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
// import { formatNumber, formatSmsMessage } from 'utils';
// import { TenantMetadata } from '@/system/models';
// import { ServiceError } from '@/exceptions';
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
// import SaleNotifyBySms from '../SaleNotifyBySms';
// import { ERRORS } from './constants';
// @Service()
// export class SaleReceiptNotifyBySms {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private eventPublisher: EventPublisher;
// @Inject()
// private smsNotificationsSettings: SmsNotificationsSettingsService;
// @Inject()
// private saleSmsNotification: SaleNotifyBySms;
// /**
// * Notify customer via sms about sale receipt.
// * @param {number} tenantId - Tenant id.
// * @param {number} saleReceiptId - Sale receipt id.
// */
// public async notifyBySms(tenantId: number, saleReceiptId: number) {
// const { SaleReceipt } = this.tenancy.models(tenantId);
// // Retrieve the sale receipt or throw not found service error.
// const saleReceipt = await SaleReceipt.query()
// .findById(saleReceiptId)
// .withGraphFetched('customer');
// // Validates the receipt receipt existance.
// this.validateSaleReceiptExistance(saleReceipt);
// // Validate the customer phone number.
// this.saleSmsNotification.validateCustomerPhoneNumber(
// saleReceipt.customer.personalPhone
// );
// // Triggers `onSaleReceiptNotifySms` event.
// await this.eventPublisher.emitAsync(events.saleReceipt.onNotifySms, {
// tenantId,
// saleReceipt,
// });
// // Sends the payment receive sms notification to the given customer.
// await this.sendSmsNotification(tenantId, saleReceipt);
// // Triggers `onSaleReceiptNotifiedSms` event.
// await this.eventPublisher.emitAsync(events.saleReceipt.onNotifiedSms, {
// tenantId,
// saleReceipt,
// });
// return saleReceipt;
// }
// /**
// * Sends SMS notification.
// * @param {ISaleReceipt} invoice
// * @param {ICustomer} customer
// * @returns
// */
// public sendSmsNotification = async (
// tenantId: number,
// saleReceipt: ISaleReceipt & { customer: ICustomer }
// ) => {
// const smsClient = this.tenancy.smsClient(tenantId);
// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
// // Retrieve formatted sms notification message of receipt details.
// const formattedSmsMessage = this.formattedReceiptDetailsMessage(
// tenantId,
// saleReceipt,
// tenantMetadata
// );
// const phoneNumber = saleReceipt.customer.personalPhone;
// // Run the send sms notification message job.
// return smsClient.sendMessageJob(phoneNumber, formattedSmsMessage);
// };
// /**
// * Notify via SMS message after receipt creation.
// * @param {number} tenantId
// * @param {number} receiptId
// * @returns {Promise<void>}
// */
// public notifyViaSmsAfterCreation = async (
// tenantId: number,
// receiptId: number
// ): Promise<void> => {
// const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
// tenantId,
// SMS_NOTIFICATION_KEY.SALE_RECEIPT_DETAILS
// );
// // Can't continue if the sms auto-notification is not enabled.
// if (!notification.isNotificationEnabled) return;
// await this.notifyBySms(tenantId, receiptId);
// };
// /**
// * Retrieve the formatted sms notification message of the given sale receipt.
// * @param {number} tenantId
// * @param {ISaleReceipt} saleReceipt
// * @param {TenantMetadata} tenantMetadata
// * @returns {string}
// */
// private formattedReceiptDetailsMessage = (
// tenantId: number,
// saleReceipt: ISaleReceipt & { customer: ICustomer },
// tenantMetadata: TenantMetadata
// ): string => {
// const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
// tenantId,
// SMS_NOTIFICATION_KEY.SALE_RECEIPT_DETAILS
// );
// return this.formatReceiptDetailsMessage(
// notification.smsMessage,
// saleReceipt,
// tenantMetadata
// );
// };
// /**
// * Formattes the receipt sms notification message.
// * @param {string} smsMessage
// * @param {ISaleReceipt} saleReceipt
// * @param {TenantMetadata} tenantMetadata
// * @returns {string}
// */
// private formatReceiptDetailsMessage = (
// smsMessage: string,
// saleReceipt: ISaleReceipt & { customer: ICustomer },
// tenantMetadata: TenantMetadata
// ): string => {
// // Format the receipt amount.
// const formattedAmount = formatNumber(saleReceipt.amount, {
// currencyCode: saleReceipt.currencyCode,
// });
// return formatSmsMessage(smsMessage, {
// ReceiptNumber: saleReceipt.receiptNumber,
// ReferenceNumber: saleReceipt.referenceNo,
// CustomerName: saleReceipt.customer.displayName,
// Amount: formattedAmount,
// CompanyName: tenantMetadata.name,
// });
// };
// /**
// * Retrieve the SMS details of the given invoice.
// * @param {number} tenantId -
// * @param {number} saleReceiptId - Sale receipt id.
// */
// public smsDetails = async (
// tenantId: number,
// saleReceiptId: number
// ): Promise<ISaleReceiptSmsDetails> => {
// const { SaleReceipt } = this.tenancy.models(tenantId);
// // Retrieve the sale receipt or throw not found service error.
// const saleReceipt = await SaleReceipt.query()
// .findById(saleReceiptId)
// .withGraphFetched('customer');
// // Validates the receipt receipt existance.
// this.validateSaleReceiptExistance(saleReceipt);
// // Current tenant metadata.
// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
// // Retrieve the sale receipt formatted sms notification message.
// const formattedSmsMessage = this.formattedReceiptDetailsMessage(
// tenantId,
// saleReceipt,
// tenantMetadata
// );
// return {
// customerName: saleReceipt.customer.displayName,
// customerPhoneNumber: saleReceipt.customer.personalPhone,
// smsMessage: formattedSmsMessage,
// };
// };
// /**
// * Validates the receipt receipt existance.
// * @param {ISaleReceipt|null} saleReceipt
// */
// private validateSaleReceiptExistance(saleReceipt: ISaleReceipt | null) {
// if (!saleReceipt) {
// throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
// }
// }
// }

View File

@@ -0,0 +1,106 @@
import { Inject, Injectable } from '@nestjs/common';
import { ERRORS } from '../constants';
import { SaleReceipt } from '../models/SaleReceipt';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ACCOUNT_PARENT_TYPE } from '@/constants/accounts';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class SaleReceiptValidators {
/**
* @param {TenantModelProxy<typeof SaleReceipt>} saleReceiptModel - Sale receipt model.
* @param {TenantModelProxy<typeof Account>} accountModel - Account model.
*/
constructor(
@Inject(SaleReceipt.name)
private saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
@Inject(Account.name)
private accountModel: TenantModelProxy<typeof Account>,
) {}
/**
* Validates the sale receipt existence.
* @param {SaleEstimate | undefined | null} estimate
*/
public validateReceiptExistence(receipt: SaleReceipt | undefined | null) {
if (!receipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
}
}
/**
* Validates the receipt not closed.
* @param {SaleReceipt} receipt
*/
public validateReceiptNotClosed(receipt: SaleReceipt) {
if (receipt.isClosed) {
throw new ServiceError(ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED);
}
}
/**
* Validate whether sale receipt deposit account exists on the storage.
* @param {number} accountId - Account id.
*/
public async validateReceiptDepositAccountExistence(accountId: number) {
const depositAccount = await this.accountModel()
.query()
.findById(accountId);
if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
}
if (!depositAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET);
}
}
/**
* Validate sale receipt number uniqueness on the storage.
* @param {string} receiptNumber -
* @param {number} notReceiptId -
*/
public async validateReceiptNumberUnique(
receiptNumber: string,
notReceiptId?: number,
) {
const saleReceipt = await this.saleReceiptModel()
.query()
.findOne('receipt_number', receiptNumber)
.onBuild((builder) => {
if (notReceiptId) {
builder.whereNot('id', notReceiptId);
}
});
if (saleReceipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NUMBER_NOT_UNIQUE);
}
}
/**
* Validate the sale receipt number require.
* @param {ISaleReceipt} saleReceipt
*/
public validateReceiptNoRequire(receiptNumber: string) {
if (!receiptNumber) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED);
}
}
/**
* Validate the given customer has no sales receipts.
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoReceipts(customerId: number) {
const receipts = await this.saleReceiptModel()
.query()
.where('customer_id', customerId);
if (receipts.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
}

View File

@@ -0,0 +1,35 @@
// import { Inject, Service } from 'typedi';
// import { ISalesReceiptsFilter } from '@/interfaces';
// import { Exportable } from '@/services/Export/Exportable';
// import { SaleReceiptApplication } from './SaleReceiptApplication';
// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
// @Service()
// export class SaleReceiptsExportable extends Exportable {
// @Inject()
// private saleReceiptsApp: SaleReceiptApplication;
// /**
// * Retrieves the accounts data to exportable sheet.
// * @param {number} tenantId
// * @returns
// */
// public exportable(tenantId: number, query: ISalesReceiptsFilter) {
// 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 ISalesReceiptsFilter;
// return this.saleReceiptsApp
// .getSaleReceipts(tenantId, parsedQuery)
// .then((output) => output.data);
// }
// }

View File

@@ -0,0 +1,45 @@
// import { Inject, Service } from 'typedi';
// import { Knex } from 'knex';
// import { IAccountCreateDTO, ISaleReceiptDTO } from '@/interfaces';
// import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service';
// import { Importable } from '@/services/Import/Importable';
// import { SaleReceiptsSampleData } from './constants';
// @Service()
// export class SaleReceiptsImportable extends Importable {
// @Inject()
// private createReceiptService: CreateSaleReceipt;
// /**
// * Importing to sale receipts service.
// * @param {number} tenantId
// * @param {IAccountCreateDTO} createAccountDTO
// * @returns
// */
// public importable(
// tenantId: number,
// createAccountDTO: ISaleReceiptDTO,
// trx?: Knex.Transaction
// ) {
// return this.createReceiptService.createSaleReceipt(
// tenantId,
// createAccountDTO,
// 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 SaleReceiptsSampleData;
// }
// }

View File

@@ -0,0 +1,126 @@
export const DEFAULT_RECEIPT_MAIL_SUBJECT =
'Receipt {Receipt Number} from {Company Name}';
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
<p>Dear {Customer Name}</p>
<p>Thank you for your business, You can view or print your receipt from attachements.</p>
<p>
Receipt <strong>#{Receipt Number}</strong><br />
Amount : <strong>{Receipt Amount}</strong></br />
</p>
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
export const SendSaleReceiptMailQueue = 'SendSaleReceiptMailQueue';
export const SendSaleReceiptMailJob = 'SendSaleReceiptMailJob';
export const ERRORS = {
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
};
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: 'Closed',
slug: 'closed',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'closed' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const SaleReceiptsSampleData = [
{
'Receipt Date': '2023-01-01',
Customer: 'Randall Kohler',
'Deposit Account': 'Petty Cash',
'Exchange Rate': '',
'Receipt Number': 'REC-00001',
'Reference No.': 'REF-0001',
Statement: 'Delectus unde aut soluta et accusamus placeat.',
'Receipt Message': 'Vitae asperiores dicta.',
Closed: 'T',
Item: 'Schmitt Group',
Quantity: 100,
Rate: 200,
'Line Description':
'Distinctio distinctio sit veritatis consequatur iste quod veritatis.',
},
];
export const defaultSaleReceiptBrandingAttributes = {
primaryColor: '',
secondaryColor: '',
companyName: 'Bigcapital Technology, Inc.',
// # Company logo
showCompanyLogo: true,
companyLogoUri: '',
companyLogoKey: '',
// # Customer address
showCustomerAddress: true,
customerAddress: '',
// # Company address
showCompanyAddress: true,
companyAddress: '',
billedToLabel: 'Billed To',
// # Total
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',
},
],
showReceiptNumber: true,
receiptNumberLabel: 'Receipt Number',
receiptNumebr: '346D3D40-0001',
receiptDate: 'September 3, 2024',
showReceiptDate: true,
receiptDateLabel: 'Receipt Date',
};

View File

@@ -0,0 +1,170 @@
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
IsString,
Min,
ValidateNested,
} from 'class-validator';
enum DiscountType {
Percentage = 'percentage',
Amount = 'amount',
}
class SaleReceiptEntryDto extends ItemEntryDto {}
class AttachmentDto {
@IsString()
key: string;
}
export class CommandSaleReceiptDto {
@IsNumber()
@IsNotEmpty()
@ApiProperty({
description: 'The id of the customer',
example: 1,
})
customerId: number;
@IsOptional()
@IsNumber()
@IsPositive()
@ApiProperty({
description: 'The exchange rate of the sale receipt',
example: 1,
})
exchangeRate?: number;
@IsNumber()
@IsNotEmpty()
@ApiProperty({ description: 'The id of the deposit account', example: 1 })
depositAccountId: number;
@IsDate()
@IsNotEmpty()
@ApiProperty({
description: 'The date of the sale receipt',
example: '2021-01-01',
})
receiptDate: Date;
@IsOptional()
@IsString()
@ApiProperty({
description: 'The receipt number of the sale receipt',
example: '123456',
})
receiptNumber?: string;
@IsOptional()
@IsString()
@ApiProperty({
description: 'The reference number of the sale receipt',
example: '123456',
})
referenceNo?: string;
@IsBoolean()
@ApiProperty({
description: 'Whether the sale receipt is closed',
example: false,
})
closed: boolean = false;
@IsOptional()
@IsNumber()
@ApiProperty({
description: 'The id of the warehouse',
example: 1,
})
warehouseId?: number;
@IsOptional()
@IsNumber()
@ApiProperty({
description: 'The id of the branch',
example: 1,
})
branchId?: number;
@IsArray()
@ValidateNested({ each: true })
@Type(() => SaleReceiptEntryDto)
@Min(1)
@ApiProperty({
description: 'The entries of the sale receipt',
example: [{ key: '123456' }],
})
entries: SaleReceiptEntryDto[];
@IsOptional()
@IsString()
@ApiProperty({
description: 'The receipt message of the sale receipt',
example: '123456',
})
receiptMessage?: string;
@IsOptional()
@IsString()
@ApiProperty({
description: 'The statement of the sale receipt',
example: '123456',
})
statement?: string;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AttachmentDto)
@ApiProperty({
description: 'The attachments of the sale receipt',
example: [{ key: '123456' }],
})
attachments?: AttachmentDto[];
@IsOptional()
@IsNumber()
@ApiProperty({
description: 'The id of the pdf template',
example: 1,
})
pdfTemplateId?: number;
@IsOptional()
@IsNumber()
@ApiProperty({
description: 'The discount of the sale receipt',
example: 1,
})
discount?: number;
@IsOptional()
@IsEnum(DiscountType)
@ApiProperty({
description: 'The discount type of the sale receipt',
example: DiscountType.Percentage,
})
discountType?: DiscountType;
@IsOptional()
@IsNumber()
@ApiProperty({
description: 'The adjustment of the sale receipt',
example: 1,
})
adjustment?: number;
}
export class CreateSaleReceiptDto extends CommandSaleReceiptDto {}
export class EditSaleReceiptDto extends CommandSaleReceiptDto {}

View File

@@ -0,0 +1,67 @@
// @ts-nocheck
import { Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { SaleReceipt } from '../models/SaleReceipt';
import { InventoryTransactionsService } from '@/modules/InventoryCost/commands/InventoryTransactions.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
@Injectable()
export class SaleReceiptInventoryTransactions {
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly inventoryService: InventoryTransactionsService,
) {}
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async recordInventoryTransactions(
saleReceipt: SaleReceipt,
override?: boolean,
trx?: Knex.Transaction,
): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
saleReceipt.entries,
);
const transaction = {
transactionId: saleReceipt.id,
transactionType: 'SaleReceipt',
transactionNumber: saleReceipt.receiptNumber,
exchangeRate: saleReceipt.exchangeRate,
date: saleReceipt.receiptDate,
direction: 'OUT',
entries: inventoryEntries,
createdAt: saleReceipt.createdAt,
warehouseId: saleReceipt.warehouseId,
};
return this.inventoryService.recordInventoryTransactionsFromItemsEntries(
transaction,
override,
trx,
);
}
/**
* Reverts the inventory transactions of the given bill id.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
receiptId: number,
trx?: Knex.Transaction,
) {
return this.inventoryService.deleteInventoryTransactions(
receiptId,
'SaleReceipt',
trx,
);
}
}

View File

@@ -0,0 +1,69 @@
import { OnEvent } from '@nestjs/event-emitter';
import {
ISaleReceiptCreatedPayload,
ISaleReceiptEditedPayload,
ISaleReceiptEventDeletedPayload,
} from '../types/SaleReceipts.types';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { SaleReceiptInventoryTransactions } from './SaleReceiptInventoryTransactions';
@Injectable()
export class SaleReceiptInventoryTransactionsSubscriber {
constructor(
private readonly saleReceiptInventory: SaleReceiptInventoryTransactions
) {}
/**
* Handles the writing inventory transactions once the receipt created.
* @param {ISaleReceiptCreatedPayload} payload -
*/
@OnEvent(events.saleReceipt.onCreated)
public async handleWritingInventoryTransactions({
saleReceipt,
trx,
}: ISaleReceiptCreatedPayload) {
// Can't continue if the sale receipt is not closed yet.
if (!saleReceipt.closedAt) return null;
await this.saleReceiptInventory.recordInventoryTransactions(
saleReceipt,
false,
trx
);
};
/**
* Rewriting the inventory transactions once the sale invoice be edited.
* @param {ISaleReceiptEditedPayload} payload -
*/
@OnEvent(events.saleReceipt.onEdited)
public async handleRewritingInventoryTransactions({
saleReceipt,
trx,
}: ISaleReceiptEditedPayload) {
// Can't continue if the sale receipt is not closed yet.
if (!saleReceipt.closedAt) return null;
await this.saleReceiptInventory.recordInventoryTransactions(
saleReceipt,
true,
trx
);
};
/**
* Handles deleting the inventory transactions once the receipt deleted.
* @param {ISaleReceiptEventDeletedPayload} payload -
*/
@OnEvent(events.saleReceipt.onDeleted)
public async handleDeletingInventoryTransactions({
saleReceiptId,
trx,
}: ISaleReceiptEventDeletedPayload) {
await this.saleReceiptInventory.revertInventoryTransactions(
saleReceiptId,
trx
);
};
}

View File

@@ -0,0 +1,167 @@
import * as R from 'ramda';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { AccountNormal } from '@/modules/Accounts/Accounts.types';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { SaleReceipt } from '../models/SaleReceipt';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
export class SaleReceiptGL {
private saleReceipt: SaleReceipt;
private discountAccountId: number;
private otherChargesAccountId: number;
/**
* Constructor method.
* @param {SaleReceipt} saleReceipt - Sale receipt.
*/
constructor(saleReceipt: SaleReceipt) {
this.saleReceipt = saleReceipt;
}
/**
* Sets the discount account id.
* @param {number} discountAccountId - Discount account id.
*/
setDiscountAccountId(discountAccountId: number) {
this.discountAccountId = discountAccountId;
return this;
}
/**
* Sets the other charges account id.
* @param {number} otherChargesAccountId - Other charges account id.
*/
setOtherChargesAccountId(otherChargesAccountId: number) {
this.otherChargesAccountId = otherChargesAccountId;
return this;
}
/**
* Retrieves the income GL common entry.
*/
private getIncomeGLCommonEntry = () => {
return {
currencyCode: this.saleReceipt.currencyCode,
exchangeRate: this.saleReceipt.exchangeRate,
transactionType: 'SaleReceipt',
transactionId: this.saleReceipt.id,
date: this.saleReceipt.receiptDate,
transactionNumber: this.saleReceipt.receiptNumber,
referenceNumber: this.saleReceipt.referenceNo,
createdAt: this.saleReceipt.createdAt,
credit: 0,
debit: 0,
userId: this.saleReceipt.userId,
branchId: this.saleReceipt.branchId,
};
};
/**
* Retrieve receipt income item G/L entry.
* @param {ItemEntry} entry - Item entry.
* @param {number} index - Index.
* @returns {ILedgerEntry}
*/
private getReceiptIncomeItemEntry = R.curry(
(entry: ItemEntry, index: number): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry();
const totalLocal =
entry.totalExcludingTax * this.saleReceipt.exchangeRate;
return {
...commonEntry,
credit: totalLocal,
accountId: entry.item.sellAccountId,
note: entry.description,
index: index + 2,
itemId: entry.itemId,
// itemQuantity: entry.quantity,
accountNormal: AccountNormal.CREDIT,
};
},
);
/**
* Retrieves the receipt deposit GL deposit entry.
* @returns {ILedgerEntry}
*/
private getReceiptDepositEntry = (): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry();
return {
...commonEntry,
debit: this.saleReceipt.totalLocal,
accountId: this.saleReceipt.depositAccountId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the discount GL entry.
* @returns {ILedgerEntry}
*/
private getDiscountEntry = (): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry();
return {
...commonEntry,
debit: this.saleReceipt.discountAmountLocal,
accountId: this.discountAccountId,
index: 1,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieves the adjustment GL entry.
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry();
const adjustmentAmount = Math.abs(this.saleReceipt.adjustmentLocal);
return {
...commonEntry,
debit: this.saleReceipt.adjustmentLocal < 0 ? adjustmentAmount : 0,
credit: this.saleReceipt.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: this.otherChargesAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the income GL entries.
* @returns {ILedgerEntry[]}
*/
public getIncomeGLEntries = (): ILedgerEntry[] => {
const getItemEntry = this.getReceiptIncomeItemEntry;
const creditEntries = this.saleReceipt.entries.map((e, index) =>
getItemEntry(e, index),
);
const depositEntry = this.getReceiptDepositEntry();
const discountEntry = this.getDiscountEntry();
const adjustmentEntry = this.getAdjustmentEntry();
return [depositEntry, ...creditEntries, discountEntry, adjustmentEntry];
};
/**
* Retrieves the income GL ledger.
* @returns {ILedger}
*/
public getIncomeLedger = (): ILedger => {
const entries = this.getIncomeGLEntries();
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,83 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { SaleReceipt } from '../models/SaleReceipt';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { SaleReceiptGL } from './SaleReceiptGL';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class SaleReceiptGLEntries {
constructor(
private readonly ledgerStorage: LedgerStorageService,
private readonly accountRepository: AccountRepository,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
) {}
/**
* Creates income GL entries.
* @param {number} saleReceiptId
* @param {Knex.Transaction} trx
*/
public writeIncomeGLEntries = async (
saleReceiptId: number,
trx?: Knex.Transaction,
): Promise<void> => {
const saleReceipt = await this.saleReceiptModel()
.query(trx)
.findById(saleReceiptId)
.withGraphFetched('entries.item');
// Find or create the discount expense account.
const discountAccount =
await this.accountRepository.findOrCreateDiscountAccount({}, trx);
// Find or create the other charges account.
const otherChargesAccount =
await this.accountRepository.findOrCreateOtherChargesAccount({}, trx);
// Retrieves the income ledger.
const incomeLedger = new SaleReceiptGL(saleReceipt)
.setDiscountAccountId(discountAccount.id)
.setOtherChargesAccountId(otherChargesAccount.id)
.getIncomeLedger();
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(incomeLedger, trx);
};
/**
* Reverts the receipt GL entries.
* @param {number} saleReceiptId - Sale receipt id.
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public revertReceiptGLEntries = async (
saleReceiptId: number,
trx?: Knex.Transaction,
): Promise<void> => {
await this.ledgerStorage.deleteByReference(
saleReceiptId,
'SaleReceipt',
trx,
);
};
/**
* Rewrites the receipt GL entries.
* @param {number} saleReceiptId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public rewriteReceiptGLEntries = async (
saleReceiptId: number,
trx?: Knex.Transaction,
): Promise<void> => {
// Reverts the receipt GL entries.
await this.revertReceiptGLEntries(saleReceiptId, trx);
// Writes the income GL entries.
await this.writeIncomeGLEntries(saleReceiptId, trx);
};
}

View File

@@ -0,0 +1,398 @@
import { Model } from 'objection';
import { defaultTo } from 'lodash';
import * as R from 'ramda';
import { BaseModel } from '@/models/Model';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { Customer } from '@/modules/Customers/models/Customer';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Branch } from '@/modules/Branches/models/Branch.model';
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
import { DiscountType } from '@/common/types/Discount';
import { MetadataModelMixin } from '@/modules/DynamicListing/models/MetadataModel';
import { ResourceableModelMixin } from '@/modules/Resource/models/ResourcableModel';
import { CustomViewBaseModelMixin } from '@/modules/CustomViews/CustomViewBaseModel';
import { SearchableBaseModelMixin } from '@/modules/DynamicListing/models/SearchableBaseModel';
const ExtendedModel = R.pipe(
CustomViewBaseModelMixin,
SearchableBaseModelMixin,
ResourceableModelMixin,
MetadataModelMixin,
)(BaseModel);
export class SaleReceipt extends ExtendedModel {
public amount!: number;
public exchangeRate!: number;
public currencyCode!: string;
public depositAccountId!: number;
public customerId!: number;
public receiptDate!: Date;
public receiptNumber!: string;
public referenceNo!: string;
public sendToEmail!: string;
public receiptMessage!: string;
public statement!: string;
public closedAt!: Date | string;
public discountType!: DiscountType;
public discount!: number;
public adjustment!: number;
public branchId!: number;
public warehouseId!: number;
public userId!: number;
public createdAt!: Date;
public updatedAt!: Date | null;
public customer!: Customer;
public entries!: ItemEntry[];
public transactions!: AccountTransaction[];
public branch!: Branch;
public warehouse!: Warehouse;
/**
* Table name
*/
static get tableName() {
return 'sales_receipts';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'localAmount',
'subtotal',
'subtotalLocal',
'total',
'totalLocal',
'adjustment',
'adjustmentLocal',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'paid',
'paidLocal',
'isClosed',
'isDraft',
];
}
/**
* Estimate amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Receipt subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Receipt subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.localAmount;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount() {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Discount amount in local currency.
* @returns {number | null}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
* Receipt total.
* @returns {number}
*/
get total(): number {
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.subtotal - this.discountAmount + adjustmentAmount;
}
/**
* Receipt total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/**
* Adjustment amount in local currency.
* @returns {number}
*/
get adjustmentLocal() {
return this.adjustment * this.exchangeRate;
}
/**
* Receipt paid amount.
* @returns {number}
*/
get paid() {
return this.total;
}
/**
* Receipt paid amount in local currency.
* @returns {number}
*/
get paidLocal() {
return this.paid * this.exchangeRate;
}
/**
* Detarmine whether the sale receipt closed.
* @return {boolean}
*/
get isClosed() {
return !!this.closedAt;
}
/**
* Detarmines whether the sale receipt drafted.
* @return {boolean}
*/
get isDraft() {
return !this.closedAt;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the closed receipts.
*/
closed(query) {
query.whereNot('closed_at', null);
},
/**
* Filters the invoices in draft status.
*/
draft(query) {
query.where('closed_at', null);
},
/**
* Sorting the receipts order by status.
*/
sortByStatus(query, order) {
query.orderByRaw(`CLOSED_AT IS NULL ${order}`);
},
/**
* Filtering the receipts orders by status.
*/
filterByStatus(query, status) {
switch (status) {
case 'draft':
query.modify('draft');
break;
case 'closed':
default:
query.modify('closed');
break;
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { Customer } = require('../../Customers/models/Customer');
const { Account } = require('../../Accounts/models/Account.model');
const {
AccountTransaction,
} = require('../../Accounts/models/AccountTransaction.model');
const {
ItemEntry,
} = require('../../TransactionItemEntry/models/ItemEntry');
const { Branch } = require('../../Branches/models/Branch.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
return {
/**
* Sale receipt may has a customer.
*/
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer,
join: {
from: 'sales_receipts.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'customer');
},
},
/**
* Sale receipt may has a deposit account.
*/
depositAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'sales_receipts.depositAccountId',
to: 'accounts.id',
},
},
/**
* Sale receipt may has many items entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry,
join: {
from: 'sales_receipts.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleReceipt');
builder.orderBy('index', 'ASC');
},
},
/**
* Sale receipt may has many transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'sales_receipts.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleReceipt');
},
},
/**
* Sale receipt may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch,
join: {
from: 'sales_receipts.branchId',
to: 'branches.id',
},
},
/**
* Sale receipt may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse,
join: {
from: 'sales_receipts.warehouseId',
to: 'warehouses.id',
},
},
/**
* Sale receipt transaction may has many attached attachments.
*/
attachments: {
relation: Model.ManyToManyRelation,
modelClass: Document,
join: {
from: 'sales_receipts.id',
through: {
from: 'document_links.modelId',
to: 'document_links.documentId',
},
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'SaleReceipt');
},
},
};
}
/**
* Sale invoice meta.
*/
// static get meta() {
// return SaleReceiptSettings;
// }
/**
* Retrieve the default custom views, roles and columns.
*/
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'receipt_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,24 @@
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { SendSaleReceiptMailQueue } from '../constants';
import { SaleReceiptMailNotification } from '../commands/SaleReceiptMailNotification';
import { SaleReceiptSendMailPayload } from '../types/SaleReceipts.types';
import { ClsService } from 'nestjs-cls';
@Processor(SendSaleReceiptMailQueue)
export class SendSaleReceiptMailProcess {
constructor(
private readonly saleReceiptMailNotification: SaleReceiptMailNotification,
private readonly clsService: ClsService,
) {}
@Process(SendSaleReceiptMailQueue)
async handleSendMailJob(job: Job<SaleReceiptSendMailPayload>) {
const { messageOpts, saleReceiptId, organizationId, userId } = job.data;
this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);
await this.saleReceiptMailNotification.sendMail(saleReceiptId, messageOpts);
}
}

View File

@@ -0,0 +1,37 @@
import { Inject, Injectable } from '@nestjs/common';
import { SaleReceiptTransformer } from './SaleReceiptTransformer';
import { SaleReceiptValidators } from '../commands/SaleReceiptValidators.service';
import { SaleReceipt } from '../models/SaleReceipt';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetSaleReceipt {
constructor(
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
private readonly transformer: TransformerInjectable,
) {}
/**
* Retrieve sale receipt with associated entries.
* @param {Integer} saleReceiptId
* @return {ISaleReceipt}
*/
public async getSaleReceipt(saleReceiptId: number) {
const saleReceipt = await this.saleReceiptModel()
.query()
.findById(saleReceiptId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('depositAccount')
.withGraphFetched('branch')
.withGraphFetched('attachments')
.throwIfNotFound();
return this.transformer.transform(
saleReceipt,
new SaleReceiptTransformer(),
);
}
}

View File

@@ -0,0 +1,27 @@
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { Inject, Injectable } from '@nestjs/common';
import { ISaleReceiptState } from '../types/SaleReceipts.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetSaleReceiptState {
constructor(
@Inject(PdfTemplateModel.name)
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
) {}
/**
* Retrieves the sale receipt state.
* @return {Promise<ISaleReceiptState>}
*/
public async getSaleReceiptState(): Promise<ISaleReceiptState> {
const defaultPdfTemplate = await this.pdfTemplateModel()
.query()
.findOne({ resource: 'SaleReceipt' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -0,0 +1,73 @@
import * as R from 'ramda';
import { Inject, Injectable } from '@nestjs/common';
import { SaleReceiptTransformer } from './SaleReceiptTransformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { ISalesReceiptsFilter } from '../types/SaleReceipts.types';
import { SaleReceipt } from '../models/SaleReceipt';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
interface GetSaleReceiptsSettings {
fetchEntriesGraph?: boolean;
}
@Injectable()
export class GetSaleReceiptsService {
constructor(
private readonly transformer: TransformerInjectable,
private readonly dynamicListService: DynamicListService,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
) {}
/**
* Retrieve sales receipts paginated and filterable list.
* @param {ISalesReceiptsFilter} salesReceiptsFilter - Sales receipts filter.
*/
public async getSaleReceipts(filterDTO: ISalesReceiptsFilter): Promise<{
data: SaleReceipt[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
SaleReceipt,
filter,
);
const { results, pagination } = await this.saleReceiptModel()
.query()
.onBuild((builder) => {
builder.withGraphFetched('depositAccount');
builder.withGraphFetched('customer');
builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder);
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformes the estimates models to POJO.
const salesEstimates = await this.transformer.transform(
results,
new SaleReceiptTransformer(),
);
return {
data: salesEstimates,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { defaultSaleReceiptBrandingAttributes } from '../constants';
import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service';
import { GetOrganizationBrandingAttributesService } from '@/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service';
import { mergePdfTemplateWithDefaultAttributes } from '@/modules/SaleInvoices/utils';
@Injectable()
export class SaleReceiptBrandingTemplate {
/**
* @param {GetPdfTemplate} getPdfTemplateService -
* @param {GetOrganizationBrandingAttributes} getOrgBrandingAttributes -
*/
constructor(
private readonly getPdfTemplateService: GetPdfTemplateService,
private readonly getOrgBrandingAttributes: GetOrganizationBrandingAttributesService,
) {}
/**
* Retrieves the sale receipt branding template.
* @param {number} templateId - The ID of the PDF template.
* @returns {Promise<Object>} The sale receipt branding template with merged attributes.
*/
public async getSaleReceiptBrandingTemplate(templateId: number) {
const template =
await this.getPdfTemplateService.getPdfTemplate(templateId);
// Retrieves the organization branding attributes.
const commonOrgBrandingAttrs =
await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes();
// Merges the default branding attributes with organization common branding attrs.
const organizationBrandingAttrs = {
...defaultSaleReceiptBrandingAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
brandingTemplateAttrs,
organizationBrandingAttrs,
);
return {
...template,
attributes,
};
}
}

View File

@@ -0,0 +1,89 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { SaleReceipt } from '../models/SaleReceipt';
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer';
export class SaleReceiptTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedSubtotal',
'formattedAmount',
'formattedReceiptDate',
'formattedClosedAtDate',
'formattedCreatedAt',
'entries',
'attachments',
];
};
/**
* Retrieve formatted receipt date.
* @param {ISaleReceipt} invoice
* @returns {String}
*/
protected formattedReceiptDate = (receipt: SaleReceipt): string => {
return this.formatDate(receipt.receiptDate);
};
/**
* Retrieve formatted estimate closed at date.
* @param {ISaleReceipt} invoice
* @returns {String}
*/
protected formattedClosedAtDate = (receipt: SaleReceipt): string => {
return this.formatDate(receipt.closedAt);
};
/**
* Retrieve formatted receipt created at date.
* @param receipt
* @returns {string}
*/
protected formattedCreatedAt = (receipt: SaleReceipt): string => {
return this.formatDate(receipt.createdAt);
};
/**
* Retrieves the estimate formatted subtotal.
* @param {ISaleReceipt} receipt
* @returns {string}
*/
protected formattedSubtotal = (receipt: SaleReceipt): string => {
return this.formatNumber(receipt.amount, { money: false });
};
/**
* Retrieve formatted invoice amount.
* @param {ISaleReceipt} estimate
* @returns {string}
*/
protected formattedAmount = (receipt: SaleReceipt): string => {
return this.formatNumber(receipt.amount, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the entries of the credit note.
* @param {ISaleReceipt} credit
* @returns {}
*/
// protected entries = (receipt: SaleReceipt) => {
// return this.item(receipt.entries, new ItemEntryTransformer(), {
// currencyCode: receipt.currencyCode,
// });
// };
/**
* Retrieves the sale receipt attachments.
* @param {SaleReceipt} receipt
* @returns
*/
// protected attachments = (receipt: SaleReceipt) => {
// return this.item(receipt.attachments, new AttachmentTransformer());
// };
}

View File

@@ -0,0 +1,112 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetSaleReceipt } from './GetSaleReceipt.service';
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate.service';
import { transformReceiptToBrandingTemplateAttributes } from '../utils';
import { SaleReceipt } from '../models/SaleReceipt';
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { ISaleReceiptBrandingTemplateAttributes } from '../types/SaleReceipts.types';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class SaleReceiptsPdfService {
/**
* @param {ChromiumlyTenancy} chromiumlyTenancy -
* @param {TemplateInjectable} templateInjectable -
* @param {GetSaleReceipt} getSaleReceiptService -
* @param {SaleReceiptBrandingTemplate} saleReceiptBrandingTemplate -
* @param {EventEmitter2} eventPublisher -
* @param {typeof SaleReceipt} saleReceiptModel -
* @param {typeof PdfTemplateModel} pdfTemplateModel -
*/
constructor(
private readonly chromiumlyTenancy: ChromiumlyTenancy,
private readonly templateInjectable: TemplateInjectable,
private readonly getSaleReceiptService: GetSaleReceipt,
private readonly saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate,
private readonly eventPublisher: EventEmitter2,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
@Inject(PdfTemplateModel.name)
private readonly pdfTemplateModel: TenantModelProxy<
typeof PdfTemplateModel
>,
) {}
/**
* Retrieves sale invoice pdf content.
* @param {number} saleReceiptId - Sale receipt identifier.
* @returns {Promise<Buffer>}
*/
public async saleReceiptPdf(
saleReceiptId: number,
): Promise<[Buffer, string]> {
const filename = await this.getSaleReceiptFilename(saleReceiptId);
const brandingAttributes =
await this.getReceiptBrandingAttributes(saleReceiptId);
// Converts the receipt template to html content.
const htmlContent = await this.templateInjectable.render(
'modules/receipt-regular',
brandingAttributes,
);
// Renders the html content to pdf document.
const content =
await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
const eventPayload = { saleReceiptId };
// Triggers the `onSaleReceiptPdfViewed` event.
await this.eventPublisher.emitAsync(
events.saleReceipt.onPdfViewed,
eventPayload,
);
return [content, filename];
}
/**
* Retrieves the filename file document of the given sale receipt.
* @param {number} receiptId
* @returns {Promise<string>}
*/
public async getSaleReceiptFilename(receiptId: number): Promise<string> {
const receipt = await this.saleReceiptModel().query().findById(receiptId);
return `Receipt-${receipt.receiptNumber}`;
}
/**
* Retrieves receipt branding attributes.
* @param {number} receiptId - Sale receipt identifier.
* @returns {Promise<ISaleReceiptBrandingTemplateAttributes>}
*/
public async getReceiptBrandingAttributes(
receiptId: number,
): Promise<ISaleReceiptBrandingTemplateAttributes> {
const saleReceipt =
await this.getSaleReceiptService.getSaleReceipt(receiptId);
// Retrieve the invoice template id of not found get the default template id.
const templateId =
saleReceipt.pdfTemplateId ??
(
await this.pdfTemplateModel().query().findOne({
resource: 'SaleReceipt',
default: true,
})
)?.id;
// Retrieves the receipt branding template.
const brandingTemplate =
await this.saleReceiptBrandingTemplate.getSaleReceiptBrandingTemplate(
templateId,
);
return {
...brandingTemplate.attributes,
...transformReceiptToBrandingTemplateAttributes(saleReceipt),
};
}
}

View File

@@ -0,0 +1,36 @@
// import { Inject, Service } from 'typedi';
// import events from '@/subscribers/events';
// import { IInventoryCostLotsGLEntriesWriteEvent } from '@/interfaces';
// import { SaleReceiptCostGLEntries } from '../SaleReceiptCostGLEntries';
// @Service()
// export class SaleReceiptCostGLEntriesSubscriber {
// @Inject()
// private saleReceiptCostEntries: SaleReceiptCostGLEntries;
// /**
// * Attaches events.
// */
// public attach(bus) {
// bus.subscribe(
// events.inventory.onCostLotsGLEntriesWrite,
// this.writeJournalEntriesOnceWriteoffCreate
// );
// }
// /**
// * Writes the receipts cost GL entries once the inventory cost lots be written.
// * @param {IInventoryCostLotsGLEntriesWriteEvent}
// */
// private writeJournalEntriesOnceWriteoffCreate = async ({
// trx,
// startingDate,
// tenantId,
// }: IInventoryCostLotsGLEntriesWriteEvent) => {
// await this.saleReceiptCostEntries.writeInventoryCostJournalEntries(
// tenantId,
// startingDate,
// trx
// );
// };
// }

View File

@@ -0,0 +1,63 @@
import { Inject, Injectable } from '@nestjs/common';
import {
ISaleReceiptCreatedPayload,
ISaleReceiptEditedPayload,
ISaleReceiptEventDeletedPayload,
} from '../types/SaleReceipts.types';
import { SaleReceiptGLEntries } from '../ledger/SaleReceiptGLEntries';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Injectable()
export class SaleReceiptGLEntriesSubscriber {
constructor(private readonly saleReceiptGLEntries: SaleReceiptGLEntries) {}
/**
* Handles writing sale receipt income journal entries once created.
* @param {ISaleReceiptCreatedPayload} payload -
*/
@OnEvent(events.saleReceipt.onCreated)
@OnEvent(events.saleReceipt.onClosed)
public async handleWriteReceiptIncomeJournalEntrieOnCreate({
saleReceiptId,
saleReceipt,
trx,
}: ISaleReceiptCreatedPayload) {
// Can't continue if the sale receipt is not closed yet.
if (!saleReceipt.closedAt) return null;
// Writes the sale receipt income journal entries.
await this.saleReceiptGLEntries.writeIncomeGLEntries(saleReceiptId, trx);
}
/**
* Handles sale receipt revert jouranl entries once be deleted.
* @param {ISaleReceiptEventDeletedPayload} payload -
*/
@OnEvent(events.saleReceipt.onDeleted)
public async handleRevertReceiptJournalEntriesOnDeleted({
saleReceiptId,
trx,
}: ISaleReceiptEventDeletedPayload) {
await this.saleReceiptGLEntries.revertReceiptGLEntries(saleReceiptId, trx);
}
/**
* Handles writing sale receipt income journal entries once be edited.
* @param {ISaleReceiptEditedPayload} payload -
*/
@OnEvent(events.saleReceipt.onEdited)
public async handleWriteReceiptIncomeJournalEntrieOnEdited({
saleReceipt,
trx,
}: ISaleReceiptEditedPayload) {
// Can't continue if the sale receipt is not closed yet.
if (!saleReceipt.closedAt) return null;
// Writes the sale receipt income journal entries.
await this.saleReceiptGLEntries.rewriteReceiptGLEntries(
saleReceipt.id,
trx,
);
}
}

View File

@@ -0,0 +1,41 @@
// import { ISaleReceiptMailPresend } from '@/interfaces';
// import events from '@/subscribers/events';
// import { CloseSaleReceipt } from '../commands/CloseSaleReceipt.service';
// import { Inject, Service } from 'typedi';
// import { ServiceError } from '@/exceptions';
// import { ERRORS } from '../constants';
// @Service()
// export class SaleReceiptMarkClosedOnMailSentSubcriber {
// @Inject()
// private closeReceiptService: CloseSaleReceipt;
// /**
// * Attaches events.
// */
// public attach(bus) {
// bus.subscribe(events.saleReceipt.onPreMailSend, this.markReceiptClosed);
// }
// /**
// * Marks the sale receipt closed on submitting mail.
// * @param {ISaleReceiptMailPresend}
// */
// private markReceiptClosed = async ({
// tenantId,
// saleReceiptId,
// messageOptions,
// }: ISaleReceiptMailPresend) => {
// try {
// await this.closeReceiptService.closeSaleReceipt(tenantId, saleReceiptId);
// } catch (error) {
// if (
// error instanceof ServiceError &&
// error.errorType === ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED
// ) {
// } else {
// throw error;
// }
// }
// };
// }

View File

@@ -0,0 +1,151 @@
import { Knex } from 'knex';
import { SaleReceipt } from '../models/SaleReceipt';
import { CommonMailOptionsDTO } from '@/modules/MailNotification/MailNotification.types';
import { CommonMailOptions } from '@/modules/MailNotification/MailNotification.types';
import { TenantJobPayload } from '@/interfaces/Tenant';
import { CreateSaleReceiptDto, EditSaleReceiptDto } from '../dtos/SaleReceipt.dto';
export interface ISalesReceiptsFilter {
filterQuery?: (query: any) => void;
}
export interface ISaleReceiptSmsDetails {
customerName: string;
customerPhoneNumber: string;
smsMessage: string;
}
export interface ISaleReceiptCreatingPayload {
saleReceiptDTO: CreateSaleReceiptDto;
trx: Knex.Transaction;
}
export interface ISaleReceiptCreatedPayload {
// tenantId: number;
saleReceipt: SaleReceipt;
saleReceiptId: number;
saleReceiptDTO: CreateSaleReceiptDto;
trx: Knex.Transaction;
}
export interface ISaleReceiptEditedPayload {
oldSaleReceipt: SaleReceipt;
saleReceipt: SaleReceipt;
saleReceiptDTO: EditSaleReceiptDto;
trx: Knex.Transaction;
}
export interface ISaleReceiptEditingPayload {
oldSaleReceipt: SaleReceipt;
saleReceiptDTO: EditSaleReceiptDto;
trx: Knex.Transaction;
}
export interface ISaleReceiptEventClosedPayload {
saleReceiptId: number;
saleReceipt: SaleReceipt;
trx: Knex.Transaction;
}
export interface ISaleReceiptEventClosingPayload {
oldSaleReceipt: SaleReceipt;
trx: Knex.Transaction;
}
export interface ISaleReceiptEventDeletedPayload {
tenantId: number;
saleReceiptId: number;
oldSaleReceipt: SaleReceipt;
trx: Knex.Transaction;
}
export enum SaleReceiptAction {
Create = 'Create',
Edit = 'Edit',
Delete = 'Delete',
View = 'View',
NotifyBySms = 'NotifyBySms',
}
export interface ISaleReceiptDeletingPayload {
tenantId: number;
oldSaleReceipt: SaleReceipt;
trx: Knex.Transaction;
}
export interface SaleReceiptMailOpts extends CommonMailOptions {
attachReceipt: boolean;
}
export interface SaleReceiptMailOptsDTO extends CommonMailOptionsDTO {
attachReceipt?: boolean;
}
export interface ISaleReceiptMailPresend {
tenantId: number;
saleReceiptId: number;
messageOptions: SaleReceiptMailOptsDTO;
}
export interface ISaleReceiptBrandingTemplateAttributes {
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
total: string;
totalLabel: string;
showTotal: boolean;
// Subtotal
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
// Customer Note
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
// Terms & Conditions
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;
// Lines
lines: Array<{
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}>;
// Receipt Number
showReceiptNumber: boolean;
receiptNumberLabel: string;
receiptNumebr: string;
// Receipt Date
receiptDate: string;
showReceiptDate: boolean;
receiptDateLabel: string;
}
export interface ISaleReceiptState {
defaultTemplateId: number;
}
export interface SaleReceiptSendMailPayload extends TenantJobPayload {
messageOpts: SaleReceiptMailOptsDTO;
saleReceiptId: number;
}

View File

@@ -0,0 +1,34 @@
// @ts-nocheck
import {
ISaleReceipt,
ISaleReceiptBrandingTemplateAttributes,
} from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
export const transformReceiptToBrandingTemplateAttributes = (
saleReceipt: ISaleReceipt
): Partial<ISaleReceiptBrandingTemplateAttributes> => {
return {
total: saleReceipt.formattedAmount,
subtotal: saleReceipt.formattedSubtotal,
lines: saleReceipt.entries?.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
total: entry.totalFormatted,
})),
receiptNumber: saleReceipt.receiptNumber,
receiptDate: saleReceipt.formattedReceiptDate,
customerAddress: contactAddressTextFormat(saleReceipt.customer),
};
};
export const transformReceiptToMailDataArgs = (saleReceipt: any) => {
return {
'Customer Name': saleReceipt.customer.displayName,
'Receipt Number': saleReceipt.receiptNumber,
'Receipt Date': saleReceipt.formattedReceiptDate,
'Receipt Amount': saleReceipt.formattedAmount,
};
};