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,112 @@
import { Injectable } from '@nestjs/common';
import { omit } from 'lodash';
import * as moment from 'moment';
import * as composeAsync from 'async/compose';
import * as R from 'ramda';
import { ERRORS } from '../constants';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BrandingTemplateDTOTransformer } from '../../PdfTemplate/BrandingTemplateDTOTransformer';
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
import { CreditNoteAutoIncrementService } from './CreditNoteAutoIncrement.service';
import { CreditNote } from '../models/CreditNote';
import {
CreateCreditNoteDto,
CreditNoteEntryDto,
EditCreditNoteDto,
} from '../dtos/CreditNote.dto';
@Injectable()
export class CommandCreditNoteDTOTransform {
/**
* @param {ItemsEntriesService} itemsEntriesService - The items entries service.
* @param {BranchTransactionDTOTransformer} branchDTOTransform - The branch transaction DTO transformer.
* @param {WarehouseTransactionDTOTransform} warehouseDTOTransform - The warehouse transaction DTO transformer.
* @param {BrandingTemplateDTOTransformer} brandingTemplatesTransformer - The branding template DTO transformer.
* @param {CreditNoteAutoIncrementService} creditNoteAutoIncrement - The credit note auto increment service.
*/
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly branchDTOTransform: BranchTransactionDTOTransformer,
private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform,
private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
private readonly creditNoteAutoIncrement: CreditNoteAutoIncrementService,
) {}
/**
* Transforms the credit/edit DTO to model.
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
* @param {string} customerCurrencyCode -
*/
public transformCreateEditDTOToModel = async (
creditNoteDTO: CreateCreditNoteDto | EditCreditNoteDto,
customerCurrencyCode: string,
oldCreditNote?: CreditNote,
): Promise<CreditNote> => {
// Retrieve the total amount of the given items entries.
const amount = this.itemsEntriesService.getTotalItemsEntries(
creditNoteDTO.entries,
);
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex,
// Associate the reference type to credit note entries.
R.map((entry: CreditNoteEntryDto) => ({
...entry,
referenceType: 'CreditNote',
})),
)(creditNoteDTO.entries);
// Retrieves the next credit note number.
const autoNextNumber = this.creditNoteAutoIncrement.getNextCreditNumber();
// Determines the credit note number.
const creditNoteNumber =
creditNoteDTO.creditNoteNumber ||
oldCreditNote?.creditNoteNumber ||
autoNextNumber;
const initialDTO = {
...omit(creditNoteDTO, ['open', 'attachments']),
creditNoteNumber,
amount,
currencyCode: customerCurrencyCode,
exchangeRate: creditNoteDTO.exchangeRate || 1,
entries,
...(creditNoteDTO.open &&
!oldCreditNote?.openedAt && {
openedAt: moment().toMySqlDateTime(),
}),
refundedAmount: 0,
invoicesAmount: 0,
};
const asyncDto = (await composeAsync(
this.branchDTOTransform.transformDTO<CreditNote>,
this.warehouseDTOTransform.transformDTO<CreditNote>,
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'CreditNote',
),
)(initialDTO)) as CreditNote;
return asyncDto;
};
/**
* Validate the credit note remaining amount.
* @param {ICreditNote} creditNote
* @param {number} amount
*/
public validateCreditRemainingAmount = (
creditNote: CreditNote,
amount: number,
) => {
if (creditNote.creditsRemaining < amount) {
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT);
}
};
}

View File

