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,79 @@
import { Injectable } from '@nestjs/common';
import { CreateCreditNoteService } from './commands/CreateCreditNote.service';
import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service';
import { EditCreditNoteService } from './commands/EditCreditNote.service';
import { OpenCreditNoteService } from './commands/OpenCreditNote.service';
import { GetCreditNotePdf } from './queries/GetCreditNotePdf.serivce';
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
import { GetCreditNotesService } from './queries/GetCreditNotes.service';
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
@Injectable()
export class CreditNoteApplication {
constructor(
private readonly createCreditNoteService: CreateCreditNoteService,
private readonly editCreditNoteService: EditCreditNoteService,
private readonly openCreditNoteService: OpenCreditNoteService,
private readonly deleteCreditNoteService: DeleteCreditNoteService,
private readonly getCreditNotePdfService: GetCreditNotePdf,
private readonly getCreditNotesService: GetCreditNotesService,
) {}
/**
* Creates a new credit note.
* @param {CreateCreditNoteDto} creditNoteDTO
* @returns {Promise<CreditNote>}
*/
createCreditNote(creditNoteDTO: CreateCreditNoteDto) {
return this.createCreditNoteService.creditCreditNote(creditNoteDTO);
}
/**
* Edits a credit note.
* @param {number} creditNoteId
* @param {EditCreditNoteDto} creditNoteDTO
* @returns {Promise<CreditNote>}
*/
editCreditNote(creditNoteId: number, creditNoteDTO: EditCreditNoteDto) {
return this.editCreditNoteService.editCreditNote(
creditNoteId,
creditNoteDTO,
);
}
/**
* Opens a credit note.
* @param {number} creditNoteId
* @returns {Promise<CreditNote>}
*/
openCreditNote(creditNoteId: number) {
return this.openCreditNoteService.openCreditNote(creditNoteId);
}
/**
* Deletes a credit note.
* @param {number} creditNoteId
* @returns {Promise<CreditNote>}
*/
deleteCreditNote(creditNoteId: number) {
return this.deleteCreditNoteService.deleteCreditNote(creditNoteId);
}
/**
* Retrieves the PDF for a credit note.
* @param {number} creditNoteId
* @returns {Promise<string>}
*/
getCreditNotePdf(creditNoteId: number) {
return this.getCreditNotePdfService.getCreditNotePdf(creditNoteId);
}
/**
* Retrieves the credit notes list.
* @param {ICreditNotesQueryDTO} creditNotesQuery
* @returns {Promise<GetCreditNotesResponse>}
*/
getCreditNotes(creditNotesQuery: ICreditNotesQueryDTO) {
return this.getCreditNotesService.getCreditNotesList(creditNotesQuery);
}
}

View File

@@ -0,0 +1,54 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
} from '@nestjs/common';
import { CreditNoteApplication } from './CreditNoteApplication.service';
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
import { ApiTags } from '@nestjs/swagger';
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
@Controller('credit-notes')
@ApiTags('credit-notes')
export class CreditNotesController {
/**
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
*/
constructor(private creditNoteApplication: CreditNoteApplication) {}
@Post()
createCreditNote(@Body() creditNoteDTO: CreateCreditNoteDto) {
return this.creditNoteApplication.createCreditNote(creditNoteDTO);
}
@Get()
getCreditNotes(@Query() creditNotesQuery: ICreditNotesQueryDTO) {
return this.creditNoteApplication.getCreditNotes(creditNotesQuery);
}
@Put(':id')
editCreditNote(
@Param('id') creditNoteId: number,
@Body() creditNoteDTO: EditCreditNoteDto,
) {
return this.creditNoteApplication.editCreditNote(
creditNoteId,
creditNoteDTO,
);
}
@Put(':id/open')
openCreditNote(@Param('id') creditNoteId: number) {
return this.creditNoteApplication.openCreditNote(creditNoteId);
}
@Delete(':id')
deleteCreditNote(@Param('id') creditNoteId: number) {
return this.creditNoteApplication.deleteCreditNote(creditNoteId);
}
}

View File

