diff --git a/packages/server-nest/src/modules/Expenses/Expenses.module.ts b/packages/server-nest/src/modules/Expenses/Expenses.module.ts index cc11874c6..4c0d766de 100644 --- a/packages/server-nest/src/modules/Expenses/Expenses.module.ts +++ b/packages/server-nest/src/modules/Expenses/Expenses.module.ts @@ -10,6 +10,8 @@ import { ExpenseDTOTransformer } from './commands/CommandExpenseDTO.transformer' import { CommandExpenseValidator } from './commands/CommandExpenseValidator.service'; import { TenancyContext } from '../Tenancy/TenancyContext.service'; import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; +import { ExpensesWriteGLSubscriber } from './subscribers/ExpenseGLEntries.subscriber'; +import { ExpenseGLEntriesStorageService } from './subscribers/ExpenseGLEntriesStorage.sevice'; @Module({ imports: [], @@ -24,7 +26,9 @@ import { TransformerInjectable } from '../Transformer/TransformerInjectable.serv GetExpenseService, ExpensesApplication, TenancyContext, - TransformerInjectable + TransformerInjectable, + ExpensesWriteGLSubscriber, + ExpenseGLEntriesStorageService, ], }) export class ExpensesModule {} diff --git a/packages/server-nest/src/modules/Expenses/models/Expense.model.ts b/packages/server-nest/src/modules/Expenses/models/Expense.model.ts index 3f9a3a572..21050b171 100644 --- a/packages/server-nest/src/modules/Expenses/models/Expense.model.ts +++ b/packages/server-nest/src/modules/Expenses/models/Expense.model.ts @@ -8,6 +8,8 @@ import { Model, mixin, raw } from 'objection'; // import ModelSearchable from './ModelSearchable'; import moment from 'moment'; import { BaseModel } from '@/models/Model'; +import { ExpenseCategory } from './ExpenseCategory.model'; +import { Account } from '@/modules/Accounts/models/Account.model'; export class Expense extends BaseModel { // ModelSetting, @@ -32,6 +34,9 @@ export class Expense extends BaseModel { branchId!: number; createdAt!: Date; + categories!: ExpenseCategory[]; + paymentAccount!: Account; + /** * Table name */ diff --git a/packages/server-nest/src/modules/Expenses/models/ExpenseCategory.model.ts b/packages/server-nest/src/modules/Expenses/models/ExpenseCategory.model.ts index ecd7230fa..d5de7c02a 100644 --- a/packages/server-nest/src/modules/Expenses/models/ExpenseCategory.model.ts +++ b/packages/server-nest/src/modules/Expenses/models/ExpenseCategory.model.ts @@ -2,8 +2,11 @@ import { Model } from 'objection'; import { BaseModel } from '@/models/Model'; export class ExpenseCategory extends BaseModel { - amount!: number; - allocatedCostAmount!: number; + public amount!: number; + public allocatedCostAmount!: number; + public expenseAccountId!: number; + public projectId!: number; + public description!: string; /** * Table name diff --git a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGL.ts b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGL.ts index 417dba013..fbc7c4197 100644 --- a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGL.ts +++ b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGL.ts @@ -1,113 +1,105 @@ -// import * as R from 'ramda'; -// import { -// AccountNormal, -// IExpenseCategory, -// ILedger, -// ILedgerEntry, -// } from '@/interfaces'; -// import Ledger from '../Accounting/Ledger'; +import * as R from 'ramda'; +import { ILedger } from '@/modules/Ledger/types/Ledger.types'; +import { AccountNormal } from '@/modules/Accounts/Accounts.types'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { ExpenseCategory } from '../models/ExpenseCategory.model'; +import { Ledger } from '@/modules/Ledger/Ledger'; +import { Expense } from '../models/Expense.model'; -// export class ExpenseGL { -// private expense: any; +export class ExpenseGL { + private expense: Expense; -// /** -// * Constructor method. -// */ -// constructor(expense: any) { -// this.expense = expense; -// } + /** + * Constructor method. + * @param {Expense} expense - Expense. + */ + constructor(expense: Expense) { + this.expense = expense; + } -// /** -// * Retrieves the expense GL common entry. -// * @param {IExpense} expense -// * @returns {Partial} -// */ -// private getExpenseGLCommonEntry = (): Partial => { -// return { -// currencyCode: this.expense.currencyCode, -// exchangeRate: this.expense.exchangeRate, + /** + * Retrieves the expense GL common entry. + */ + private getExpenseGLCommonEntry = () => { + return { + currencyCode: this.expense.currencyCode, + exchangeRate: this.expense.exchangeRate, -// transactionType: 'Expense', -// transactionId: this.expense.id, + transactionType: 'Expense', + transactionId: this.expense.id, -// date: this.expense.paymentDate, -// userId: this.expense.userId, + date: this.expense.paymentDate, + userId: this.expense.userId, -// debit: 0, -// credit: 0, + debit: 0, + credit: 0, -// branchId: this.expense.branchId, -// }; -// }; + branchId: this.expense.branchId, + }; + }; -// /** -// * Retrieves the expense GL payment entry. -// * @param {IExpense} expense -// * @returns {ILedgerEntry} -// */ -// private getExpenseGLPaymentEntry = (): ILedgerEntry => { -// const commonEntry = this.getExpenseGLCommonEntry(); + /** + * Retrieves the expense GL payment entry. + * @returns {ILedgerEntry} + */ + private getExpenseGLPaymentEntry = (): ILedgerEntry => { + const commonEntry = this.getExpenseGLCommonEntry(); -// return { -// ...commonEntry, -// credit: this.expense.localAmount, -// accountId: this.expense.paymentAccountId, -// accountNormal: -// this.expense?.paymentAccount?.accountNormal === 'debit' -// ? AccountNormal.DEBIT -// : AccountNormal.CREDIT, -// index: 1, -// }; -// }; + return { + ...commonEntry, + credit: this.expense.localAmount, + accountId: this.expense.paymentAccountId, + accountNormal: + this.expense?.paymentAccount?.accountNormal === 'debit' + ? AccountNormal.DEBIT + : AccountNormal.CREDIT, + index: 1, + }; + }; -// /** -// * Retrieves the expense GL category entry. -// * @param {IExpense} expense - -// * @param {IExpenseCategory} expenseCategory - -// * @param {number} index -// * @returns {ILedgerEntry} -// */ -// private getExpenseGLCategoryEntry = R.curry( -// (category: IExpenseCategory, index: number): ILedgerEntry => { -// const commonEntry = this.getExpenseGLCommonEntry(); -// const localAmount = category.amount * this.expense.exchangeRate; + /** + * Retrieves the expense GL category entry. + * @param {ExpenseCategory} category - Expense category. + * @param {number} index + * @returns {ILedgerEntry} + */ + private getExpenseGLCategoryEntry = R.curry( + (category: ExpenseCategory, index: number): ILedgerEntry => { + const commonEntry = this.getExpenseGLCommonEntry(); + const localAmount = category.amount * this.expense.exchangeRate; -// return { -// ...commonEntry, -// accountId: category.expenseAccountId, -// accountNormal: AccountNormal.DEBIT, -// debit: localAmount, -// note: category.description, -// index: index + 2, -// projectId: category.projectId, -// }; -// } -// ); + return { + ...commonEntry, + accountId: category.expenseAccountId, + accountNormal: AccountNormal.DEBIT, + debit: localAmount, + note: category.description, + index: index + 2, + projectId: category.projectId, + }; + } + ); -// /** -// * Retrieves the expense GL entries. -// * @param {IExpense} expense -// * @returns {ILedgerEntry[]} -// */ -// public getExpenseGLEntries = (): ILedgerEntry[] => { -// const getCategoryEntry = this.getExpenseGLCategoryEntry(); + /** + * Retrieves the expense GL entries. + * @returns {ILedgerEntry[]} + */ + public getExpenseGLEntries = (): ILedgerEntry[] => { + const getCategoryEntry = this.getExpenseGLCategoryEntry(); -// const paymentEntry = this.getExpenseGLPaymentEntry(); -// const categoryEntries = this.expense.categories.map(getCategoryEntry); + const paymentEntry = this.getExpenseGLPaymentEntry(); + const categoryEntries = this.expense.categories.map(getCategoryEntry); -// return [paymentEntry, ...categoryEntries]; -// }; + return [paymentEntry, ...categoryEntries]; + }; -// /** -// * Retrieves the given expense ledger. -// * @param {IExpense} expense -// * @returns {ILedger} -// */ -// public getExpenseLedger = (): ILedger => { -// const entries = this.getExpenseGLEntries(); + /** + * Retrieves the given expense ledger. + * @returns {ILedger} + */ + public getExpenseLedger = (): ILedger => { + const entries = this.getExpenseGLEntries(); -// console.log(entries, 'entries'); - -// return new Ledger(entries); -// }; -// } + return new Ledger(entries); + }; +} diff --git a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntries.service.ts b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntries.service.ts index d3f2f552c..eb6da79bd 100644 --- a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntries.service.ts +++ b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntries.service.ts @@ -1,42 +1,44 @@ -// import { Knex } from 'knex'; -// import { ExpenseGL } from './ExpenseGL'; -// import { Inject, Injectable } from '@nestjs/common'; -// import { Expense } from '../models/Expense.model'; +import { Knex } from 'knex'; +import { ExpenseGL } from './ExpenseGL'; +import { Inject, Injectable } from '@nestjs/common'; +import { Expense } from '../models/Expense.model'; +import { ILedger } from '@/modules/Ledger/types/Ledger.types'; -// @Injectable() -// export class ExpenseGLEntries { -// constructor( -// @Inject(Expense.name) -// private readonly expense: typeof Expense, -// ) {} -// /** -// * Retrieves the expense G/L of the given id. -// * @param {number} expenseId -// * @param {Knex.Transaction} trx -// * @returns {Promise} -// */ -// public getExpenseLedgerById = async ( -// expenseId: number, -// trx?: Knex.Transaction, -// ): Promise => { -// const expense = await this.expense -// .query(trx) -// .findById(expenseId) -// .withGraphFetched('categories') -// .withGraphFetched('paymentAccount') -// .throwIfNotFound(); +@Injectable() +export class ExpenseGLEntriesService { + constructor( + @Inject(Expense.name) + private readonly expense: typeof Expense, + ) {} -// return this.getExpenseLedger(expense); -// }; + /** + * Retrieves the expense G/L of the given id. + * @param {number} expenseId + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + public getExpenseLedgerById = async ( + expenseId: number, + trx?: Knex.Transaction, + ): Promise => { + const expense = await this.expense + .query(trx) + .findById(expenseId) + .withGraphFetched('categories') + .withGraphFetched('paymentAccount') + .throwIfNotFound(); -// /** -// * Retrieves the given expense ledger. -// * @param {IExpense} expense -// * @returns {ILedger} -// */ -// public getExpenseLedger = (expense: Expense): ILedger => { -// const expenseGL = new ExpenseGL(expense); + return this.getExpenseLedger(expense); + }; -// return expenseGL.getExpenseLedger(); -// }; -// } + /** + * Retrieves the given expense ledger. + * @param {IExpense} expense + * @returns {ILedger} + */ + public getExpenseLedger = (expense: Expense): ILedger => { + const expenseGL = new ExpenseGL(expense); + + return expenseGL.getExpenseLedger(); + }; +} diff --git a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntries.subscriber.ts b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntries.subscriber.ts index d607eefa3..5200a5ce8 100644 --- a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntries.subscriber.ts +++ b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntries.subscriber.ts @@ -1,77 +1,79 @@ -// import { -// IExpenseCreatedPayload, -// IExpenseEventDeletePayload, -// IExpenseEventEditPayload, -// IExpenseEventPublishedPayload, -// } from '../Expenses.types'; -// import { ExpenseGLEntriesStorage } from './ExpenseGLEntriesStorage'; -// import { Injectable } from '@nestjs/common'; -// import { OnEvent } from '@nestjs/event-emitter'; -// import { events } from '@/common/events/events'; +import { + IExpenseCreatedPayload, + IExpenseEventDeletePayload, + IExpenseEventEditPayload, + IExpenseEventPublishedPayload, +} from '../Expenses.types'; +import { ExpenseGLEntriesStorageService } from './ExpenseGLEntriesStorage.sevice'; +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; -// @Injectable() -// export class ExpensesWriteGLSubscriber { -// /** -// * @param {ExpenseGLEntriesStorage} expenseGLEntries - -// */ -// constructor(private readonly expenseGLEntries: ExpenseGLEntriesStorage) {} +@Injectable() +export class ExpensesWriteGLSubscriber { + /** + * @param {ExpenseGLEntriesStorageService} expenseGLEntries - + */ + constructor( + private readonly expenseGLEntries: ExpenseGLEntriesStorageService, + ) {} -// /** -// * Handles the writing journal entries once the expense created. -// * @param {IExpenseCreatedPayload} payload - -// */ -// @OnEvent(events.expenses.onCreated) -// public async handleWriteGLEntriesOnceCreated({ -// expense, -// trx, -// }: IExpenseCreatedPayload) { -// // In case expense published, write journal entries. -// if (!expense.publishedAt) return; + /** + * Handles the writing journal entries once the expense created. + * @param {IExpenseCreatedPayload} payload - + */ + @OnEvent(events.expenses.onCreated) + public async handleWriteGLEntriesOnceCreated({ + expense, + trx, + }: IExpenseCreatedPayload) { + // In case expense published, write journal entries. + if (!expense.publishedAt) return; -// await this.expenseGLEntries.writeExpenseGLEntries(expense.id, trx); -// } + await this.expenseGLEntries.writeExpenseGLEntries(expense.id, trx); + } -// /** -// * Handle writing expense journal entries once the expense edited. -// * @param {IExpenseEventEditPayload} payload - -// */ -// @OnEvent(events.expenses.onEdited) -// public async handleRewriteGLEntriesOnceEdited({ -// expenseId, -// expense, -// authorizedUser, -// trx, -// }: IExpenseEventEditPayload) { -// // Cannot continue if the expense is not published. -// if (!expense.publishedAt) return; + /** + * Handle writing expense journal entries once the expense edited. + * @param {IExpenseEventEditPayload} payload - + */ + @OnEvent(events.expenses.onEdited) + public async handleRewriteGLEntriesOnceEdited({ + expenseId, + expense, + authorizedUser, + trx, + }: IExpenseEventEditPayload) { + // Cannot continue if the expense is not published. + if (!expense.publishedAt) return; -// await this.expenseGLEntries.rewriteExpenseGLEntries(expense.id, trx); -// } + await this.expenseGLEntries.rewriteExpenseGLEntries(expense.id, trx); + } -// /** -// * Reverts expense journal entries once the expense deleted. -// * @param {IExpenseEventDeletePayload} payload - -// */ -// @OnEvent(events.expenses.onDeleted) -// public async handleRevertGLEntriesOnceDeleted({ -// expenseId, -// trx, -// }: IExpenseEventDeletePayload) { -// await this.expenseGLEntries.revertExpenseGLEntries(expenseId, trx); -// } + /** + * Reverts expense journal entries once the expense deleted. + * @param {IExpenseEventDeletePayload} payload - + */ + @OnEvent(events.expenses.onDeleted) + public async handleRevertGLEntriesOnceDeleted({ + expenseId, + trx, + }: IExpenseEventDeletePayload) { + await this.expenseGLEntries.revertExpenseGLEntries(expenseId, trx); + } -// /** -// * Handles writing expense journal once the expense publish. -// * @param {IExpenseEventPublishedPayload} payload - -// */ -// @OnEvent(events.expenses.onPublished) -// public async handleWriteGLEntriesOncePublished({ -// expense, -// trx, -// }: IExpenseEventPublishedPayload) { -// // In case expense published, write journal entries. -// if (!expense.publishedAt) return; + /** + * Handles writing expense journal once the expense publish. + * @param {IExpenseEventPublishedPayload} payload - + */ + @OnEvent(events.expenses.onPublished) + public async handleWriteGLEntriesOncePublished({ + expense, + trx, + }: IExpenseEventPublishedPayload) { + // In case expense published, write journal entries. + if (!expense.publishedAt) return; -// await this.expenseGLEntries.rewriteExpenseGLEntries(expense.id, trx); -// } -// } + await this.expenseGLEntries.rewriteExpenseGLEntries(expense.id, trx); + } +} diff --git a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntriesStorage.sevice.ts b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntriesStorage.sevice.ts new file mode 100644 index 000000000..a934e5e1b --- /dev/null +++ b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntriesStorage.sevice.ts @@ -0,0 +1,68 @@ +import { Knex } from 'knex'; +import { Injectable } from '@nestjs/common'; +import { ExpenseGLEntriesService } from './ExpenseGLEntries.service'; +import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service'; + +@Injectable() +export class ExpenseGLEntriesStorageService { + /** + * @param {ExpenseGLEntriesService} expenseGLEntries - Expense GL entries service. + * @param {LedgerStorageService} ledgerStorage - Ledger storage service. + */ + constructor( + private readonly expenseGLEntries: ExpenseGLEntriesService, + private readonly ledgerStorage: LedgerStorageService, + ) {} + + /** + * Writes the expense GL entries. + * @param {number} tenantId + * @param {number} expenseId + * @param {Knex.Transaction} trx + */ + public writeExpenseGLEntries = async ( + expenseId: number, + trx?: Knex.Transaction, + ) => { + // Retrieves the given expense ledger. + const expenseLedger = await this.expenseGLEntries.getExpenseLedgerById( + expenseId, + trx, + ); + // Commits the expense ledger entries. + await this.ledgerStorage.commit(expenseLedger, trx); + }; + + /** + * Reverts the given expense GL entries. + * @param {number} tenantId + * @param {number} expenseId + * @param {Knex.Transaction} trx + */ + public revertExpenseGLEntries = async ( + expenseId: number, + trx?: Knex.Transaction, + ) => { + await this.ledgerStorage.deleteByReference( + expenseId, + 'Expense', + trx, + ); + }; + + /** + * Rewrites the expense GL entries. + * @param {number} expenseId + * @param {Knex.Transaction} trx + */ + public rewriteExpenseGLEntries = async ( + expenseId: number, + trx?: Knex.Transaction, + ) => { + // Reverts the expense GL entries. + await this.revertExpenseGLEntries(expenseId, trx); + + // Writes the expense GL entries. + await this.writeExpenseGLEntries(expenseId, trx); + }; +} diff --git a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntriesStorage.ts b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntriesStorage.ts deleted file mode 100644 index 2a5444116..000000000 --- a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGLEntriesStorage.ts +++ /dev/null @@ -1,67 +0,0 @@ -// import { Knex } from 'knex'; -// import { ExpenseGLEntries } from './ExpenseGLEntries.service'; -// import { Injectable } from '@nestjs/common'; - -// @Injectable() -// export class ExpenseGLEntriesStorage { -// /** -// * @param {ExpenseGLEntries} expenseGLEntries -// * @param {LedgerStorageService} ledgerStorage -// */ -// constructor( -// private readonly expenseGLEntries: ExpenseGLEntries, -// private readonly ledgerStorage: LedgerStorageService, -// ) {} - -// /** -// * Writes the expense GL entries. -// * @param {number} tenantId -// * @param {number} expenseId -// * @param {Knex.Transaction} trx -// */ -// public writeExpenseGLEntries = async ( -// expenseId: number, -// trx?: Knex.Transaction, -// ) => { -// // Retrieves the given expense ledger. -// const expenseLedger = await this.expenseGLEntries.getExpenseLedgerById( -// expenseId, -// trx, -// ); -// // Commits the expense ledger entries. -// await this.ledgerStorage.commit(expenseLedger, trx); -// }; - -// /** -// * Reverts the given expense GL entries. -// * @param {number} tenantId -// * @param {number} expenseId -// * @param {Knex.Transaction} trx -// */ -// public revertExpenseGLEntries = async ( -// expenseId: number, -// trx?: Knex.Transaction, -// ) => { -// await this.ledgerStorage.deleteByReference( -// expenseId, -// 'Expense', -// trx, -// ); -// }; - -// /** -// * Rewrites the expense GL entries. -// * @param {number} expenseId -// * @param {Knex.Transaction} trx -// */ -// public rewriteExpenseGLEntries = async ( -// expenseId: number, -// trx?: Knex.Transaction, -// ) => { -// // Reverts the expense GL entries. -// await this.revertExpenseGLEntries(expenseId, trx); - -// // Writes the expense GL entries. -// await this.writeExpenseGLEntries(expenseId, trx); -// }; -// } diff --git a/packages/server-nest/src/modules/Ledger/JournalEntry.ts b/packages/server-nest/src/modules/Ledger/JournalEntry.ts new file mode 100644 index 000000000..bd0ac2634 --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/JournalEntry.ts @@ -0,0 +1,13 @@ +import { ILedgerEntry } from "./types/Ledger.types"; + +export class JournalEntry { + entry: ILedgerEntry; + + constructor(entry: ILedgerEntry) { + const defaults = { + credit: 0, + debit: 0, + }; + this.entry = { ...defaults, ...entry }; + } +} diff --git a/packages/server-nest/src/modules/Ledger/Ledger.module.ts b/packages/server-nest/src/modules/Ledger/Ledger.module.ts new file mode 100644 index 000000000..d186db0e3 --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/Ledger.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { LedgerStorageService } from './LedgerStorage.service'; +import { LedgerEntriesStorageService } from './LedgerEntriesStorage.service'; +import { LedgerRevertService } from './LedgerStorageRevert.service'; +import { LedgerContactsBalanceStorage } from './LedgerContactStorage.service'; + +@Module({ + providers: [ + LedgerStorageService, + LedgerEntriesStorageService, + LedgerRevertService, + LedgerContactsBalanceStorage, + ], + exports: [ + LedgerStorageService, + LedgerEntriesStorageService, + LedgerRevertService, + ], +}) +export class LedgerModule {} diff --git a/packages/server-nest/src/modules/Ledger/Ledger.ts b/packages/server-nest/src/modules/Ledger/Ledger.ts new file mode 100644 index 000000000..0a06e8cb6 --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/Ledger.ts @@ -0,0 +1,291 @@ +import moment from 'moment'; +import { defaultTo, sumBy, uniqBy } from 'lodash'; +import { IAccountTransaction, ILedger, ILedgerEntry } from '@/interfaces'; + +export class Ledger implements ILedger { + readonly entries: ILedgerEntry[]; + + /** + * Constructor method. + * @param {ILedgerEntry[]} entries + */ + constructor(entries: ILedgerEntry[]) { + this.entries = entries; + } + + /** + * Filters the ledegr entries. + * @param callback + * @returns {ILedger} + */ + public filter(callback): ILedger { + const entries = this.entries.filter(callback); + return new Ledger(entries); + } + + /** + * Retrieve the all entries of the ledger. + * @return {ILedgerEntry[]} + */ + public getEntries(): ILedgerEntry[] { + return this.entries; + } + + /** + * Filters entries by th given contact id and returns a new ledger. + * @param {number} contactId + * @returns {ILedger} + */ + public whereContactId(contactId: number): ILedger { + return this.filter((entry) => entry.contactId === contactId); + } + + /** + * Filters entries by the given account id and returns a new ledger. + * @param {number} accountId + * @returns {ILedger} + */ + public whereAccountId(accountId: number): ILedger { + return this.filter((entry) => entry.accountId === accountId); + } + + /** + * Filters entries by the given accounts ids then returns a new ledger. + * @param {number[]} accountIds + * @returns {ILedger} + */ + public whereAccountsIds(accountIds: number[]): ILedger { + return this.filter((entry) => accountIds.indexOf(entry.accountId) !== -1); + } + + /** + * Filters entries that before or same the given date and returns a new ledger. + * @param {Date|string} fromDate + * @returns {ILedger} + */ + public whereFromDate(fromDate: Date | string): ILedger { + const fromDateParsed = moment(fromDate); + + return this.filter( + (entry) => + fromDateParsed.isBefore(entry.date) || fromDateParsed.isSame(entry.date) + ); + } + + /** + * Filters ledger entries that after the given date and retruns a new ledger. + * @param {Date|string} toDate + * @returns {ILedger} + */ + public whereToDate(toDate: Date | string): ILedger { + const toDateParsed = moment(toDate); + + return this.filter( + (entry) => + toDateParsed.isAfter(entry.date) || toDateParsed.isSame(entry.date) + ); + } + + /** + * Filters the ledget entries by the given currency code. + * @param {string} currencyCode - + * @returns {ILedger} + */ + public whereCurrencyCode(currencyCode: string): ILedger { + return this.filter((entry) => entry.currencyCode === currencyCode); + } + + /** + * Filters the ledger entries by the given branch id. + * @param {number} branchId + * @returns {ILedger} + */ + public whereBranch(branchId: number): ILedger { + return this.filter((entry) => entry.branchId === branchId); + } + + /** + * + * @param {number} projectId + * @returns {ILedger} + */ + public whereProject(projectId: number): ILedger { + return this.filter((entry) => entry.projectId === projectId); + } + + /** + * Filters the ledger entries by the given item id. + * @param {number} itemId + * @returns {ILedger} + */ + public whereItem(itemId: number): ILedger { + return this.filter((entry) => entry.itemId === itemId); + } + + /** + * Retrieve the closing balance of the entries. + * @returns {number} + */ + public getClosingBalance(): number { + let closingBalance = 0; + + this.entries.forEach((entry) => { + if (entry.accountNormal === 'credit') { + closingBalance += entry.credit - entry.debit; + } else if (entry.accountNormal === 'debit') { + closingBalance += entry.debit - entry.credit; + } + }); + return closingBalance; + } + + /** + * Retrieves the closing credit of the entries. + * @returns {number} + */ + public getClosingCredit(): number { + return sumBy(this.entries, 'credit'); + } + + /** + * Retrieves the closing debit of the entries. + * @returns {number} + */ + public getClosingDebit(): number { + return sumBy(this.entries, 'debit'); + } + + /** + * Retrieve the closing balance of the entries. + * @returns {number} + */ + public getForeignClosingBalance(): number { + let closingBalance = 0; + + this.entries.forEach((entry) => { + const exchangeRate = entry.exchangeRate || 1; + + if (entry.accountNormal === 'credit') { + closingBalance += (entry.credit - entry.debit) / exchangeRate; + } else if (entry.accountNormal === 'debit') { + closingBalance += (entry.debit - entry.credit) / exchangeRate; + } + }); + return closingBalance; + } + + /** + * Detarmines whether the ledger has no entries. + * @returns {boolean} + */ + public isEmpty(): boolean { + return this.entries.length === 0; + } + + /** + * Retrieves the accounts ids of the entries uniquely. + * @returns {number[]} + */ + public getAccountsIds = (): number[] => { + return uniqBy(this.entries, 'accountId').map( + (e: ILedgerEntry) => e.accountId + ); + }; + + /** + * Retrieves the contacts ids of the entries uniquely. + * @returns {number[]} + */ + public getContactsIds = (): number[] => { + return uniqBy(this.entries, 'contactId') + .filter((e: ILedgerEntry) => e.contactId) + .map((e: ILedgerEntry) => e.contactId); + }; + + /** + * Reverses the ledger entries. + * @returns {Ledger} + */ + public reverse = (): Ledger => { + const newEntries = this.entries.map((e) => { + const credit = e.debit; + const debit = e.credit; + + return { ...e, credit, debit }; + }); + return new Ledger(newEntries); + }; + + // --------------------------------- + // # STATIC METHODS. + // ---------------------------------- + + /** + * Mappes the account transactions to ledger entries. + * @param {IAccountTransaction[]} entries + * @returns {ILedgerEntry[]} + */ + static mappingTransactions(entries: IAccountTransaction[]): ILedgerEntry[] { + return entries.map(this.mapTransaction); + } + + /** + * Mappes the account transaction to ledger entry. + * @param {IAccountTransaction} entry + * @returns {ILedgerEntry} + */ + static mapTransaction(entry: IAccountTransaction): ILedgerEntry { + return { + credit: defaultTo(entry.credit, 0), + debit: defaultTo(entry.debit, 0), + + exchangeRate: entry.exchangeRate, + currencyCode: entry.currencyCode, + + accountNormal: entry.account.accountNormal, + accountId: entry.accountId, + contactId: entry.contactId, + + date: entry.date, + + transactionId: entry.referenceId, + transactionType: entry.referenceType, + transactionSubType: entry.transactionType, + + transactionNumber: entry.transactionNumber, + referenceNumber: entry.referenceNumber, + + index: entry.index, + indexGroup: entry.indexGroup, + + entryId: entry.id, + branchId: entry.branchId, + projectId: entry.projectId, + + taxRateId: entry.taxRateId, + taxRate: entry.taxRate, + + note: entry.note, + }; + } + + /** + * Mappes the account transactions to ledger entries. + * @param {IAccountTransaction[]} transactions + * @returns {ILedger} + */ + static fromTransactions(transactions: IAccountTransaction[]): Ledger { + const entries = Ledger.mappingTransactions(transactions); + return new Ledger(entries); + } + + /** + * Retrieve the transaction amount. + * @param {number} credit - Credit amount. + * @param {number} debit - Debit amount. + * @param {string} normal - Credit or debit. + */ + static getAmount(credit: number, debit: number, normal: string) { + return normal === 'credit' ? credit - debit : debit - credit; + } +} diff --git a/packages/server-nest/src/modules/Ledger/LedgerContactStorage.service.ts b/packages/server-nest/src/modules/Ledger/LedgerContactStorage.service.ts new file mode 100644 index 000000000..cde2ca314 --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/LedgerContactStorage.service.ts @@ -0,0 +1,148 @@ +import async from 'async'; +import { Knex } from 'knex'; +import { + ILedger, + ILedgerEntry, + ISaleContactsBalanceQueuePayload, +} from './types/Ledger.types'; +import { ACCOUNT_TYPE } from '@/constants/accounts'; +import { Inject, Injectable } from '@nestjs/common'; +import { Contact } from '../Contacts/models/Contact'; +import { Account } from '../Accounts/models/Account.model'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; + +@Injectable() +export class LedgerContactsBalanceStorage { + constructor( + private tenancyContext: TenancyContext, + + @Inject(Contact.name) + private contactModel: typeof Contact, + + @Inject(Account.name) + private accountModel: typeof Account, + ) {} + + /** + * + * @param {ILedger} ledger + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + public saveContactsBalance = async ( + ledger: ILedger, + trx?: Knex.Transaction, + ): Promise => { + // Save contact balance queue. + const saveContactsBalanceQueue = async.queue( + this.saveContactBalanceTask, + 10, + ); + // Retrieves the effected contacts ids. + const effectedContactsIds = ledger.getContactsIds(); + + effectedContactsIds.forEach((contactId: number) => { + saveContactsBalanceQueue.push({ contactId, ledger, trx }); + }); + if (effectedContactsIds.length > 0) await saveContactsBalanceQueue.drain(); + }; + + /** + * + * @param {ISaleContactsBalanceQueuePayload} task + * @returns {Promise} + */ + private saveContactBalanceTask = async ( + task: ISaleContactsBalanceQueuePayload, + ) => { + const { contactId, ledger, trx } = task; + + await this.saveContactBalance(ledger, contactId, trx); + }; + + /** + * Filters AP/AR ledger entries. + * @param {number} tenantId + * @param {Knex.Transaction} trx + * @returns {Promise<(entry: ILedgerEntry) => boolean>} + */ + private filterARAPLedgerEntris = async ( + tenantId: number, + trx?: Knex.Transaction, + ): Promise<(entry: ILedgerEntry) => boolean> => { + const ARAPAccounts = await this.accountModel + .query(trx) + .whereIn('accountType', [ + ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE, + ACCOUNT_TYPE.ACCOUNTS_PAYABLE, + ]); + const ARAPAccountsIds = ARAPAccounts.map((a) => a.id); + + return (entry: ILedgerEntry) => { + return ARAPAccountsIds.indexOf(entry.accountId) !== -1; + }; + }; + + /** + * + * @param {number} tenantId + * @param {ILedger} ledger + * @param {number} contactId + * @returns {Promise} + */ + private saveContactBalance = async ( + tenantId: number, + ledger: ILedger, + contactId: number, + trx?: Knex.Transaction, + ): Promise => { + const contact = await this.contactModel.query(trx).findById(contactId); + + // Retrieves the given tenant metadata. + const tenant = await this.tenancyContext.getTenant(true); + + // Detarmines whether the contact has foreign currency. + const isForeignContact = + contact.currencyCode !== tenant?.metadata.baseCurrency; + + // Filters the ledger base on the given contact id. + const filterARAPLedgerEntris = await this.filterARAPLedgerEntris( + tenantId, + trx, + ); + const contactLedger = ledger + // Filter entries only that have contact id. + .whereContactId(contactId) + // Filter entries on AR/AP accounts. + .filter(filterARAPLedgerEntris); + + const closingBalance = isForeignContact + ? contactLedger + .whereCurrencyCode(contact.currencyCode) + .getForeignClosingBalance() + : contactLedger.getClosingBalance(); + + await this.changeContactBalance(tenantId, contactId, closingBalance, trx); + }; + + /** + * + * @param {number} tenantId + * @param {number} contactId + * @param {number} change + * @returns + */ + private changeContactBalance = ( + tenantId: number, + contactId: number, + change: number, + trx?: Knex.Transaction, + ) => { + return this.contactModel.changeAmount( + { id: contactId }, + 'balance', + change, + trx, + ); + }; +} diff --git a/packages/server-nest/src/modules/Ledger/LedgerEntriesStorage.service.ts b/packages/server-nest/src/modules/Ledger/LedgerEntriesStorage.service.ts new file mode 100644 index 000000000..738f4a817 --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/LedgerEntriesStorage.service.ts @@ -0,0 +1,83 @@ +import { Knex } from 'knex'; +import async from 'async'; +import { Inject, Injectable } from '@nestjs/common'; +import { transformLedgerEntryToTransaction } from './utils'; +import { + ILedgerEntry, + ISaveLedgerEntryQueuePayload, +} from './types/Ledger.types'; +import { ILedger } from './types/Ledger.types'; +import { AccountTransaction } from '../Accounts/models/AccountTransaction.model'; + +// Filter the blank entries. +const filterBlankEntry = (entry: ILedgerEntry) => + Boolean(entry.credit || entry.debit); + +@Injectable() +export class LedgerEntriesStorageService { + /** + * @param {typeof AccountTransaction} accountTransactionModel - Account transaction model. + */ + constructor( + @Inject(AccountTransaction.name) + private readonly accountTransactionModel: typeof AccountTransaction, + ) {} + + /** + * Saves entries of the given ledger. + * @param {ILedger} ledger - Ledger. + * @param {Knex.Transaction} trx - Knex transaction. + * @returns {Promise} + */ + public saveEntries = async (ledger: ILedger, trx?: Knex.Transaction) => { + const saveEntryQueue = async.queue(this.saveEntryTask, 10); + const entries = ledger.filter(filterBlankEntry).getEntries(); + + entries.forEach((entry) => { + saveEntryQueue.push({ entry, trx }); + }); + if (entries.length > 0) await saveEntryQueue.drain(); + }; + + /** + * Deletes the ledger entries. + * @param {ILedger} ledger - Ledger. + * @param {Knex.Transaction} trx - Knex transaction. + */ + public deleteEntries = async (ledger: ILedger, trx?: Knex.Transaction) => { + const entriesIds = ledger + .getEntries() + .filter((e) => e.entryId) + .map((e) => e.entryId); + + await AccountTransaction.query(trx).whereIn('id', entriesIds).delete(); + }; + + /** + * Saves the ledger entry to the account transactions repository. + * @param {ILedgerEntry} entry - Ledger entry. + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + private saveEntry = async ( + entry: ILedgerEntry, + trx?: Knex.Transaction, + ): Promise => { + const transaction = transformLedgerEntryToTransaction(entry); + + await this.accountTransactionModel.query(trx).insert(transaction); + }; + + /** + * Save the ledger entry to the transactions repository async task. + * @param {ISaveLedgerEntryQueuePayload} task - Task payload. + * @returns {Promise} + */ + private saveEntryTask = async ( + task: ISaveLedgerEntryQueuePayload, + ): Promise => { + const { entry, trx } = task; + + await this.saveEntry(entry, trx); + }; +} diff --git a/packages/server-nest/src/modules/Ledger/LedgerStorage.service.ts b/packages/server-nest/src/modules/Ledger/LedgerStorage.service.ts new file mode 100644 index 000000000..d6e0097f7 --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/LedgerStorage.service.ts @@ -0,0 +1,85 @@ +import { Knex } from 'knex'; +import { ILedger } from './types/Ledger.types'; +import { LedgerContactsBalanceStorage } from './LedgerContactStorage.service'; +import { LedegrAccountsStorage } from './LedgetAccountStorage.service'; +import { LedgerEntriesStorageService } from './LedgerEntriesStorage.service'; +import { Ledger } from './Ledger'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LedgerStorageService { + constructor( + private ledgerContactsBalance: LedgerContactsBalanceStorage, + private ledgerAccountsBalance: LedegrAccountsStorage, + private ledgerEntriesService: LedgerEntriesStorageService, + ) {} + + /** + * Commit the ledger to the storage layer as one unit-of-work. + * @param {ILedger} ledger + * @returns {Promise} + */ + public commit = async ( + ledger: ILedger, + trx?: Knex.Transaction, + ): Promise => { + const tasks = [ + // Saves the ledger entries. + this.ledgerEntriesService.saveEntries(ledger, trx), + + // Mutates the associated accounts balances. + this.ledgerAccountsBalance.saveAccountsBalance(ledger, trx), + + // Mutates the associated contacts balances. + this.ledgerContactsBalance.saveContactsBalance(ledger, trx), + ]; + await Promise.all(tasks); + }; + + /** + * Deletes the given ledger and revert balances. + * @param {number} tenantId + * @param {ILedger} ledger + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + public delete = async ( + ledger: ILedger, + trx?: Knex.Transaction, + ) => { + const tasks = [ + // Deletes the ledger entries. + this.ledgerEntriesService.deleteEntries(ledger, trx), + + // Mutates the associated accounts balances. + this.ledgerAccountsBalance.saveAccountsBalance(ledger, trx), + + // Mutates the associated contacts balances. + this.ledgerContactsBalance.saveContactsBalance(ledger, trx), + ]; + await Promise.all(tasks); + }; + + /** + * @param {number | number[]} referenceId + * @param {string | string[]} referenceType + * @param {Knex.Transaction} trx + */ + public deleteByReference = async ( + referenceId: number | number[], + referenceType: string | string[], + trx?: Knex.Transaction, + ) => { + // Retrieves the transactions of the given reference. + const transactions = + await transactionsRepository.getTransactionsByReference( + referenceId, + referenceType, + ); + // Creates a new ledger from transaction and reverse the entries. + const reversedLedger = Ledger.fromTransactions(transactions).reverse(); + + // Deletes and reverts the balances. + await this.delete(tenantId, reversedLedger, trx); + }; +} diff --git a/packages/server-nest/src/modules/Ledger/LedgerStorageRevert.service.ts b/packages/server-nest/src/modules/Ledger/LedgerStorageRevert.service.ts new file mode 100644 index 000000000..f86614ffe --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/LedgerStorageRevert.service.ts @@ -0,0 +1,62 @@ +import { castArray } from 'lodash'; +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { Ledger } from './Ledger'; +import { LedgerStorageService } from './LedgerStorage.service'; +import { AccountTransaction } from '../Accounts/models/AccountTransaction.model'; + +@Injectable() +export class LedgerRevertService { + /** + * @param ledgerStorage - Ledger storage service. + * @param accountTransactionModel - Account transaction model. + */ + constructor( + private readonly ledgerStorage: LedgerStorageService, + + @Inject(AccountTransaction.name) + private readonly accountTransactionModel: typeof AccountTransaction, + ) {} + + /** + * Reverts the jouranl entries. + * @param {number|number[]} referenceId - Reference id. + * @param {string} referenceType - Reference type. + */ + public getTransactionsByReference = async ( + referenceId: number | number[], + referenceType: string | string[], + ) => { + const transactions = await this.accountTransactionModel + .query() + .whereIn('reference_type', castArray(referenceType)) + .whereIn('reference_id', castArray(referenceId)) + .withGraphFetched('account'); + + return transactions; + }; + + /** + * Reverts the journal entries. + * @param {number|number[]} referenceId - Reference id. + * @param {string|string[]} referenceType - Reference type. + * @param {Knex.Transaction} trx + */ + public revertGLEntries = async ( + referenceId: number | number[], + referenceType: string | string[], + trx?: Knex.Transaction, + ) => { + // Gets the transactions by reference. + const transactions = await this.getTransactionsByReference( + referenceId, + referenceType, + ); + // Creates a new ledger from transaction and reverse the entries. + const ledger = Ledger.fromTransactions(transactions); + const reversedLedger = ledger.reverse(); + + // Commits the reversed ledger. + await this.ledgerStorage.commit(reversedLedger, trx); + }; +} diff --git a/packages/server-nest/src/modules/Ledger/LedgetAccountStorage.service.ts b/packages/server-nest/src/modules/Ledger/LedgetAccountStorage.service.ts new file mode 100644 index 000000000..7eab97944 --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/LedgetAccountStorage.service.ts @@ -0,0 +1,166 @@ +import async from 'async'; +import { Knex } from 'knex'; +import { uniq } from 'lodash'; +import { + ILedger, + ISaveAccountsBalanceQueuePayload, +} from './types/Ledger.types'; +import { Inject, Injectable } from '@nestjs/common'; +import { Account } from '../Accounts/models/Account.model'; +import { AccountRepository } from '../Accounts/repositories/Account.repository'; + +@Injectable() +export class LedegrAccountsStorage { + /** + * @param {typeof Account} accountModel + * @param {AccountRepository} accountRepository - + */ + constructor( + @Inject(Account.name) + private accountModel: typeof Account, + + @Inject(AccountRepository.name) + private accountRepository: AccountRepository, + ) {} + + /** + * Retrieve depepants ids of the give accounts ids. + * @param {number[]} accountsIds + * @param depGraph + * @returns {number[]} + */ + private getDependantsAccountsIds = ( + accountsIds: number[], + depGraph, + ): number[] => { + const depAccountsIds = []; + + accountsIds.forEach((accountId: number) => { + const depAccountIds = depGraph.dependantsOf(accountId); + depAccountsIds.push(accountId, ...depAccountIds); + }); + return uniq(depAccountsIds); + }; + + /** + * + * @param {number[]} accountsIds + * @returns {number[]} + */ + private findDependantsAccountsIds = async ( + accountsIds: number[], + trx?: Knex.Transaction, + ): Promise => { + const accountsGraph = await this.accountRepository.getDependencyGraph( + null, + trx, + ); + return this.getDependantsAccountsIds(accountsIds, accountsGraph); + }; + + /** + * Atomic mutation for accounts balances. + * @param {number} tenantId + * @param {ILedger} ledger + * @param {Knex.Transaction} trx - + * @returns {Promise} + */ + public saveAccountsBalance = async ( + ledger: ILedger, + trx?: Knex.Transaction, + ): Promise => { + // Initiate a new queue for accounts balance mutation. + const saveAccountsBalanceQueue = async.queue( + this.saveAccountBalanceTask, + 10, + ); + const effectedAccountsIds = ledger.getAccountsIds(); + const dependAccountsIds = await this.findDependantsAccountsIds( + effectedAccountsIds, + trx, + ); + dependAccountsIds.forEach((accountId: number) => { + saveAccountsBalanceQueue.push({ ledger, accountId, trx }); + }); + if (dependAccountsIds.length > 0) { + await saveAccountsBalanceQueue.drain(); + } + }; + + /** + * Async task mutates the given account balance. + * @param {ISaveAccountsBalanceQueuePayload} task + * @returns {Promise} + */ + private saveAccountBalanceTask = async ( + task: ISaveAccountsBalanceQueuePayload, + ): Promise => { + const { tenantId, ledger, accountId, trx } = task; + + await this.saveAccountBalanceFromLedger(tenantId, ledger, accountId, trx); + }; + + /** + * Saves specific account balance from the given ledger. + * @param {number} tenantId + * @param {ILedger} ledger + * @param {number} accountId + * @param {Knex.Transaction} trx - + * @returns {Promise} + */ + private saveAccountBalanceFromLedger = async ( + tenantId: number, + ledger: ILedger, + accountId: number, + trx?: Knex.Transaction, + ): Promise => { + const account = await this.accountModel.query(trx).findById(accountId); + + // Filters the ledger entries by the current account. + const accountLedger = ledger.whereAccountId(accountId); + + // Retrieves the given tenant metadata. + const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + + // Detarmines whether the account has foreign currency. + const isAccountForeign = account.currencyCode !== tenantMeta.baseCurrency; + + // Calculates the closing foreign balance by the given currency if account was has + // foreign currency otherwise get closing balance. + const closingBalance = isAccountForeign + ? accountLedger + .whereCurrencyCode(account.currencyCode) + .getForeignClosingBalance() + : accountLedger.getClosingBalance(); + + await this.saveAccountBalance(tenantId, accountId, closingBalance, trx); + }; + + /** + * Saves the account balance. + * @param {number} tenantId + * @param {number} accountId + * @param {number} change + * @param {Knex.Transaction} trx - + * @returns {Promise} + */ + private saveAccountBalance = async ( + accountId: number, + change: number, + trx?: Knex.Transaction, + ) => { + // Ensure the account has atleast zero in amount. + await this.accountModel + .query(trx) + .findById(accountId) + .whereNull('amount') + .patch({ amount: 0 }); + + await this.accountModel.changeAmount( + { id: accountId }, + 'amount', + change, + trx, + ); + }; +} diff --git a/packages/server-nest/src/modules/Ledger/types/Ledger.types.ts b/packages/server-nest/src/modules/Ledger/types/Ledger.types.ts new file mode 100644 index 000000000..152db4eda --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/types/Ledger.types.ts @@ -0,0 +1,88 @@ +import { Knex } from 'knex'; + +export interface ILedger { + entries: ILedgerEntry[]; + + getEntries(): ILedgerEntry[]; + + filter(cb: (entry: ILedgerEntry) => boolean): ILedger; + + whereAccountId(accountId: number): ILedger; + whereAccountsIds(accountsIds: number[]): ILedger; + whereContactId(contactId: number): ILedger; + whereFromDate(fromDate: Date | string): ILedger; + whereToDate(toDate: Date | string): ILedger; + whereCurrencyCode(currencyCode: string): ILedger; + whereBranch(branchId: number): ILedger; + whereItem(itemId: number): ILedger; + whereProject(projectId: number): ILedger; + + getClosingBalance(): number; + getForeignClosingBalance(): number; + getClosingDebit(): number; + getClosingCredit(): number; + + getContactsIds(): number[]; + getAccountsIds(): number[]; + + reverse(): ILedger; + isEmpty(): boolean; +} + +export interface ILedgerEntry { + credit: number; + debit: number; + currencyCode: string; + exchangeRate: number; + + accountId?: number; + accountNormal: string; + contactId?: number; + date: Date | string; + + transactionType: string; + transactionSubType?: string; + + transactionId: number; + + transactionNumber?: string; + + referenceNumber?: string; + index: number; + indexGroup?: number; + + note?: string; + + userId?: number; + itemId?: number; + branchId?: number; + projectId?: number; + + taxRateId?: number; + taxRate?: number; + + entryId?: number; + createdAt?: Date; + + costable?: boolean; +} + +export interface ISaveLedgerEntryQueuePayload { + tenantId: number; + entry: ILedgerEntry; + trx?: Knex.Transaction; +} + +export interface ISaveAccountsBalanceQueuePayload { + ledger: ILedger; + tenantId: number; + accountId: number; + trx?: Knex.Transaction; +} + +export interface ISaleContactsBalanceQueuePayload { + ledger: ILedger; + tenantId: number; + contactId: number; + trx?: Knex.Transaction; +} diff --git a/packages/server-nest/src/modules/Ledger/utils.ts b/packages/server-nest/src/modules/Ledger/utils.ts new file mode 100644 index 000000000..2edc25b97 --- /dev/null +++ b/packages/server-nest/src/modules/Ledger/utils.ts @@ -0,0 +1,41 @@ +import { IAccountTransaction, ILedgerEntry } from '@/interfaces'; + +export const transformLedgerEntryToTransaction = ( + entry: ILedgerEntry +): IAccountTransaction => { + return { + date: entry.date, + + credit: entry.credit, + debit: entry.debit, + + currencyCode: entry.currencyCode, + exchangeRate: entry.exchangeRate, + + accountId: entry.accountId, + contactId: entry.contactId, + + referenceType: entry.transactionType, + referenceId: entry.transactionId, + + transactionNumber: entry.transactionNumber, + transactionType: entry.transactionSubType, + + referenceNumber: entry.referenceNumber, + + note: entry.note, + + index: entry.index, + indexGroup: entry.indexGroup, + + branchId: entry.branchId, + userId: entry.userId, + itemId: entry.itemId, + projectId: entry.projectId, + + costable: entry.costable, + + taxRateId: entry.taxRateId, + taxRate: entry.taxRate, + }; +}; diff --git a/packages/server-nest/src/modules/Warehouses/AccountsTransactionsWarehouses.ts b/packages/server-nest/src/modules/Warehouses/AccountsTransactionsWarehouses.ts new file mode 100644 index 000000000..b7ca809cd --- /dev/null +++ b/packages/server-nest/src/modules/Warehouses/AccountsTransactionsWarehouses.ts @@ -0,0 +1,26 @@ +// import { Service, Inject } from 'typedi'; +// import { Knex } from 'knex'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; + +// @Service() +// export class InventoryTransactionsWarehouses { +// @Inject() +// tenancy: HasTenancyService; + +// /** +// * Updates all accounts transctions with the priamry branch. +// * @param tenantId +// * @param primaryBranchId +// */ +// public updateTransactionsWithWarehouse = async ( +// tenantId: number, +// primaryBranchId: number, +// trx?: Knex.Transaction +// ) => { +// const { AccountTransaction } = await this.tenancy.models(tenantId); + +// await AccountTransaction.query(trx).update({ +// branchId: primaryBranchId, +// }); +// }; +// } diff --git a/packages/server-nest/src/modules/Warehouses/AccountsTransactionsWarehousesSubscribe.ts b/packages/server-nest/src/modules/Warehouses/AccountsTransactionsWarehousesSubscribe.ts new file mode 100644 index 000000000..bb559bb82 --- /dev/null +++ b/packages/server-nest/src/modules/Warehouses/AccountsTransactionsWarehousesSubscribe.ts @@ -0,0 +1,38 @@ +// import { Service, Inject } from 'typedi'; +// import events from '@/subscribers/events'; +// import { InventoryTransactionsWarehouses } from './AccountsTransactionsWarehouses'; +// import { IBranchesActivatedPayload } from '@/interfaces'; + +// @Service() +// export class AccountsTransactionsWarehousesSubscribe { +// @Inject() +// accountsTransactionsWarehouses: InventoryTransactionsWarehouses; + +// /** +// * Attaches events with handlers. +// */ +// public attach = (bus) => { +// bus.subscribe( +// events.branch.onActivated, +// this.updateGLTransactionsToPrimaryBranchOnActivated +// ); +// return bus; +// }; + +// /** +// * Updates all GL transactions to primary branch once +// * the multi-branches activated. +// * @param {IBranchesActivatedPayload} +// */ +// private updateGLTransactionsToPrimaryBranchOnActivated = async ({ +// tenantId, +// primaryBranch, +// trx, +// }: IBranchesActivatedPayload) => { +// await this.accountsTransactionsWarehouses.updateTransactionsWithWarehouse( +// tenantId, +// primaryBranch.id, +// trx +// ); +// }; +// }