@@ -0,0 +1,98 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import {
ICreditNoteCreatedPayload,
ICreditNoteCreatingPayload,
} from '../types/CreditNotes.types';
import { CreditNote } from '../models/CreditNote';
import { Contact } from '../../Contacts/models/Contact';
import { CommandCreditNoteDTOTransform } from './CommandCreditNoteDTOTransform.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { CreateCreditNoteDto } from '../dtos/CreditNote.dto';
@Injectable()
export class CreateCreditNoteService {
/**
* @param {UnitOfWork} uow - Unit of work.
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {typeof CreditNote} creditNoteModel - Credit note model.
* @param {typeof Contact} contactModel - Contact model.
* @param {CommandCreditNoteDTOTransform} commandCreditNoteDTOTransform - Command credit note DTO transform service.
*/
constructor(
private readonly uow: UnitOfWork,
private readonly itemsEntriesService: ItemsEntriesService,
private readonly eventPublisher: EventEmitter2,
private readonly commandCreditNoteDTOTransform: CommandCreditNoteDTOTransform,
@Inject(CreditNote.name)
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
@Inject(Contact.name)
private readonly contactModel: TenantModelProxy<typeof Contact>,
) {}
/**
* Creates a new credit note.
* @param creditNoteDTO
*/
public creditCreditNote = async (
creditNoteDTO: CreateCreditNoteDto,
trx?: Knex.Transaction,
) => {
// Triggers `onCreditNoteCreate` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreate, {
creditNoteDTO,
});
// Validate customer existance.
const customer = await this.contactModel()
.query()
.modify('customer')
.findById(creditNoteDTO.customerId)
.throwIfNotFound();
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
creditNoteDTO.entries,
);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
creditNoteDTO.entries,
);
// Transformes the given DTO to storage layer data.
const creditNoteModel =
await this.commandCreditNoteDTOTransform.transformCreateEditDTOToModel(
creditNoteDTO,
customer.currencyCode,
);
// Creates a new credit card transactions under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteCreating` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreating, {
creditNoteDTO,
trx,
} as ICreditNoteCreatingPayload);
// Upsert the credit note graph.
const creditNote = await this.creditNoteModel()
.query(trx)
.upsertGraph({
...creditNoteModel,
});
// Triggers `onCreditNoteCreated` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
creditNoteDTO,
creditNote,
creditNoteId: creditNote.id,
trx,
} as ICreditNoteCreatedPayload);
return creditNote;
}, trx);
};
}

View File

@@ -0,0 +1,28 @@
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class CreditNoteAutoIncrementService {
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
) {}
/**
* Retrieve the next unique credit number.
* @return {string}
*/
public getNextCreditNumber(): Promise<string> {
return this.autoIncrementOrdersService.getNextTransactionNumber(
'credit_note',
);
}
/**
* Increment the credit note serial next number.
*/
public incrementSerialNumber() {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'credit_note',
);
}
}

View File