@@ -0,0 +1,72 @@
import { Module } from '@nestjs/common';
import { CreateCreditNoteService } from './commands/CreateCreditNote.service';
import { CommandCreditNoteDTOTransform } from './commands/CommandCreditNoteDTOTransform.service';
import { EditCreditNoteService } from './commands/EditCreditNote.service';
import { OpenCreditNoteService } from './commands/OpenCreditNote.service';
import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service';
import { CreditNoteAutoIncrementService } from './commands/CreditNoteAutoIncrement.service';
import { CreditNoteApplication } from './CreditNoteApplication.service';
import { CreditNotesController } from './CreditNotes.controller';
import { GetCreditNoteState } from './queries/GetCreditNoteState.service';
import { GetCreditNotePdf } from './queries/GetCreditNotePdf.serivce';
import { ItemsModule } from '../Items/items.module';
import { BranchesModule } from '../Branches/Branches.module';
import { WarehousesModule } from '../Warehouses/Warehouses.module';
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
import { TemplateInjectableModule } from '../TemplateInjectable/TemplateInjectable.module';
import { GetCreditNote } from './queries/GetCreditNote.service';
import { CreditNoteBrandingTemplate } from './queries/CreditNoteBrandingTemplate.service';
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
import { CreditNoteGLEntries } from './commands/CreditNoteGLEntries';
import { CreditNoteGLEntriesSubscriber } from './subscribers/CreditNoteGLEntriesSubscriber';
import { LedgerModule } from '../Ledger/Ledger.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { GetCreditNotesService } from './queries/GetCreditNotes.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
@Module({
imports: [
ItemsModule,
BranchesModule,
WarehousesModule,
PdfTemplatesModule,
ChromiumlyTenancyModule,
TemplateInjectableModule,
AutoIncrementOrdersModule,
LedgerModule,
AccountsModule,
DynamicListModule
],
providers: [
CreateCreditNoteService,
GetCreditNote,
CommandCreditNoteDTOTransform,
EditCreditNoteService,
OpenCreditNoteService,
DeleteCreditNoteService,
GetCreditNotePdf,
GetCreditNotesService,
CreditNoteAutoIncrementService,
GetCreditNoteState,
CreditNoteApplication,
CreditNoteBrandingTemplate,
CreditNoteGLEntries,
CreditNoteGLEntriesSubscriber,
],
exports: [
CreateCreditNoteService,
GetCreditNote,
CommandCreditNoteDTOTransform,
EditCreditNoteService,
OpenCreditNoteService,
DeleteCreditNoteService,
GetCreditNotePdf,
CreditNoteAutoIncrementService,
GetCreditNoteState,
CreditNoteApplication,
CreditNoteBrandingTemplate,
],
controllers: [CreditNotesController],
})
export class CreditNotesModule {}

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);
}
};
}

View File

@@ -0,0 +1,132 @@
export const ERRORS = {
CREDIT_NOTE_NOT_FOUND: 'CREDIT_NOTE_NOT_FOUND',
REFUND_CREDIT_NOTE_NOT_FOUND: 'REFUND_CREDIT_NOTE_NOT_FOUND',
CREDIT_NOTE_ALREADY_OPENED: 'CREDIT_NOTE_ALREADY_OPENED',
ACCOUNT_INVALID_TYPE: 'ACCOUNT_INVALID_TYPE',
CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT: 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT',
INVOICES_HAS_NO_REMAINING_AMOUNT: 'INVOICES_HAS_NO_REMAINING_AMOUNT',
CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND:
'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND',
CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS',
CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES',
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES',
};
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'credit_note.view.draft',
slug: 'draft',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'credit_note.view.published',
slug: 'published',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'published',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'credit_note.view.open',
slug: 'open',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'open',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'credit_note.view.closed',
slug: 'closed',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'closed',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const defaultCreditNoteBrandingAttributes = {
// # Colors
primaryColor: '',
secondaryColor: '',
// # Company logo
showCompanyLogo: true,
companyLogoKey: '',
companyLogoUri: '',
// # Company name
companyName: 'Bigcapital Technology, Inc.',
// # Customer address
showCustomerAddress: true,
customerAddress: '',
// # Company address
showCompanyAddress: true,
companyAddress: '',
billedToLabel: 'Billed To',
// Total
total: '$1000.00',
totalLabel: 'Total',
showTotal: true,
// Subtotal
subtotal: '1000/00',
subtotalLabel: 'Subtotal',
showSubtotal: true,
// Customer note
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',
// Terms & conditions
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',
},
],
// Credit note number.
showCreditNoteNumber: true,
creditNoteNumberLabel: 'Credit Note Number',
creditNoteNumebr: '346D3D40-0001',
// Credit note date.
creditNoteDate: 'September 3, 2024',
showCreditNoteDate: true,
creditNoteDateLabel: 'Credit Note Date',
};

View File

@@ -0,0 +1,143 @@
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsInt,
IsNumber,
IsOptional,
IsPositive,
IsString,
Min,
ValidateNested,
} from 'class-validator';
enum DiscountType {
Percentage = 'percentage',
Amount = 'amount',
}
export class CreditNoteEntryDto extends ItemEntryDto {}
class AttachmentDto {
@IsString()
key: string;
}
export class CommandCreditNoteDto {
@IsInt()
@ApiProperty({ example: 1, description: 'The customer ID' })
customerId: number;
@IsOptional()
@IsPositive()
@ApiProperty({ example: 3.43, description: 'The exchange rate' })
exchangeRate?: number;
@IsDate()
@Type(() => Date)
@ApiProperty({ example: '2021-09-01', description: 'The credit note date' })
creditNoteDate: Date;
@IsOptional()
@IsString()
@ApiProperty({ example: '123', description: 'The reference number' })
referenceNo?: string;
@IsOptional()
@IsString()
@ApiProperty({ example: '123', description: 'The credit note number' })
creditNoteNumber?: string;
@IsOptional()
@IsString()
@ApiProperty({ example: '123', description: 'The note' })
note?: string;
@IsOptional()
@IsString()
@ApiProperty({ example: '123', description: 'The terms and conditions' })
termsConditions?: string;
@IsBoolean()
@ApiProperty({
example: false,
description: 'The credit note is open',
})
open: boolean = false;
@IsOptional()
@IsInt()
@ApiProperty({
example: 1,
description: 'The warehouse ID',
})
warehouseId?: number;
@IsOptional()
@IsInt()
@ApiProperty({
example: 1,
description: 'The branch ID',
})
branchId?: number;
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreditNoteEntryDto)
@ArrayMinSize(1)
@ApiProperty({
example: [
{
itemId: 1,
quantity: 1,
rate: 10,
taxRateId: 1,
},
],
description: 'The credit note entries',
})
entries: CreditNoteEntryDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AttachmentDto)
attachments?: AttachmentDto[];
@IsOptional()
@IsInt()
@ApiProperty({
example: 1,
description: 'The pdf template ID',
})
pdfTemplateId?: number;
@IsOptional()
@IsNumber()
@ApiProperty({
example: 10,
description: 'The discount amount',
})
discount?: number;
@IsOptional()
@IsEnum(DiscountType)
@ApiProperty({
example: 'percentage',
description: 'The discount type',
enum: DiscountType,
})
discountType?: DiscountType;
@IsOptional()
@IsNumber()
adjustment?: number;
}
export class CreateCreditNoteDto extends CommandCreditNoteDto {}
export class EditCreditNoteDto extends CommandCreditNoteDto {}