@@ -0,0 +1,185 @@
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { CreditNote } from '../models/CreditNote';
import { AccountNormal } from '@/interfaces/Account';
import { Ledger } from '@/modules/Ledger/Ledger';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
export class CreditNoteGL {
creditNoteModel: CreditNote;
ARAccountId: number;
discountAccountId: number;
adjustmentAccountId: number;
/**
* @param {CreditNote} creditNoteModel - Credit note model.
*/
constructor(creditNoteModel: CreditNote) {
this.creditNoteModel = creditNoteModel;
}
/**
* Sets the A/R account id.
* @param {number} ARAccountId - A/R account id.
*/
public setARAccountId(ARAccountId: number) {
this.ARAccountId = ARAccountId;
return this;
}
/**
* Sets the discount account id.
* @param {number} discountAccountId - Discount account id.
*/
public setDiscountAccountId(discountAccountId: number) {
this.discountAccountId = discountAccountId;
return this;
}
/**
* Sets the adjustment account id.
* @param {number} adjustmentAccountId - Adjustment account id.
*/
public setAdjustmentAccountId(adjustmentAccountId: number) {
this.adjustmentAccountId = adjustmentAccountId;
return this;
}
/**
* Retrieve the credit note common entry.
* @returns {ICreditNoteGLCommonEntry}
*/
private get creditNoteCommonEntry() {
return {
date: this.creditNoteModel.creditNoteDate,
userId: this.creditNoteModel.userId,
currencyCode: this.creditNoteModel.currencyCode,
exchangeRate: this.creditNoteModel.exchangeRate,
transactionType: 'CreditNote',
transactionId: this.creditNoteModel.id,
transactionNumber: this.creditNoteModel.creditNoteNumber,
referenceNumber: this.creditNoteModel.referenceNo,
createdAt: this.creditNoteModel.createdAt,
indexGroup: 10,
credit: 0,
debit: 0,
branchId: this.creditNoteModel.branchId,
};
}
/**
* Retrieves the creidt note A/R entry.
* @param {ICreditNote} creditNote -
* @param {number} ARAccountId -
* @returns {ILedgerEntry}
*/
private get creditNoteAREntry() {
const commonEntry = this.creditNoteCommonEntry;
return {
...commonEntry,
credit: this.creditNoteModel.totalLocal,
accountId: this.ARAccountId,
contactId: this.creditNoteModel.customerId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
}
/**
* Retrieve the credit note item entry.
* @param {ItemEntry} entry
* @param {number} index
* @returns {ILedgerEntry}
*/
private getCreditNoteItemEntry(
entry: ItemEntry,
index: number,
): ILedgerEntry {
const commonEntry = this.creditNoteCommonEntry;
const totalLocal =
entry.totalExcludingTax * this.creditNoteModel.exchangeRate;
return {
...commonEntry,
debit: totalLocal,
accountId: entry.sellAccountId || entry.item.sellAccountId,
note: entry.description,
index: index + 2,
itemId: entry.itemId,
accountNormal: AccountNormal.CREDIT,
};
}
/**
* Retrieves the credit note discount entry.
* @param {ICreditNote} creditNote
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
private get discountEntry(): ILedgerEntry {
const commonEntry = this.creditNoteCommonEntry;
return {
...commonEntry,
credit: this.creditNoteModel.discountAmountLocal,
accountId: this.discountAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
}
/**
* Retrieves the credit note adjustment entry.
* @param {ICreditNote} creditNote
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
private get adjustmentEntry(): ILedgerEntry {
const commonEntry = this.creditNoteCommonEntry;
const adjustmentAmount = Math.abs(this.creditNoteModel.adjustmentLocal);
return {
...commonEntry,
credit: this.creditNoteModel.adjustmentLocal < 0 ? adjustmentAmount : 0,
debit: this.creditNoteModel.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: this.adjustmentAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
}
/**
* Retrieve the credit note GL entries.
* @param {ICreditNote} creditNote - Credit note.
* @param {IAccount} receivableAccount - Receviable account.
* @returns {ILedgerEntry[]} - Ledger entries.
*/
public getCreditNoteGLEntries(): ILedgerEntry[] {
const AREntry = this.creditNoteAREntry;
const itemsEntries = this.creditNoteModel.entries.map((entry, index) =>
this.getCreditNoteItemEntry(entry, index),
);
const discountEntry = this.discountEntry;
const adjustmentEntry = this.adjustmentEntry;
return [AREntry, discountEntry, adjustmentEntry, ...itemsEntries];
}
/**
* Retrieves the credit note GL.
* @param {ICreditNote} creditNote
* @param {number} receivableAccount
* @returns {Ledger}
*/
public getCreditNoteLedger(): Ledger {
const ledgerEntries = this.getCreditNoteGLEntries();
return new Ledger(ledgerEntries);
}
}

View File

@@ -0,0 +1,81 @@
import { Knex } from 'knex';
import { CreditNoteGL } from './CreditNoteGL';
import { Inject, Injectable } from '@nestjs/common';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { CreditNote } from '../models/CreditNote';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class CreditNoteGLEntries {
constructor(
private readonly ledgerStorage: LedgerStorageService,
private readonly accountRepository: AccountRepository,
@Inject(CreditNote.name)
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
) {}
/**
* Reverts the credit note associated GL entries.
* @param {number} creditNoteId - Credit note id.
* @param {Knex.Transaction} trx
*/
public revertVendorCreditGLEntries = async (
creditNoteId: number,
trx?: Knex.Transaction,
): Promise<void> => {
await this.ledgerStorage.deleteByReference(creditNoteId, 'CreditNote', trx);
};
/**
* Writes vendor credit associated GL entries.
* @param {number} creditNoteId - Credit note id.
* @param {Knex.Transaction} trx - Knex transactions.
*/
public createVendorCreditGLEntries = async (
creditNoteId: number,
trx?: Knex.Transaction,
): Promise<void> => {
// Retrieve the credit note with associated entries and items.
const creditNoteWithItems = await CreditNote.query(trx)
.findById(creditNoteId)
.withGraphFetched('entries.item');
// Retreive the the `accounts receivable` account based on the given currency.
const ARAccount =
await this.accountRepository.findOrCreateAccountReceivable(
creditNoteWithItems.currencyCode,
);
const discountAccount =
await this.accountRepository.findOrCreateDiscountAccount({});
const adjustmentAccount =
await this.accountRepository.findOrCreateOtherChargesAccount({});
const creditNoteLedger = new CreditNoteGL(creditNoteWithItems)
.setARAccountId(ARAccount.id)
.setDiscountAccountId(discountAccount.id)
.setAdjustmentAccountId(adjustmentAccount.id)
.getCreditNoteLedger();
// Saves the credit note GL entries.
await this.ledgerStorage.commit(creditNoteLedger, trx);
};
/**
* Edits vendor credit associated GL entries.
* @param {number} creditNoteId - Credit note id.
* @param {Knex.Transaction} trx
*/
public editVendorCreditGLEntries = async (
creditNoteId: number,
trx?: Knex.Transaction,
): Promise<void> => {
// Reverts vendor credit GL entries.
await this.revertVendorCreditGLEntries(creditNoteId, trx);
// Creates vendor credit Gl entries.
await this.createVendorCreditGLEntries(creditNoteId, trx);
};
}

View File

@@ -0,0 +1,35 @@
// import { Inject, Service } from 'typedi';
// import { ICreditNotesQueryDTO } from '@/interfaces';
// import { Exportable } from '@/services/Export/Exportable';
// import ListCreditNotes from '../ListCreditNotes';
// @Service()
// export class CreditNotesExportable extends Exportable {
// @Inject()
// private getCreditNotes: ListCreditNotes;
// /**
// * Retrieves the accounts data to exportable sheet.
// * @param {number} tenantId -
// * @param {IVendorCreditsQueryDTO} query -
// * @returns {}
// */
// public exportable(tenantId: number, query: ICreditNotesQueryDTO) {
// const filterQuery = (query) => {
// query.withGraphFetched('branch');
// query.withGraphFetched('warehouse');
// };
// const parsedQuery = {
// sortOrder: 'desc',
// columnSortBy: 'created_at',
// ...query,
// page: 1,
// pageSize: 12000,
// filterQuery,
// } as ICreditNotesQueryDTO;
// return this.getCreditNotes
// .getCreditNotesList(tenantId, parsedQuery)
// .then((output) => output.creditNotes);
// }
// }

View File

@@ -0,0 +1,44 @@
// import { Inject, Service } from 'typedi';
// import { Knex } from 'knex';
// import { ICreditNoteNewDTO } from '@/interfaces';
// import { Importable } from '../Import/Importable';
// import CreateCreditNote from './commands/CreateCreditNote.service';
// @Service()
// export class CreditNotesImportable extends Importable {
// @Inject()
// private createCreditNoteImportable: CreateCreditNote;
// /**
// * Importing to account service.
// * @param {number} tenantId
// * @param {IAccountCreateDTO} createAccountDTO
// * @returns
// */
// public importable(
// tenantId: number,
// createAccountDTO: ICreditNoteNewDTO,
// trx?: Knex.Transaction
// ) {
// return this.createCreditNoteImportable.newCreditNote(
// 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 [];
// }
// }

View File

@@ -0,0 +1,82 @@
// @ts-nocheck
import { Injectable } from '@nestjs/common';
import { InventoryTransactionsService } from '@/modules/InventoryCost/commands/InventoryTransactions.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { CreditNote } from '../models/CreditNote';
import { Knex } from 'knex';
@Injectable()
export class CreditNoteInventoryTransactions {
constructor(
private readonly inventoryService: InventoryTransactionsService,
private readonly itemsEntriesService: ItemsEntriesService,
) {}
/**
* Creates credit note inventory transactions.
* @param {number} tenantId
* @param {ICreditNote} creditNote
*/
public createInventoryTransactions = async (
creditNote: CreditNote,
trx?: Knex.Transaction,
): Promise<void> => {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(creditNote.entries);
const transaction = {
transactionId: creditNote.id,
transactionType: 'CreditNote',
transactionNumber: creditNote.creditNoteNumber,
exchangeRate: creditNote.exchangeRate,
date: creditNote.creditNoteDate,
direction: 'IN',
entries: inventoryEntries,
createdAt: creditNote.createdAt,
warehouseId: creditNote.warehouseId,
};
// Writes inventory tranactions.
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
transaction,
false,
trx,
);
};
/**
* Edits vendor credit associated inventory transactions.
* @param {number} tenantId
* @param {number} creditNoteId
* @param {ICreditNote} creditNote
* @param {Knex.Transactions} trx
*/
public editInventoryTransactions = async (
creditNoteId: number,
creditNote: CreditNote,
trx?: Knex.Transaction,
): Promise<void> => {
// Deletes inventory transactions.
await this.deleteInventoryTransactions(creditNoteId, trx);
// Re-write inventory transactions.
await this.createInventoryTransactions(creditNote, trx);
};
/**
* Deletes credit note associated inventory transactions.
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
* @param {Knex.Transaction} trx -
*/
public deleteInventoryTransactions = async (
creditNoteId: number,
trx?: Knex.Transaction,
): Promise<void> => {
// Deletes the inventory transactions by the given reference id and type.
await this.inventoryService.deleteInventoryTransactions(
creditNoteId,
'CreditNote',
trx,
);
};
}

View File

@@ -0,0 +1,127 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
ICreditNoteDeletedPayload,
ICreditNoteDeletingPayload,
} from '../types/CreditNotes.types';
import { ERRORS } from '../constants';
import { CreditNote } from '../models/CreditNote';
import { CreditNoteAppliedInvoice } from '../../CreditNotesApplyInvoice/models/CreditNoteAppliedInvoice';
import { RefundCreditNote as RefundCreditNoteModel } from '../../CreditNoteRefunds/models/RefundCreditNote';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { ServiceError } from '@/modules/Items/ServiceError';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DeleteCreditNoteService {
/**
* @param {UnitOfWork} uow - Unit of work.
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {typeof CreditNote} creditNoteModel - Credit note model.
* @param {typeof ItemEntry} itemEntryModel - Item entry model.
* @param {typeof CreditNoteAppliedInvoice} creditNoteAppliedInvoiceModel - Credit note applied invoice model.
* @param {typeof RefundCreditNote} refundCreditNoteModel - Refund credit note model.
*/
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
@Inject(CreditNote.name)
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
@Inject(ItemEntry.name)
private readonly itemEntryModel: TenantModelProxy<typeof ItemEntry>,
@Inject(CreditNoteAppliedInvoice.name)
private readonly creditNoteAppliedInvoiceModel: TenantModelProxy<
typeof CreditNoteAppliedInvoice
>,
@Inject(RefundCreditNoteModel.name)
private readonly refundCreditNoteModel: TenantModelProxy<
typeof RefundCreditNoteModel
>,
) {}
/**
* Deletes the given credit note transactions.
* @param {number} creditNoteId
* @returns {Promise<void>}
*/
public async deleteCreditNote(creditNoteId: number): Promise<void> {
// Retrieve the credit note or throw not found service error.
const oldCreditNote = await this.creditNoteModel()
.query()
.findById(creditNoteId)
.throwIfNotFound();
// Validate credit note has no refund transactions.
await this.validateCreditNoteHasNoRefundTransactions(creditNoteId);
// Validate credit note has no applied invoices transactions.
await this.validateCreditNoteHasNoApplyInvoiceTransactions(creditNoteId);
// Deletes the credit note transactions under unit-of-work transaction.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteDeleting` event.
await this.eventPublisher.emitAsync(events.creditNote.onDeleting, {
trx,
oldCreditNote,
} as ICreditNoteDeletingPayload);
// Deletes the associated credit note entries.
await this.itemEntryModel()
.query(trx)
.where('reference_id', creditNoteId)
.where('reference_type', 'CreditNote')
.delete();
// Deletes the credit note transaction.
await this.creditNoteModel().query(trx).findById(creditNoteId).delete();
// Triggers `onCreditNoteDeleted` event.
await this.eventPublisher.emitAsync(events.creditNote.onDeleted, {
oldCreditNote,
creditNoteId,
trx,
} as ICreditNoteDeletedPayload);
});
}
/**
* Validates credit note has no associated refund transactions.
* @param {number} creditNoteId
* @returns {Promise<void>}
*/
private async validateCreditNoteHasNoRefundTransactions(
creditNoteId: number,
): Promise<void> {
const refundTransactions = await this.refundCreditNoteModel()
.query()
.where('creditNoteId', creditNoteId);
if (refundTransactions.length > 0) {
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS);
}
}
/**
* Validate credit note has no associated applied invoices transactions.
* @param {number} creditNoteId - Credit note id.
* @returns {Promise<void>}
*/
private async validateCreditNoteHasNoApplyInvoiceTransactions(
creditNoteId: number,
): Promise<void> {
const appliedTransactions = await this.creditNoteAppliedInvoiceModel()
.query()
.where('creditNoteId', creditNoteId);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_APPLIED_INVOICES);
}
}
}