View File

@@ -0,0 +1,12 @@
import {
IsDate,
IsInt,
IsNumber,
IsOptional,
IsPositive,
IsString,
} from 'class-validator';
export class RefundCreditNoteDto {
}

View File

@@ -0,0 +1,437 @@
import { DiscountType } from '@/common/types/Discount';
import { BaseModel } from '@/models/Model';
import { Branch } from '@/modules/Branches/models/Branch.model';
import { Customer } from '@/modules/Customers/models/Customer';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
import { mixin, Model, raw } from 'objection';
export class CreditNote extends TenantBaseModel {
public amount: number;
public exchangeRate: number;
public openedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
public refundedAmount: number;
public invoicesAmount: number;
public creditNoteDate: Date;
public creditNoteNumber: string;
public referenceNo: string;
public currencyCode: string;
public customerId: number;
public userId: number;
public branchId: number;
public warehouseId: number;
public customer!: Customer;
public entries!: ItemEntry[];
public branch!: Branch;
public warehouse!: Warehouse;
public createdAt!: Date | string;
public updatedAt!: Date | string;
/**
* Table name
*/
static get tableName() {
return 'credit_notes';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'localAmount',
'isDraft',
'isPublished',
'isOpen',
'isClosed',
'creditsRemaining',
'creditsUsed',
'subtotal',
'subtotalLocal',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'total',
'totalLocal',
'adjustmentLocal',
];
}
/**
* Credit note amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Credit note subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Credit note subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.subtotal * this.exchangeRate;
}
/**
* 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}
*/
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;
}
/**
* Adjustment amount in local currency.
* @returns {number}
*/
get adjustmentLocal() {
return this.adjustment ? this.adjustment * this.exchangeRate : null;
}
/**
* Credit note total.
* @returns {number}
*/
get total() {
return this.subtotal - this.discountAmount + this.adjustment;
}
/**
* Credit note total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/**
* Detarmines whether the credit note is draft.
* @returns {boolean}
*/
get isDraft() {
return !this.openedAt;
}
/**
* Detarmines whether vendor credit is published.
* @returns {boolean}
*/
get isPublished() {
return !!this.openedAt;
}
/**
* Detarmines whether the credit note is open.
* @return {boolean}
*/
get isOpen() {
return !!this.openedAt && this.creditsRemaining > 0;
}
/**
* Detarmines whether the credit note is closed.
* @return {boolean}
*/
get isClosed() {
return this.openedAt && this.creditsRemaining === 0;
}
/**
* Retrieve the credits remaining.
*/
get creditsRemaining() {
return Math.max(this.amount - this.refundedAmount - this.invoicesAmount, 0);
}
/**
* Retrieve the credits used.
*/
get creditsUsed() {
return this.refundedAmount + this.invoicesAmount;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the credit notes in draft status.
*/
draft(query) {
query.where('opened_at', null);
},
/**
* Filters the.
*/
published(query) {
query.whereNot('opened_at', null);
},
/**
* Filters the open credit notes.
*/
open(query) {
query
.where(
raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) <
COALESCE(AMOUNT)`),
)
.modify('published');
},
/**
* Filters the closed credit notes.
*/
closed(query) {
query
.where(
raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) =
COALESCE(AMOUNT)`),
)
.modify('published');
},
/**
* Status filter.
*/
filterByStatus(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'published':
query.modify('published');
break;
case 'open':
default:
query.modify('open');
break;
case 'closed':
query.modify('closed');
break;
}
},
/**
*
*/
sortByStatus(query, order) {
query.orderByRaw(
`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) = COALESCE(AMOUNT) ${order}`,
);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const {
AccountTransaction,
} = require('../../Accounts/models/AccountTransaction.model');
const {
ItemEntry,
} = require('../../TransactionItemEntry/models/ItemEntry');
const { Customer } = require('../../Customers/models/Customer');
const { Branch } = require('../../Branches/models/Branch.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
const {
PdfTemplateModel,
} = require('../../PdfTemplate/models/PdfTemplate');
return {
/**
* Credit note associated entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry,
join: {
from: 'credit_notes.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'CreditNote');
builder.orderBy('index', 'ASC');
},
},
/**
* Belongs to customer model.
*/
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer,
join: {
from: 'credit_notes.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'Customer');
},
},
/**
* Credit note associated GL entries.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'credit_notes.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'CreditNote');
},
},
/**
* Credit note may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch,
join: {
from: 'credit_notes.branchId',
to: 'branches.id',
},
},
/**
* Credit note may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse,
join: {
from: 'credit_notes.warehouseId',
to: 'warehouses.id',
},
},
/**
* Credit note may has many attached attachments.
*/
attachments: {
relation: Model.ManyToManyRelation,
modelClass: Document,
join: {
from: 'credit_notes.id',
through: {
from: 'document_links.modelId',
to: 'document_links.documentId',
},
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'CreditNote');
},
},
/**
* Credit note may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplateModel,
join: {
from: 'credit_notes.pdfTemplateId',
to: 'pdf_templates.id',
},
},
};
}
// /**
// * Sale invoice meta.
// */
// static get meta() {
// return CreditNoteMeta;
// }
// /**
// * Retrieve the default custom views, roles and columns.
// */
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model searchable.
*/
static get searchable() {
return true;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'credit_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.
* @returns {boolean}
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { defaultCreditNoteBrandingAttributes } from '../constants';
import { GetPdfTemplateService } from '../../PdfTemplate/queries/GetPdfTemplate.service';
import { GetOrganizationBrandingAttributesService } from '../../PdfTemplate/queries/GetOrganizationBrandingAttributes.service';
import { mergePdfTemplateWithDefaultAttributes } from '../../SaleInvoices/utils';
@Injectable()
export class CreditNoteBrandingTemplate {
constructor(
private getPdfTemplateService: GetPdfTemplateService,
private getOrgBrandingAttributes: GetOrganizationBrandingAttributesService,
) {}
/**
* Retrieves the credit note branding template.
* @param {number} templateId
* @returns {}
*/
public async getCreditNoteBrandingTemplate(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 common organization branding attrs.
const organizationBrandingAttrs = {
...defaultCreditNoteBrandingAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
brandingTemplateAttrs,
organizationBrandingAttrs,
);
return {
...template,
attributes,
};
}
}

View File

@@ -0,0 +1,194 @@
import { AttachmentTransformer } from "@/modules/Attachments/Attachment.transformer";
import { ItemEntryTransformer } from "@/modules/TransactionItemEntry/ItemEntry.transformer";
import { Transformer } from "@/modules/Transformer/Transformer";
export class CreditNoteTransformer extends Transformer {
/**
* Include these attributes to sale credit note object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedCreditsRemaining',
'formattedCreditNoteDate',
'formattedCreatedAt',
'formattedCreatedAt',
'formattedAmount',
'formattedCreditsUsed',
'formattedSubtotal',
'discountAmountFormatted',
'discountAmountLocalFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'entries',
'attachments',
];
};
/**
* Retrieve formatted credit note date.
* @param {ICreditNote} credit
* @returns {String}
*/
protected formattedCreditNoteDate = (credit): string => {
return this.formatDate(credit.creditNoteDate);
};
/**
* Retrieve formatted created at date.
* @param credit
* @returns {string}
*/
protected formattedCreatedAt = (credit): string => {
return this.formatDate(credit.createdAt);
};
/**
* Retrieve formatted invoice amount.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedAmount = (credit): string => {
return this.formatNumber(credit.amount, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieve formatted credits remaining.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedCreditsRemaining = (credit) => {
return this.formatNumber(credit.creditsRemaining, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieve formatted credits used.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedCreditsUsed = (credit) => {
return this.formatNumber(credit.creditsUsed, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the formatted subtotal.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedSubtotal = (credit): string => {
return this.formatNumber(credit.amount, { money: false });
};
/**
* Retrieves formatted discount amount.
* @param credit
* @returns {string}
*/
protected discountAmountFormatted = (credit): string => {
return this.formatNumber(credit.discountAmount, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount amount in local currency.
* @param {ICreditNote} credit
* @returns {string}
*/
protected discountAmountLocalFormatted = (credit): string => {
return this.formatNumber(credit.discountAmountLocal, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param credit
* @returns {string}
*/
protected discountPercentageFormatted = (credit): string => {
return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
};
/**
* Retrieves formatted adjustment amount.
* @param credit
* @returns {string}
*/
protected adjustmentFormatted = (credit): string => {
return this.formatMoney(credit.adjustment, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted adjustment amount in local currency.
* @param {ICreditNote} credit
* @returns {string}
*/
protected adjustmentLocalFormatted = (credit): string => {
return this.formatNumber(credit.adjustmentLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted total.
* @param credit
* @returns {string}
*/
protected totalFormatted = (credit): string => {
return this.formatNumber(credit.total, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the formatted total in local currency.
* @param credit
* @returns {string}
*/
protected totalLocalFormatted = (credit): string => {
return this.formatNumber(credit.totalLocal, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the entries of the credit note.
* @param {ICreditNote} credit
* @returns {}
*/
protected entries = (credit) => {
return this.item(credit.entries, new ItemEntryTransformer(), {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the credit note attachments.
* @param {ISaleInvoice} invoice
* @returns
*/
protected attachments = (creditNote) => {
return this.item(creditNote.attachments, new AttachmentTransformer());
};
}

View File

@@ -0,0 +1,38 @@
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { ERRORS } from '../constants';
import { CreditNoteTransformer } from './CreditNoteTransformer';
import { Inject, Injectable } from '@nestjs/common';
import { CreditNote } from '../models/CreditNote';
import { ServiceError } from '@/modules/Items/ServiceError';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetCreditNote {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(CreditNote.name)
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
) {}
/**
* Retrieve the credit note graph.
* @param {number} creditNoteId
*/
public async getCreditNote(creditNoteId: number) {
// Retrieve the vendor credit model graph.
const creditNote = await this.creditNoteModel()
.query()
.findById(creditNoteId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('branch')
.withGraphFetched('attachments');
if (!creditNote) {
throw new ServiceError(ERRORS.CREDIT_NOTE_NOT_FOUND);
}
// Transforms the credit note model to POJO.
return this.transformer.transform(creditNote, new CreditNoteTransformer());
}
}

View File

@@ -0,0 +1,111 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetCreditNote } from './GetCreditNote.service';
import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate.service';
import { transformCreditNoteToPdfTemplate } from '../utils';
import { CreditNote } from '../models/CreditNote';
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 { CreditNotePdfTemplateAttributes } from '../types/CreditNotes.types';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetCreditNotePdf {
/**
* @param {ChromiumlyTenancy} chromiumlyTenancy - Chromiumly tenancy service.
* @param {TemplateInjectable} templateInjectable - Template injectable service.
* @param {GetCreditNote} getCreditNoteService - Get credit note service.
* @param {CreditNoteBrandingTemplate} creditNoteBrandingTemplate - Credit note branding template service.
* @param {EventEmitter2} eventPublisher - Event publisher service.
* @param {typeof CreditNote} creditNoteModel - Credit note model.
* @param {typeof PdfTemplateModel} pdfTemplateModel - Pdf template model.
*/
constructor(
private readonly chromiumlyTenancy: ChromiumlyTenancy,
private readonly templateInjectable: TemplateInjectable,
private readonly getCreditNoteService: GetCreditNote,
private readonly creditNoteBrandingTemplate: CreditNoteBrandingTemplate,
private readonly eventPublisher: EventEmitter2,
@Inject(CreditNote.name)
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
@Inject(PdfTemplateModel.name)
private readonly pdfTemplateModel: TenantModelProxy<
typeof PdfTemplateModel
>,
) {}
/**
* Retrieves sale invoice pdf content.
* @param {number} creditNoteId - Credit note id.
* @returns {Promise<[Buffer, string]>}
*/
public async getCreditNotePdf(
creditNoteId: number,
): Promise<[Buffer, string]> {
const brandingAttributes =
await this.getCreditNoteBrandingAttributes(creditNoteId);
const htmlContent = await this.templateInjectable.render(
'modules/credit-note-standard',
brandingAttributes,
);
const filename = await this.getCreditNoteFilename(creditNoteId);
const document =
await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
const eventPayload = { creditNoteId };
// Triggers the `onCreditNotePdfViewed` event.
await this.eventPublisher.emitAsync(
events.creditNote.onPdfViewed,
eventPayload,
);
return [document, filename];
}
/**
* Retrieves the filename pdf document of the given credit note.
* @param {number} creditNoteId
* @returns {Promise<string>}
*/
public async getCreditNoteFilename(creditNoteId: number): Promise<string> {
const creditNote = await this.creditNoteModel()
.query()
.findById(creditNoteId);
return `Credit-${creditNote.creditNoteNumber}`;
}
/**
* Retrieves credit note branding attributes.
* @param {number} creditNoteId - The ID of the credit note.
* @returns {Promise<CreditNotePdfTemplateAttributes>} The credit note branding attributes.
*/
public async getCreditNoteBrandingAttributes(
creditNoteId: number,
): Promise<CreditNotePdfTemplateAttributes> {
const creditNote =
await this.getCreditNoteService.getCreditNote(creditNoteId);
// Retrieve the invoice template id of not found get the default template id.
const templateId =
creditNote.pdfTemplateId ??
(
await this.pdfTemplateModel().query().findOne({
resource: 'CreditNote',
default: true,
})
)?.id;
// Retrieves the credit note branding template.
const brandingTemplate =
await this.creditNoteBrandingTemplate.getCreditNoteBrandingTemplate(
templateId,
);
return {
...brandingTemplate.attributes,
...transformCreditNoteToPdfTemplate(creditNote),
};
}
}

View File

@@ -0,0 +1,27 @@
import { Inject, Injectable } from '@nestjs/common';
import { ICreditNoteState } from '../types/CreditNotes.types';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetCreditNoteState {
constructor(
@Inject(PdfTemplateModel.name)
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
) {}
/**
* Retrieves the create/edit initial state of the payment received.
* @return {Promise<ICreditNoteState>}
*/
public async getCreditNoteState(): Promise<ICreditNoteState> {
const defaultPdfTemplate = await this.pdfTemplateModel()
.query()
.findOne({ resource: 'CreditNote' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -0,0 +1,72 @@
import * as R from 'ramda';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
import {
GetCreditNotesResponse,
ICreditNotesQueryDTO,
} from '../types/CreditNotes.types';
import { CreditNote } from '../models/CreditNote';
import { CreditNoteTransformer } from './CreditNoteTransformer';
import { Inject } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetCreditNotesService {
constructor(
private readonly dynamicListService: DynamicListService,
private readonly transformer: TransformerInjectable,
@Inject(CreditNote.name)
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
) {}
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO = (filterDTO) => {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
};
/**
* Retrieves the paginated and filterable credit notes list.
* @param {number} tenantId -
* @param {ICreditNotesQueryDTO} creditNotesQuery -
*/
public async getCreditNotesList(
creditNotesQuery: ICreditNotesQueryDTO,
): Promise<GetCreditNotesResponse> {
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(creditNotesQuery);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
this.creditNoteModel(),
filter,
);
const { results, pagination } = await this.creditNoteModel()
.query()
.onBuild((builder) => {
builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
creditNotesQuery?.filterQuery?.(builder as any);
})
.pagination(filter.page - 1, filter.pageSize);
// Transforomes the credit notes to POJO.
const creditNotes = await this.transformer.transform(
results,
new CreditNoteTransformer(),
);
return {
creditNotes,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
}

View File

@@ -0,0 +1,29 @@
import { Transformer } from "@/modules/Transformer/Transformer";
export class RefundCreditNoteTransformer extends Transformer{
/**
* Includeded attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['formttedAmount', 'formattedDate'];
};
/**
* Formatted amount.
* @returns {string}
*/
protected formttedAmount = (item) => {
return this.formatNumber(item.amount, {
currencyCode: item.currencyCode,
});
};
/**
* Formatted date.
* @returns {string}
*/
protected formattedDate = (item) => {
return this.formatDate(item.date);
};
}

View File

@@ -0,0 +1,30 @@
// import { Service, Inject } from 'typedi';
// import events from '@/subscribers/events';
// import BaseCreditNotes from '../commands/CommandCreditNoteDTOTransform.service';
// import { ICreditNoteCreatedPayload } from '@/interfaces';
// @Service()
// export default class CreditNoteAutoSerialSubscriber {
// @Inject()
// creditNotesService: BaseCreditNotes;
// /**
// * Attaches events with handlers.
// */
// public attach(bus) {
// bus.subscribe(
// events.creditNote.onCreated,
// this.autoSerialIncrementOnceCreated
// );
// }
// /**
// * Auto serial increment once credit note created.
// * @param {ICreditNoteCreatedPayload} payload -
// */
// private autoSerialIncrementOnceCreated = async ({
// tenantId,
// }: ICreditNoteCreatedPayload) => {
// await this.creditNotesService.incrementSerialNumber(tenantId);
// };
// }

View File

@@ -0,0 +1,78 @@
import {
ICreditNoteCreatedPayload,
ICreditNoteDeletedPayload,
ICreditNoteEditedPayload,
ICreditNoteOpenedPayload,
} from '../types/CreditNotes.types';
import { CreditNoteGLEntries } from '../commands/CreditNoteGLEntries';
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
@Injectable()
export class CreditNoteGLEntriesSubscriber {
constructor(private readonly creditNoteGLEntries: CreditNoteGLEntries) {}
/**
* Writes the GL entries once the credit note transaction created or open.
* @param {ICreditNoteCreatedPayload|ICreditNoteOpenedPayload} payload -
*/
@OnEvent(events.creditNote.onCreated)
public async writeGlEntriesOnceCreditNoteCreated({
creditNote,
trx,
}: ICreditNoteCreatedPayload | ICreditNoteOpenedPayload) {
// Can't continue if the credit note is not published yet.
if (!creditNote.isPublished) return;
await this.creditNoteGLEntries.createVendorCreditGLEntries(
creditNote.id,
trx,
);
}
/**
* Writes the GL entries once the vendor credit transaction opened.
* @param {ICreditNoteOpenedPayload} payload
*/
@OnEvent(events.creditNote.onOpened)
public async writeGLEntriesOnceCreditNoteOpened({
creditNote,
trx,
}: ICreditNoteOpenedPayload) {
await this.creditNoteGLEntries.createVendorCreditGLEntries(
creditNote.id,
trx,
);
}
/**
* Reverts GL entries once credit note deleted.
*/
@OnEvent(events.creditNote.onDeleted)
public async revertGLEntriesOnceCreditNoteDeleted({
oldCreditNote,
creditNoteId,
trx,
}: ICreditNoteDeletedPayload) {
// Can't continue if the credit note is not published yet.
if (!oldCreditNote.isPublished) return;
await this.creditNoteGLEntries.revertVendorCreditGLEntries(creditNoteId);
}
/**
* Edits vendor credit associated GL entries once the transaction edited.
* @param {ICreditNoteEditedPayload} payload -
*/
@OnEvent(events.creditNote.onEdited)
public async editVendorCreditGLEntriesOnceEdited({
creditNote,
trx,
}: ICreditNoteEditedPayload) {
// Can't continue if the credit note is not published yet.
if (!creditNote.isPublished) return;
await this.creditNoteGLEntries.editVendorCreditGLEntries(creditNote.id, trx);
}
}

View File

@@ -0,0 +1,74 @@
import { Injectable } from '@nestjs/common';
import {
ICreditNoteCreatedPayload,
ICreditNoteDeletedPayload,
ICreditNoteEditedPayload,
} from '../types/CreditNotes.types';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { CreditNoteInventoryTransactions } from '../commands/CreditNotesInventoryTransactions';
@Injectable()
export class CreditNoteInventoryTransactionsSubscriber {
constructor(
private readonly inventoryTransactions: CreditNoteInventoryTransactions,
) {}
/**
* Writes inventory transactions once credit note created.
* @param {ICreditNoteCreatedPayload} payload -
* @returns {Promise<void>}
*/
@OnEvent(events.creditNote.onCreated)
@OnEvent(events.creditNote.onOpened)
public async writeInventoryTranscationsOnceCreated({
creditNote,
trx,
}: ICreditNoteCreatedPayload) {
// Can't continue if the credit note is open yet.
if (!creditNote.isOpen) return;
await this.inventoryTransactions.createInventoryTransactions(
creditNote,
trx,
);
}
/**
* Rewrites inventory transactions once credit note edited.
* @param {ICreditNoteEditedPayload} payload -
* @returns {Promise<void>}
*/
@OnEvent(events.creditNote.onEdited)
public async rewriteInventoryTransactionsOnceEdited({
creditNote,
trx,
}: ICreditNoteEditedPayload) {
// Can't continue if the credit note is open yet.
if (!creditNote.isOpen) return;
await this.inventoryTransactions.editInventoryTransactions(
creditNote.id,
creditNote,
trx,
);
}
/**
* Reverts inventory transactions once credit note deleted.
* @param {ICreditNoteDeletedPayload} payload -
*/
@OnEvent(events.creditNote.onDeleted)
public async revertInventoryTransactionsOnceDeleted({
oldCreditNote,
trx,
}: ICreditNoteDeletedPayload) {
// Can't continue if the credit note is open yet.
if (!oldCreditNote.isOpen) return;
await this.inventoryTransactions.deleteInventoryTransactions(
oldCreditNote.id,
trx,
);
}
}

View File

@@ -0,0 +1,48 @@
// import { Inject, Service } from 'typedi';
// import { ServiceError } from '@/exceptions';
// import TenancyService from '@/services/Tenancy/TenancyService';
// import events from '@/subscribers/events';
// import { ICustomerDeletingPayload } from '@/interfaces';
// import DeleteCustomerLinkedCreidtNote from '../commands/DeleteCustomerLinkedCreditNote.service';
// const ERRORS = {
// CUSTOMER_HAS_TRANSACTIONS: 'CUSTOMER_HAS_TRANSACTIONS',
// };
// @Service()
// export default class DeleteCustomerLinkedCreditSubscriber {
// @Inject()
// tenancy: TenancyService;
// @Inject()
// deleteCustomerLinkedCredit: DeleteCustomerLinkedCreidtNote;
// /**
// * Attaches events with handlers.
// * @param bus
// */
// public attach = (bus) => {
// bus.subscribe(
// events.customers.onDeleting,
// this.validateCustomerHasNoLinkedCreditsOnDeleting
// );
// };
// /**
// * Validate vendor has no associated credit transaction once the vendor deleting.
// * @param {IVendorEventDeletingPayload} payload -
// */
// public validateCustomerHasNoLinkedCreditsOnDeleting = async ({
// tenantId,
// customerId,
// }: ICustomerDeletingPayload) => {
// try {
// await this.deleteCustomerLinkedCredit.validateCustomerHasNoCreditTransaction(
// tenantId,
// customerId
// );
// } catch (error) {
// throw new ServiceError(ERRORS.CUSTOMER_HAS_TRANSACTIONS);
// }
// };
// }

View File

@@ -0,0 +1,61 @@
// import { Service, Inject } from 'typedi';
// import events from '@/subscribers/events';
// import RefundCreditNoteGLEntries from '../commands/RefundCreditNoteGLEntries';
// import {
// IRefundCreditNoteCreatedPayload,
// IRefundCreditNoteDeletedPayload,
// } from '@/interfaces';
// @Service()
// export default class RefundCreditNoteGLEntriesSubscriber {
// @Inject()
// refundCreditGLEntries: RefundCreditNoteGLEntries;
// /**
// * Attaches events with handlers.
// */
// public attach = (bus) => {
// bus.subscribe(
// events.creditNote.onRefundCreated,
// this.writeRefundCreditGLEntriesOnceCreated
// );
// bus.subscribe(
// events.creditNote.onRefundDeleted,
// this.revertRefundCreditGLEntriesOnceDeleted
// );
// };
// /**
// * Writes refund credit note GL entries once the transaction created.
// * @param {IRefundCreditNoteCreatedPayload} payload -
// */
// private writeRefundCreditGLEntriesOnceCreated = async ({
// trx,
// refundCreditNote,
// creditNote,
// tenantId,
// }: IRefundCreditNoteCreatedPayload) => {
// await this.refundCreditGLEntries.createRefundCreditGLEntries(
// tenantId,
// refundCreditNote.id,
// trx
// );
// };
// /**
// * Reverts refund credit note GL entries once the transaction deleted.
// * @param {IRefundCreditNoteDeletedPayload} payload -
// */
// private revertRefundCreditGLEntriesOnceDeleted = async ({
// trx,
// refundCreditId,
// oldRefundCredit,
// tenantId,
// }: IRefundCreditNoteDeletedPayload) => {
// await this.refundCreditGLEntries.revertRefundCreditGLEntries(
// tenantId,
// refundCreditId,
// trx
// );
// };
// }

View File

@@ -0,0 +1,62 @@
// import { Inject, Service } from 'typedi';
// import {
// IRefundCreditNoteCreatedPayload,
// IRefundCreditNoteDeletedPayload,
// } from '@/interfaces';
// import events from '@/subscribers/events';
// import RefundSyncCreditNoteBalance from '../commands/RefundSyncCreditNoteBalance';
// @Service()
// export default class RefundSyncCreditNoteBalanceSubscriber {
// @Inject()
// refundSyncCreditBalance: RefundSyncCreditNoteBalance;
// /**
// * Attaches events with handlers.
// */
// attach(bus) {
// bus.subscribe(
// events.creditNote.onRefundCreated,
// this.incrementRefundedAmountOnceRefundCreated
// );
// bus.subscribe(
// events.creditNote.onRefundDeleted,
// this.decrementRefundedAmountOnceRefundDeleted
// );
// return bus;
// }
// /**
// * Increment credit note refunded amount once associated refund transaction created.
// * @param {IRefundCreditNoteCreatedPayload} payload -
// */
// private incrementRefundedAmountOnceRefundCreated = async ({
// trx,
// refundCreditNote,
// tenantId,
// }: IRefundCreditNoteCreatedPayload) => {
// await this.refundSyncCreditBalance.incrementCreditNoteRefundAmount(
// tenantId,
// refundCreditNote.creditNoteId,
// refundCreditNote.amount,
// trx
// );
// };
// /**
// * Decrement credit note refunded amount once associated refuned transaction deleted.
// * @param {IRefundCreditNoteDeletedPayload} payload -
// */
// private decrementRefundedAmountOnceRefundDeleted = async ({
// trx,
// oldRefundCredit,
// tenantId,
// }: IRefundCreditNoteDeletedPayload) => {
// await this.refundSyncCreditBalance.decrementCreditNoteRefundAmount(
// tenantId,
// oldRefundCredit.creditNoteId,
// oldRefundCredit.amount,
// trx
// );
// };
// }

View File

@@ -0,0 +1,159 @@
import { Knex } from 'knex';
import { CreditNote } from '../models/CreditNote';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { CreateCreditNoteDto, EditCreditNoteDto } from '../dtos/CreditNote.dto';
export enum CreditNoteAction {
Create = 'Create',
Edit = 'Edit',
Delete = 'Delete',
View = 'View',
Refund = 'Refund',
}
export interface ICreditNoteDeletingPayload {
tenantId: number;
oldCreditNote: CreditNote;
trx: Knex.Transaction;
}
export interface ICreditNoteDeletedPayload {
tenantId: number;
oldCreditNote: CreditNote;
creditNoteId: number;
trx: Knex.Transaction;
}
export interface ICreditNoteEditingPayload {
oldCreditNote: CreditNote;
creditNoteEditDTO: EditCreditNoteDto;
trx?: Knex.Transaction;
}
export interface ICreditNoteEditedPayload {
trx?: Knex.Transaction;
oldCreditNote: CreditNote;
creditNote: CreditNote;
creditNoteEditDTO: EditCreditNoteDto;
}
export interface ICreditNoteCreatedPayload {
creditNoteDTO: CreateCreditNoteDto;
creditNote: CreditNote;
trx: Knex.Transaction;
}
export interface ICreditNoteCreatingPayload {
creditNoteDTO: CreateCreditNoteDto;
trx: Knex.Transaction;
}
export interface ICreditNoteOpeningPayload {
oldCreditNote: CreditNote;
trx: Knex.Transaction;
}
export interface ICreditNoteOpenedPayload {
creditNote: CreditNote;
oldCreditNote: CreditNote;
trx: Knex.Transaction;
}
export interface ICreditNotesQueryDTO extends IDynamicListFilter {
page: number;
pageSize: number;
searchKeyword?: string;
filterQuery?: (builder: Knex.QueryBuilder) => void;
}
export interface ICreditNoteRefundDTO {
fromAccountId: number;
amount: number;
exchangeRate?: number;
referenceNo: string;
description: string;
date: Date;
branchId?: number;
}
export type ICreditNoteGLCommonEntry = Pick<
ILedgerEntry,
| 'date'
| 'userId'
| 'currencyCode'
| 'exchangeRate'
| 'transactionType'
| 'transactionId'
| 'transactionNumber'
| 'referenceNumber'
| 'createdAt'
| 'indexGroup'
| 'credit'
| 'debit'
| 'branchId'
>;
export interface GetCreditNotesResponse {
creditNotes: CreditNote[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}
export interface CreditNotePdfTemplateAttributes {
// # Primary color
primaryColor: string;
secondaryColor: string;
// # Company logo
showCompanyLogo: boolean;
companyLogo: string;
// # Company name
companyName: string;
// # Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// # Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
total: string;
totalLabel: string;
showTotal: boolean;
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;
lines: Array<{
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}>;
showCreditNoteNumber: boolean;
creditNoteNumberLabel: string;
creditNoteNumebr: string;
creditNoteDate: string;
showCreditNoteDate: boolean;
creditNoteDateLabel: string;
}
export interface ICreditNoteState {
defaultTemplateId: number;
}

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import { CreditNotePdfTemplateAttributes, ICreditNote } from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
export const transformCreditNoteToPdfTemplate = (
creditNote: ICreditNote
): Partial<CreditNotePdfTemplateAttributes> => {
return {
creditNoteDate: creditNote.formattedCreditNoteDate,
creditNoteNumebr: creditNote.creditNoteNumber,
total: creditNote.formattedAmount,
subtotal: creditNote.formattedSubtotal,
lines: creditNote.entries?.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
total: entry.totalFormatted,
})),
customerNote: creditNote.note,
termsConditions: creditNote.termsConditions,
customerAddress: contactAddressTextFormat(creditNote.customer),
};
};