View File

@@ -0,0 +1,108 @@
import { Inject, Injectable } from '@nestjs/common';
import {
ICreditNoteEditedPayload,
ICreditNoteEditingPayload,
} from '../types/CreditNotes.types';
import { Knex } from 'knex';
import { CreditNote } from '../models/CreditNote';
import { Contact } from '../../Contacts/models/Contact';
import { ItemsEntriesService } from '../../Items/ItemsEntries.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { CommandCreditNoteDTOTransform } from './CommandCreditNoteDTOTransform.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { EditCreditNoteDto } from '../dtos/CreditNote.dto';
@Injectable()
export class EditCreditNoteService {
/**
* @param {typeof CreditNote} creditNoteModel - The credit note model.
* @param {typeof Contact} contactModel - The contact model.
* @param {CommandCreditNoteDTOTransform} commandCreditNoteDTOTransform - The command credit note DTO transform service.
* @param {ItemsEntriesService} itemsEntriesService - The items entries service.
* @param {EventEmitter2} eventPublisher - The event publisher.
* @param {UnitOfWork} uow - The unit of work.
*/
constructor(
@Inject(CreditNote.name)
private creditNoteModel: TenantModelProxy<typeof CreditNote>,
@Inject(Contact.name)
private contactModel: TenantModelProxy<typeof Contact>,
private commandCreditNoteDTOTransform: CommandCreditNoteDTOTransform,
private itemsEntriesService: ItemsEntriesService,
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
) {}
/**
* Edits the given credit note.
* @param {ICreditNoteEditDTO} creditNoteEditDTO -
*/
public async editCreditNote(
creditNoteId: number,
creditNoteEditDTO: EditCreditNoteDto,
) {
// Retrieve the sale invoice or throw not found service error.
const oldCreditNote = await this.creditNoteModel()
.query()
.findById(creditNoteId)
.throwIfNotFound();
// Validate customer existance.
const customer = await this.contactModel()
.query()
.findById(creditNoteEditDTO.customerId);
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
creditNoteEditDTO.entries,
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
creditNoteEditDTO.entries,
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
creditNoteId,
'CreditNote',
creditNoteEditDTO.entries,
);
// Transformes the given DTO to storage layer data.
const creditNoteModel =
await this.commandCreditNoteDTOTransform.transformCreateEditDTOToModel(
creditNoteEditDTO,
customer.currencyCode,
oldCreditNote,
);
// Sales the credit note transactions with associated entries.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteEditing` event.
await this.eventPublisher.emitAsync(events.creditNote.onEditing, {
creditNoteEditDTO,
oldCreditNote,
trx,
} as ICreditNoteEditingPayload);
// Saves the credit note graph to the storage.
const creditNote = await this.creditNoteModel()
.query(trx)
.upsertGraph({
id: creditNoteId,
...creditNoteModel,
});
// Triggers `onCreditNoteEdited` event.
await this.eventPublisher.emitAsync(events.creditNote.onEdited, {
trx,
oldCreditNote,
creditNoteId,
creditNote,
creditNoteEditDTO,
} as ICreditNoteEditedPayload);
return creditNote;
});
}
}

View File

@@ -0,0 +1,87 @@
import {
ICreditNoteOpenedPayload,
ICreditNoteOpeningPayload,
} from '../types/CreditNotes.types';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { ERRORS } from '../constants';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { CreditNote } from '../models/CreditNote';
import { events } from '@/common/events/events';
import { ServiceError } from '@/modules/Items/ServiceError';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class OpenCreditNoteService {
/**
* @param {EventEmitter2} eventPublisher - The event publisher.
* @param {UnitOfWork} uow - The unit of work.
* @param {typeof CreditNote} creditNoteModel - The credit note model.
*/
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(CreditNote.name)
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
) {}
/**
* Opens the given credit note.
* @param {number} creditNoteId -
* @returns {Promise<CreditNote>}
*/
public openCreditNote = async (creditNoteId: number): Promise<CreditNote> => {
// Retrieve the sale invoice or throw not found service error.
const oldCreditNote = await this.creditNoteModel()
.query()
.findById(creditNoteId)
.throwIfNotFound();
// Throw service error if the credit note is already open.
this.throwErrorIfAlreadyOpen(oldCreditNote);
// Triggers `onCreditNoteOpen` event.
this.eventPublisher.emitAsync(events.creditNote.onOpen, {
creditNoteId,
oldCreditNote,
});
// Sales the credit note transactions with associated entries.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
const eventPayload = {
oldCreditNote,
trx,
} as ICreditNoteOpeningPayload;
// Triggers `onCreditNoteOpening` event.
await this.eventPublisher.emitAsync(
events.creditNote.onOpening,
eventPayload,
);
// Saves the credit note graph to the storage.
const creditNote = await this.creditNoteModel()
.query(trx)
.updateAndFetchById(creditNoteId, {
openedAt: new Date(),
});
// Triggers `onCreditNoteOpened` event.
await this.eventPublisher.emitAsync(events.creditNote.onOpened, {
...eventPayload,
creditNote,
} as ICreditNoteOpenedPayload);
return creditNote;
});
};
/**
* Throws an error if the given credit note is already open.
* @param {CreditNote} creditNote -
*/
public throwErrorIfAlreadyOpen = (creditNote: CreditNote) => {
if (creditNote.openedAt) {
throw new ServiceError(ERRORS.CREDIT_NOTE_ALREADY_OPENED);
}
};
}