add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,277 @@
import moment from 'moment';
import { sumBy } from 'lodash';
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import { AccountNormal, IBillPayment, ILedgerEntry } from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models';
@Service()
export class BillPaymentGLEntries {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private ledgerStorage: LedgerStorageService;
/**
* Creates a bill payment GL entries.
* @param {number} tenantId
* @param {number} billPaymentId
* @param {Knex.Transaction} trx
*/
public writePaymentGLEntries = async (
tenantId: number,
billPaymentId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { accountRepository } = this.tenancy.repositories(tenantId);
const { BillPayment, Account } = this.tenancy.models(tenantId);
// Retrieves the bill payment details with associated entries.
const payment = await BillPayment.query(trx)
.findById(billPaymentId)
.withGraphFetched('entries.bill');
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Finds or creates a new A/P account of the given currency.
const APAccount = await accountRepository.findOrCreateAccountsPayable(
payment.currencyCode,
{},
trx
);
// Exchange gain or loss account.
const EXGainLossAccount = await Account.query(trx).modify(
'findBySlug',
'exchange-grain-loss'
);
// Retrieves the bill payment ledger.
const ledger = this.getBillPaymentLedger(
payment,
APAccount.id,
EXGainLossAccount.id,
tenantMeta.baseCurrency
);
// Commits the ledger on the storage.
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
/**
* Rewrites the bill payment GL entries.
* @param {number} tenantId
* @param {number} billPaymentId
* @param {Knex.Transaction} trx
*/
public rewritePaymentGLEntries = async (
tenantId: number,
billPaymentId: number,
trx?: Knex.Transaction
): Promise<void> => {
// Revert payment GL entries.
await this.revertPaymentGLEntries(tenantId, billPaymentId, trx);
// Write payment GL entries.
await this.writePaymentGLEntries(tenantId, billPaymentId, trx);
};
/**
* Reverts the bill payment GL entries.
* @param {number} tenantId
* @param {number} billPaymentId
* @param {Knex.Transaction} trx
*/
public revertPaymentGLEntries = async (
tenantId: number,
billPaymentId: number,
trx?: Knex.Transaction
): Promise<void> => {
await this.ledgerStorage.deleteByReference(
tenantId,
billPaymentId,
'BillPayment',
trx
);
};
/**
* Retrieves the payment common entry.
* @param {IBillPayment} billPayment
* @returns {}
*/
private getPaymentCommonEntry = (billPayment: IBillPayment) => {
const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD');
return {
debit: 0,
credit: 0,
exchangeRate: billPayment.exchangeRate,
currencyCode: billPayment.currencyCode,
transactionId: billPayment.id,
transactionType: 'BillPayment',
transactionNumber: billPayment.paymentNumber,
referenceNumber: billPayment.reference,
date: formattedDate,
createdAt: billPayment.createdAt,
branchId: billPayment.branchId,
};
};
/**
* Calculates the payment total exchange gain/loss.
* @param {IBillPayment} paymentReceive - Payment receive with entries.
* @returns {number}
*/
private getPaymentExGainOrLoss = (billPayment: IBillPayment): number => {
return sumBy(billPayment.entries, (entry) => {
const paymentLocalAmount = entry.paymentAmount * billPayment.exchangeRate;
const invoicePayment = entry.paymentAmount * entry.bill.exchangeRate;
return invoicePayment - paymentLocalAmount;
});
};
/**
* Retrieves the payment exchange gain/loss entries.
* @param {IBillPayment} billPayment -
* @param {number} APAccountId -
* @param {number} gainLossAccountId -
* @param {string} baseCurrency -
* @returns {ILedgerEntry[]}
*/
private getPaymentExGainOrLossEntries = (
billPayment: IBillPayment,
APAccountId: number,
gainLossAccountId: number,
baseCurrency: string
): ILedgerEntry[] => {
const commonEntry = this.getPaymentCommonEntry(billPayment);
const totalExGainOrLoss = this.getPaymentExGainOrLoss(billPayment);
const absExGainOrLoss = Math.abs(totalExGainOrLoss);
return totalExGainOrLoss
? [
{
...commonEntry,
currencyCode: baseCurrency,
exchangeRate: 1,
credit: totalExGainOrLoss > 0 ? absExGainOrLoss : 0,
debit: totalExGainOrLoss < 0 ? absExGainOrLoss : 0,
accountId: gainLossAccountId,
index: 2,
indexGroup: 20,
accountNormal: AccountNormal.DEBIT,
},
{
...commonEntry,
currencyCode: baseCurrency,
exchangeRate: 1,
debit: totalExGainOrLoss > 0 ? absExGainOrLoss : 0,
credit: totalExGainOrLoss < 0 ? absExGainOrLoss : 0,
accountId: APAccountId,
index: 3,
accountNormal: AccountNormal.DEBIT,
},
]
: [];
};
/**
* Retrieves the payment deposit GL entry.
* @param {IBillPayment} billPayment
* @returns {ILedgerEntry}
*/
private getPaymentGLEntry = (billPayment: IBillPayment): ILedgerEntry => {
const commonEntry = this.getPaymentCommonEntry(billPayment);
return {
...commonEntry,
credit: billPayment.localAmount,
accountId: billPayment.paymentAccountId,
accountNormal: AccountNormal.DEBIT,
index: 2,
};
};
/**
* Retrieves the payment GL payable entry.
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @returns {ILedgerEntry}
*/
private getPaymentGLPayableEntry = (
billPayment: IBillPayment,
APAccountId: number
): ILedgerEntry => {
const commonEntry = this.getPaymentCommonEntry(billPayment);
return {
...commonEntry,
exchangeRate: billPayment.exchangeRate,
debit: billPayment.localAmount,
contactId: billPayment.vendorId,
accountId: APAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the payment GL entries.
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @returns {ILedgerEntry[]}
*/
private getPaymentGLEntries = (
billPayment: IBillPayment,
APAccountId: number,
gainLossAccountId: number,
baseCurrency: string
): ILedgerEntry[] => {
// Retrieves the payment deposit entry.
const paymentEntry = this.getPaymentGLEntry(billPayment);
// Retrieves the payment debit A/R entry.
const payableEntry = this.getPaymentGLPayableEntry(
billPayment,
APAccountId
);
// Retrieves the exchange gain/loss entries.
const exGainLossEntries = this.getPaymentExGainOrLossEntries(
billPayment,
APAccountId,
gainLossAccountId,
baseCurrency
);
return [paymentEntry, payableEntry, ...exGainLossEntries];
};
/**
* Retrieves the bill payment ledger.
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @returns {Ledger}
*/
private getBillPaymentLedger = (
billPayment: IBillPayment,
APAccountId: number,
gainLossAccountId: number,
baseCurrency: string
): Ledger => {
const entries = this.getPaymentGLEntries(
billPayment,
APAccountId,
gainLossAccountId,
baseCurrency
);
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,76 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
IBillPaymentEventCreatedPayload,
IBillPaymentEventDeletedPayload,
IBillPaymentEventEditedPayload,
} from '@/interfaces';
import { BillPaymentGLEntries } from './BillPaymentGLEntries';
@Service()
export class PaymentWriteGLEntriesSubscriber {
@Inject()
private billPaymentGLEntries: BillPaymentGLEntries;
/**
* Attaches events with handles.
*/
public attach(bus) {
bus.subscribe(events.billPayment.onCreated, this.handleWriteJournalEntries);
bus.subscribe(
events.billPayment.onEdited,
this.handleRewriteJournalEntriesOncePaymentEdited
);
bus.subscribe(
events.billPayment.onDeleted,
this.handleRevertJournalEntries
);
}
/**
* Handle bill payment writing journal entries once created.
*/
private handleWriteJournalEntries = async ({
tenantId,
billPayment,
trx,
}: IBillPaymentEventCreatedPayload) => {
// Records the journal transactions after bills payment
// and change diff acoount balance.
await this.billPaymentGLEntries.writePaymentGLEntries(
tenantId,
billPayment.id,
trx
);
};
/**
* Handle bill payment re-writing journal entries once the payment transaction be edited.
*/
private handleRewriteJournalEntriesOncePaymentEdited = async ({
tenantId,
billPayment,
trx,
}: IBillPaymentEventEditedPayload) => {
await this.billPaymentGLEntries.rewritePaymentGLEntries(
tenantId,
billPayment.id,
trx
);
};
/**
* Reverts journal entries once bill payment deleted.
*/
private handleRevertJournalEntries = async ({
tenantId,
billPaymentId,
trx,
}: IBillPaymentEventDeletedPayload) => {
await this.billPaymentGLEntries.revertPaymentGLEntries(
tenantId,
billPaymentId,
trx
);
};
}

View File

@@ -0,0 +1,61 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class BillPaymentTransactionTransformer extends Transformer {
/**
* Include these attributes to sale credit note object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['formattedPaymentAmount', 'formattedPaymentDate'];
};
/**
* Retrieve formatted bill payment amount.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedPaymentAmount = (entry): string => {
return formatNumber(entry.paymentAmount, {
currencyCode: entry.payment.currencyCode,
});
};
/**
* Retrieve formatted bill payment date.
* @param entry
* @returns
*/
protected formattedPaymentDate = (entry): string => {
return this.formatDate(entry.payment.paymentDate);
};
/**
*
* @param entry
* @returns
*/
public transform = (entry) => {
return {
billId: entry.billId,
billPaymentId: entry.billPaymentId,
paymentDate: entry.payment.paymentDate,
formattedPaymentDate: entry.formattedPaymentDate,
paymentAmount: entry.paymentAmount,
formattedPaymentAmount: entry.formattedPaymentAmount,
currencyCode: entry.payment.currencyCode,
paymentNumber: entry.payment.paymentNumber,
paymentReferenceNo: entry.payment.reference,
billNumber: entry.bill.billNumber,
billReferenceNo: entry.bill.referenceNo,
paymentAccountId: entry.payment.paymentAccountId,
paymentAccountName: entry.payment.paymentAccount.name,
paymentAccountSlug: entry.payment.paymentAccount.slug,
};
};
}

View File

@@ -0,0 +1,33 @@
import { IBillPayment } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class BillPaymentTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['formattedPaymentDate', 'formattedAmount'];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedPaymentDate = (billPayment: IBillPayment): string => {
return this.formatDate(billPayment.paymentDate);
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedAmount = (billPayment: IBillPayment): string => {
return formatNumber(billPayment.amount, {
currencyCode: billPayment.currencyCode,
});
};
}

View File

@@ -0,0 +1,713 @@
import { Inject, Service } from 'typedi';
import { sumBy, difference } from 'lodash';
import * as R from 'ramda';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import {
IBill,
IBillPaymentDTO,
IBillPaymentEntryDTO,
IBillPayment,
IBillPaymentsFilter,
IPaginationMeta,
IFilterMeta,
IBillPaymentEntry,
IBillPaymentEventCreatedPayload,
IBillPaymentEventEditedPayload,
IBillPaymentEventDeletedPayload,
IBillPaymentCreatingPayload,
IBillPaymentEditingPayload,
IBillPaymentDeletingPayload,
IVendor,
} from '@/interfaces';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { entriesAmountDiff, formatDateFields } from 'utils';
import { ServiceError } from '@/exceptions';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
import { BillPaymentTransformer } from './BillPaymentTransformer';
import { ERRORS } from './constants';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { TenantMetadata } from '@/system/models';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
/**
* Bill payments service.
* @service
*/
@Service('BillPayments')
export default class BillPaymentsService implements IBillPaymentsService {
@Inject()
tenancy: TenancyService;
@Inject()
journalService: JournalPosterService;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
eventPublisher: EventPublisher;
@Inject()
private transformer: TransformerInjectable;
@Inject()
uow: UnitOfWork;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
/**
* Validates the bill payment existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async getPaymentMadeOrThrowError(
tenantid: number,
paymentMadeId: number
) {
const { BillPayment } = this.tenancy.models(tenantid);
const billPayment = await BillPayment.query()
.withGraphFetched('entries')
.findById(paymentMadeId);
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
return billPayment;
}
/**
* Validates the payment account.
* @param {number} tenantId -
* @param {number} paymentAccountId
* @return {Promise<IAccountType>}
*/
private async getPaymentAccountOrThrowError(
tenantId: number,
paymentAccountId: number
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAccount = await accountRepository.findOneById(
paymentAccountId
);
if (!paymentAccount) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
}
// Validate the payment account type.
if (
!paymentAccount.isAccountType([
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
])
) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE);
}
return paymentAccount;
}
/**
* Validates the payment number uniqness.
* @param {number} tenantId -
* @param {string} paymentMadeNumber -
* @return {Promise<IBillPayment>}
*/
private async validatePaymentNumber(
tenantId: number,
paymentMadeNumber: string,
notPaymentMadeId?: number
) {
const { BillPayment } = this.tenancy.models(tenantId);
const foundBillPayment = await BillPayment.query().onBuild(
(builder: any) => {
builder.findOne('payment_number', paymentMadeNumber);
if (notPaymentMadeId) {
builder.whereNot('id', notPaymentMadeId);
}
}
);
if (foundBillPayment) {
throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE);
}
return foundBillPayment;
}
/**
* Validate whether the entries bills ids exist on the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async validateBillsExistance(
tenantId: number,
billPaymentEntries: { billId: number }[],
vendorId: number
) {
const { Bill } = this.tenancy.models(tenantId);
const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId);
const storedBills = await Bill.query()
.whereIn('id', entriesBillsIds)
.where('vendor_id', vendorId);
const storedBillsIds = storedBills.map((t: IBill) => t.id);
const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds);
if (notFoundBillsIds.length > 0) {
throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND);
}
// Validate the not opened bills.
const notOpenedBills = storedBills.filter((bill) => !bill.openedAt);
if (notOpenedBills.length > 0) {
throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, {
notOpenedBills,
});
}
return storedBills;
}
/**
* Validate wether the payment amount bigger than the payable amount.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @return {void}
*/
private async validateBillsDueAmount(
tenantId: number,
billPaymentEntries: IBillPaymentEntryDTO[],
oldPaymentEntries: IBillPaymentEntry[] = []
) {
const { Bill } = this.tenancy.models(tenantId);
const billsIds = billPaymentEntries.map(
(entry: IBillPaymentEntryDTO) => entry.billId
);
const storedBills = await Bill.query().whereIn('id', billsIds);
const storedBillsMap = new Map(
storedBills.map((bill) => {
const oldEntries = oldPaymentEntries.filter(
(entry) => entry.billId === bill.id
);
const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0;
return [
bill.id,
{ ...bill, dueAmount: bill.dueAmount + oldPaymentAmount },
];
})
);
interface invalidPaymentAmountError {
index: number;
due_amount: number;
}
const hasWrongPaymentAmount: invalidPaymentAmountError[] = [];
billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => {
const entryBill = storedBillsMap.get(entry.billId);
const { dueAmount } = entryBill;
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
});
if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT);
}
}
/**
* Validate the payment receive entries IDs existance.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
private async validateEntriesIdsExistance(
tenantId: number,
billPaymentId: number,
billPaymentEntries: IBillPaymentEntry[]
) {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
const entriesIds = billPaymentEntries
.filter((entry: any) => entry.id)
.map((entry: any) => entry.id);
const storedEntries = await BillPaymentEntry.query().where(
'bill_payment_id',
billPaymentId
);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.BILL_PAYMENT_ENTRIES_NOT_FOUND);
}
}
/**
* * Validate the payment vendor whether modified.
* @param {string} billPaymentNo
*/
private validateVendorNotModified(
billPaymentDTO: IBillPaymentDTO,
oldBillPayment: IBillPayment
) {
if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) {
throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY);
}
}
/**
* Validates the payment account currency code. The deposit account curreny
* should be equals the customer currency code or the base currency.
* @param {string} paymentAccountCurrency
* @param {string} customerCurrency
* @param {string} baseCurrency
* @throws {ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID)}
*/
public validateWithdrawalAccountCurrency = (
paymentAccountCurrency: string,
customerCurrency: string,
baseCurrency: string
) => {
if (
paymentAccountCurrency !== customerCurrency &&
paymentAccountCurrency !== baseCurrency
) {
throw new ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID);
}
};
/**
* Transforms create/edit DTO to model.
* @param {number} tenantId
* @param {IBillPaymentDTO} billPaymentDTO - Bill payment.
* @param {IBillPayment} oldBillPayment - Old bill payment.
* @return {Promise<IBillPayment>}
*/
async transformDTOToModel(
tenantId: number,
billPaymentDTO: IBillPaymentDTO,
vendor: IVendor,
oldBillPayment?: IBillPayment
): Promise<IBillPayment> {
const initialDTO = {
...formatDateFields(billPaymentDTO, ['paymentDate']),
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
currencyCode: vendor.currencyCode,
exchangeRate: billPaymentDTO.exchangeRate || 1,
entries: billPaymentDTO.entries,
};
return R.compose(
this.branchDTOTransform.transformDTO<IBillPayment>(tenantId)
)(initialDTO);
}
/**
* Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions.
*
* Precedures:-
* ------
* - Records the bill payment transaction.
* - Records the bill payment associated entries.
* - Increment the payment amount of the given vendor bills.
* - Decrement the vendor balance.
* - Records payment journal entries.
* ------
* @param {number} tenantId - Tenant id.
* @param {BillPaymentDTO} billPayment - Bill payment object.
*/
public async createBillPayment(
tenantId: number,
billPaymentDTO: IBillPaymentDTO
): Promise<IBillPayment> {
const { BillPayment, Contact } = this.tenancy.models(tenantId);
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Retrieves the payment vendor or throw not found error.
const vendor = await Contact.query()
.findById(billPaymentDTO.vendorId)
.modify('vendor')
.throwIfNotFound();
// Transform create DTO to model object.
const billPaymentObj = await this.transformDTOToModel(
tenantId,
billPaymentDTO,
vendor
);
// Validate the payment account existance and type.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
billPaymentObj.paymentAccountId
);
// Validate the payment number uniquiness.
if (billPaymentObj.paymentNumber) {
await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber);
}
// Validates the bills existance and associated to the given vendor.
await this.validateBillsExistance(
tenantId,
billPaymentObj.entries,
billPaymentDTO.vendorId
);
// Validates the bills due payment amount.
await this.validateBillsDueAmount(tenantId, billPaymentObj.entries);
// Validates the withdrawal account currency code.
this.validateWithdrawalAccountCurrency(
paymentAccount.currencyCode,
vendor.currencyCode,
tenantMeta.baseCurrency
);
// Writes bill payment transacation with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillPaymentCreating` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreating, {
tenantId,
billPaymentDTO,
trx,
} as IBillPaymentCreatingPayload);
// Writes the bill payment graph to the storage.
const billPayment = await BillPayment.query(trx).insertGraphAndFetch({
...billPaymentObj,
});
// Triggers `onBillPaymentCreated` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreated, {
tenantId,
billPayment,
billPaymentId: billPayment.id,
trx,
} as IBillPaymentEventCreatedPayload);
return billPayment;
});
}
/**
* Edits the details of the given bill payment.
*
* Preceducres:
* ------
* - Update the bill payment transaction.
* - Insert the new bill payment entries that have no ids.
* - Update the bill paymeny entries that have ids.
* - Delete the bill payment entries that not presented.
* - Re-insert the journal transactions and update the diff accounts balance.
* - Update the diff vendor balance.
* - Update the diff bill payment amount.
* ------
* @param {number} tenantId - Tenant id
* @param {Integer} billPaymentId
* @param {BillPaymentDTO} billPayment
* @param {IBillPayment} oldBillPayment
*/
public async editBillPayment(
tenantId: number,
billPaymentId: number,
billPaymentDTO
): Promise<IBillPayment> {
const { BillPayment, Contact } = this.tenancy.models(tenantId);
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
//
const oldBillPayment = await this.getPaymentMadeOrThrowError(
tenantId,
billPaymentId
);
//
const vendor = await Contact.query()
.modify('vendor')
.findById(billPaymentDTO.vendorId)
.throwIfNotFound();
// Transform bill payment DTO to model object.
const billPaymentObj = await this.transformDTOToModel(
tenantId,
billPaymentDTO,
vendor,
oldBillPayment
);
// Validate vendor not modified.
this.validateVendorNotModified(billPaymentDTO, oldBillPayment);
// Validate the payment account existance and type.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
billPaymentObj.paymentAccountId
);
// Validate the items entries IDs existance on the storage.
await this.validateEntriesIdsExistance(
tenantId,
billPaymentId,
billPaymentObj.entries
);
// Validate the bills existance and associated to the given vendor.
await this.validateBillsExistance(
tenantId,
billPaymentObj.entries,
billPaymentDTO.vendorId
);
// Validates the bills due payment amount.
await this.validateBillsDueAmount(
tenantId,
billPaymentObj.entries,
oldBillPayment.entries
);
// Validate the payment number uniquiness.
if (billPaymentObj.paymentNumber) {
await this.validatePaymentNumber(
tenantId,
billPaymentObj.paymentNumber,
billPaymentId
);
}
// Validates the withdrawal account currency code.
this.validateWithdrawalAccountCurrency(
paymentAccount.currencyCode,
vendor.currencyCode,
tenantMeta.baseCurrency
);
// Edits the bill transactions with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillPaymentEditing` event.
await this.eventPublisher.emitAsync(events.billPayment.onEditing, {
tenantId,
oldBillPayment,
billPaymentDTO,
trx,
} as IBillPaymentEditingPayload);
// Deletes the bill payment transaction graph from the storage.
const billPayment = await BillPayment.query(trx).upsertGraphAndFetch({
id: billPaymentId,
...billPaymentObj,
});
// Triggers `onBillPaymentEdited` event.
await this.eventPublisher.emitAsync(events.billPayment.onEdited, {
tenantId,
billPaymentId,
billPayment,
oldBillPayment,
trx,
} as IBillPaymentEventEditedPayload);
return billPayment;
});
}
/**
* Deletes the bill payment and associated transactions.
* @param {number} tenantId - Tenant id.
* @param {Integer} billPaymentId - The given bill payment id.
* @return {Promise}
*/
public async deleteBillPayment(tenantId: number, billPaymentId: number) {
const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId);
// Retrieve the bill payment or throw not found service error.
const oldBillPayment = await this.getPaymentMadeOrThrowError(
tenantId,
billPaymentId
);
// Deletes the bill transactions with associated transactions under
// unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillPaymentDeleting` payload.
await this.eventPublisher.emitAsync(events.billPayment.onDeleting, {
tenantId,
trx,
oldBillPayment,
} as IBillPaymentDeletingPayload);
// Deletes the bill payment assocaited entries.
await BillPaymentEntry.query(trx)
.where('bill_payment_id', billPaymentId)
.delete();
// Deletes the bill payment transaction.
await BillPayment.query(trx).where('id', billPaymentId).delete();
// Triggers `onBillPaymentDeleted` event.
await this.eventPublisher.emitAsync(events.billPayment.onDeleted, {
tenantId,
billPaymentId,
oldBillPayment,
trx,
} as IBillPaymentEventDeletedPayload);
});
}
/**
* Retrieve payment made associated bills.
* @param {number} tenantId -
* @param {number} billPaymentId -
*/
public async getPaymentBills(tenantId: number, billPaymentId: number) {
const { Bill } = this.tenancy.models(tenantId);
const billPayment = await this.getPaymentMadeOrThrowError(
tenantId,
billPaymentId
);
const paymentBillsIds = billPayment.entries.map((entry) => entry.id);
const bills = await Bill.query().whereIn('id', paymentBillsIds);
return bills;
}
/**
* Retrieve bill payment.
* @param {number} tenantId
* @param {number} billPyamentId
* @return {Promise<IBillPayment>}
*/
public async getBillPayment(
tenantId: number,
billPyamentId: number
): Promise<IBillPayment> {
const { BillPayment } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query()
.withGraphFetched('entries.bill')
.withGraphFetched('vendor')
.withGraphFetched('paymentAccount')
.withGraphFetched('transactions')
.withGraphFetched('branch')
.findById(billPyamentId);
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
return this.transformer.transform(
tenantId,
billPayment,
new BillPaymentTransformer()
);
}
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve bill payment paginted and filterable list.
* @param {number} tenantId
* @param {IBillPaymentsFilter} billPaymentsFilter
*/
public async listBillPayments(
tenantId: number,
filterDTO: IBillPaymentsFilter
): Promise<{
billPayments: IBillPayment;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { BillPayment } = this.tenancy.models(tenantId);
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
BillPayment,
filter
);
const { results, pagination } = await BillPayment.query()
.onBuild((builder) => {
builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount');
dynamicList.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformes the bill payments models to POJO.
const billPayments = await this.transformer.transform(
tenantId,
results,
new BillPaymentTransformer()
);
return {
billPayments,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
/**
* Saves bills payment amount changes different.
* @param {number} tenantId -
* @param {IBillPaymentEntryDTO[]} paymentMadeEntries -
* @param {IBillPaymentEntryDTO[]} oldPaymentMadeEntries -
*/
public async saveChangeBillsPaymentAmount(
tenantId: number,
paymentMadeEntries: IBillPaymentEntryDTO[],
oldPaymentMadeEntries?: IBillPaymentEntryDTO[],
trx?: Knex.Transaction
): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
const opers: Promise<void>[] = [];
const diffEntries = entriesAmountDiff(
paymentMadeEntries,
oldPaymentMadeEntries,
'paymentAmount',
'billId'
);
diffEntries.forEach(
(diffEntry: { paymentAmount: number; billId: number }) => {
if (diffEntry.paymentAmount === 0) {
return;
}
const oper = Bill.changePaymentAmount(
diffEntry.billId,
diffEntry.paymentAmount,
trx
);
opers.push(oper);
}
);
await Promise.all(opers);
}
/**
* Validates the given vendor has no associated payments.
* @param {number} tenantId
* @param {number} vendorId
*/
public async validateVendorHasNoPayments(tenantId: number, vendorId: number) {
const { BillPayment } = this.tenancy.models(tenantId);
const payments = await BillPayment.query().where('vendor_id', vendorId);
if (payments.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_PAYMENTS);
}
}
}

View File

@@ -0,0 +1,102 @@
import { Inject, Service } from 'typedi';
import { omit } from 'lodash';
import TenancyService from '@/services/Tenancy/TenancyService';
import { IBill, IBillPayment, IBillReceivePageEntry } from '@/interfaces';
import { ERRORS } from './constants';
import { ServiceError } from '@/exceptions';
/**
* Bill payments edit and create pages services.
*/
@Service()
export default class BillPaymentsPages {
@Inject()
tenancy: TenancyService;
/**
* Retrieve bill payment with associated metadata.
* @param {number} billPaymentId - The bill payment id.
* @return {object}
*/
public async getBillPaymentEditPage(
tenantId: number,
billPaymentId: number
): Promise<{
billPayment: Omit<IBillPayment, 'entries'>;
entries: IBillReceivePageEntry[];
}> {
const { BillPayment, Bill } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query()
.findById(billPaymentId)
.withGraphFetched('entries.bill');
// Throw not found the bill payment.
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
const paymentEntries = billPayment.entries.map((entry) => ({
...this.mapBillToPageEntry(entry.bill),
dueAmount: entry.bill.dueAmount + entry.paymentAmount,
paymentAmount: entry.paymentAmount,
}));
const resPayableBills = await Bill.query()
.modify('opened')
.modify('dueBills')
.where('vendor_id', billPayment.vendorId)
.whereNotIn(
'id',
billPayment.entries.map((e) => e.billId)
)
.orderBy('bill_date', 'ASC');
// Mapping the payable bills to entries.
const restPayableEntries = resPayableBills.map(this.mapBillToPageEntry);
const entries = [...paymentEntries, ...restPayableEntries];
return {
billPayment: omit(billPayment, ['entries']),
entries,
};
}
/**
* Retrieve the payable entries of the new page once vendor be selected.
* @param {number} tenantId
* @param {number} vendorId
*/
public async getNewPageEntries(
tenantId: number,
vendorId: number
): Promise<IBillReceivePageEntry[]> {
const { Bill } = this.tenancy.models(tenantId);
// Retrieve all payable bills that assocaited to the payment made transaction.
const payableBills = await Bill.query()
.modify('opened')
.modify('dueBills')
.where('vendor_id', vendorId)
.orderBy('bill_date', 'ASC');
return payableBills.map(this.mapBillToPageEntry);
}
/**
* Retrive edit page invoices entries from the given sale invoices models.
* @param {ISaleInvoice[]} invoices - Invoices.
* @return {IPaymentReceiveEditPageEntry}
*/
private mapBillToPageEntry(bill: IBill): IBillReceivePageEntry {
return {
entryType: 'invoice',
billId: bill.id,
billNo: bill.billNumber,
amount: bill.amount,
dueAmount: bill.dueAmount,
totalPaymentAmount: bill.paymentAmount,
paymentAmount: bill.paymentAmount,
currencyCode: bill.currencyCode,
date: bill.billDate,
};
}
}

View File

@@ -0,0 +1,17 @@
export const ERRORS = {
BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND',
PAYMENT_MADE_NOT_FOUND: 'PAYMENT_MADE_NOT_FOUND',
BILL_PAYMENT_NUMBER_NOT_UNQIUE: 'BILL_PAYMENT_NUMBER_NOT_UNQIUE',
PAYMENT_ACCOUNT_NOT_FOUND: 'PAYMENT_ACCOUNT_NOT_FOUND',
PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE:
'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND',
INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT',
PAYMENT_NUMBER_SHOULD_NOT_MODIFY: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY',
BILLS_NOT_OPENED_YET: 'BILLS_NOT_OPENED_YET',
VENDOR_HAS_PAYMENTS: 'VENDOR_HAS_PAYMENTS',
WITHDRAWAL_ACCOUNT_CURRENCY_INVALID: 'WITHDRAWAL_ACCOUNT_CURRENCY_INVALID',
};
export const DEFAULT_VIEWS = [];

View File

@@ -0,0 +1,35 @@
import { Service, Inject } from 'typedi';
import TenancyService from '@/services/Tenancy/TenancyService';
import { BillPaymentTransactionTransformer } from './BillPayments/BillPaymentTransactionTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class BillPaymentsService {
@Inject()
private tenancy: TenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the specific bill associated payment transactions.
* @param {number} tenantId
* @param {number} billId
* @returns {}
*/
public getBillPayments = async (tenantId: number, billId: number) => {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
const billsEntries = await BillPaymentEntry.query()
.where('billId', billId)
.withGraphJoined('payment.paymentAccount')
.withGraphJoined('bill')
.orderBy('payment:paymentDate', 'ASC');
return this.transformer.transform(
tenantId,
billsEntries,
new BillPaymentTransactionTransformer()
);
};
}

View File

@@ -0,0 +1,751 @@
import { omit, sumBy } from 'lodash';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { Knex } from 'knex';
import composeAsync from 'async/compose';
import events from '@/subscribers/events';
import InventoryService from '@/services/Inventory/Inventory';
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { formatDateFields, transformToMap } from 'utils';
import {
IBillDTO,
IBill,
ISystemUser,
IBillEditDTO,
IPaginationMeta,
IFilterMeta,
IBillsFilter,
IBillsService,
IItemEntry,
IItemEntryDTO,
IBillCreatedPayload,
IBillEditedPayload,
IBIllEventDeletedPayload,
IBillEventDeletingPayload,
IBillEditingPayload,
IBillCreatingPayload,
IVendor,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import { ERRORS } from './constants';
import EntriesService from '@/services/Entries';
import { PurchaseInvoiceTransformer } from './PurchaseInvoices/PurchaseInvoiceTransformer';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
/**
* Vendor bills services.
* @service
*/
@Service('Bills')
export default class BillsService
extends SalesInvoicesCost
implements IBillsService
{
@Inject()
inventoryService: InventoryService;
@Inject()
tenancy: TenancyService;
@Inject()
eventPublisher: EventPublisher;
@Inject('logger')
logger: any;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
itemsEntriesService: ItemsEntriesService;
@Inject()
journalPosterService: JournalPosterService;
@Inject()
entriesService: EntriesService;
@Inject()
transformer: TransformerInjectable;
@Inject()
uow: UnitOfWork;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
/**
* Validates the given bill existance.
* @async
* @param {number} tenantId -
* @param {number} billId -
*/
public async getBillOrThrowError(tenantId: number, billId: number) {
const { Bill } = this.tenancy.models(tenantId);
const foundBill = await Bill.query()
.findById(billId)
.withGraphFetched('entries');
if (!foundBill) {
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
}
return foundBill;
}
/**
* Validates the bill number existance.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async validateBillNumberExists(
tenantId: number,
billNumber: string,
notBillId?: number
) {
const { Bill } = this.tenancy.models(tenantId);
const foundBills = await Bill.query()
.where('bill_number', billNumber)
.onBuild((builder) => {
if (notBillId) {
builder.whereNot('id', notBillId);
}
});
if (foundBills.length > 0) {
throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS);
}
}
/**
* Validate the bill has no payment entries.
* @param {number} tenantId
* @param {number} billId - Bill id.
*/
private async validateBillHasNoEntries(tenantId, billId: number) {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
// Retireve the bill associate payment made entries.
const entries = await BillPaymentEntry.query().where('bill_id', billId);
if (entries.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES);
}
return entries;
}
/**
* Validate the bill number require.
* @param {string} billNo -
*/
private validateBillNoRequire(billNo: string) {
if (!billNo) {
throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED);
}
}
/**
* Validate bill transaction has no associated allocated landed cost transactions.
* @param {number} tenantId
* @param {number} billId
*/
private async validateBillHasNoLandedCost(tenantId: number, billId: number) {
const { BillLandedCost } = this.tenancy.models(tenantId);
const billLandedCosts = await BillLandedCost.query().where(
'billId',
billId
);
if (billLandedCosts.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS);
}
}
/**
* Validate transaction entries that have landed cost type should not be
* inventory items.
* @param {number} tenantId -
* @param {IItemEntryDTO[]} newEntriesDTO -
*/
public async validateCostEntriesShouldBeInventoryItems(
tenantId: number,
newEntriesDTO: IItemEntryDTO[]
) {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = newEntriesDTO.map((e) => e.itemId);
const entriesItems = await Item.query().whereIn('id', entriesItemsIds);
const entriesItemsById = transformToMap(entriesItems, 'id');
// Filter the landed cost entries that not associated with inventory item.
const nonInventoryHasCost = newEntriesDTO.filter((entry) => {
const item = entriesItemsById.get(entry.itemId);
return entry.landedCost && item.type !== 'inventory';
});
if (nonInventoryHasCost.length > 0) {
throw new ServiceError(
ERRORS.LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS
);
}
}
/**
* Sets the default cost account to the bill entries.
*/
private setBillEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
const items = await Item.query().whereIn('id', entriesItemsIds);
return entries.map((entry) => {
const item = items.find((i) => i.id === entry.itemId);
return {
...entry,
...(item.type !== 'inventory' && {
costAccountId: entry.costAccountId || item.costAccountId,
}),
};
});
};
}
/**
* Retrieve the bill entries total.
* @param {IItemEntry[]} entries
* @returns {number}
*/
private getBillEntriesTotal(tenantId: number, entries: IItemEntry[]): number {
const { ItemEntry } = this.tenancy.models(tenantId);
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
}
/**
* Retrieve the bill landed cost amount.
* @param {IBillDTO} billDTO
* @returns {number}
*/
private getBillLandedCostAmount(tenantId: number, billDTO: IBillDTO): number {
const costEntries = billDTO.entries.filter((entry) => entry.landedCost);
return this.getBillEntriesTotal(tenantId, costEntries);
}
/**
* Converts create bill DTO to model.
* @param {number} tenantId
* @param {IBillDTO} billDTO
* @param {IBill} oldBill
* @returns {IBill}
*/
private async billDTOToModel(
tenantId: number,
billDTO: IBillDTO,
vendor: IVendor,
authorizedUser: ISystemUser,
oldBill?: IBill
) {
const { ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e));
// Retrieve the landed cost amount from landed cost entries.
const landedCostAmount = this.getBillLandedCostAmount(tenantId, billDTO);
// Bill number from DTO or from auto-increment.
const billNumber = billDTO.billNumber || oldBill?.billNumber;
const initialEntries = billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
}));
const entries = await composeAsync(
// Sets the default cost account to the bill entries.
this.setBillEntriesDefaultAccounts(tenantId)
)(initialEntries);
const initialDTO = {
...formatDateFields(omit(billDTO, ['open', 'entries']), [
'billDate',
'dueDate',
]),
amount,
landedCostAmount,
currencyCode: vendor.currencyCode,
exchangeRate: billDTO.exchangeRate || 1,
billNumber,
entries,
// Avoid rewrite the open date in edit mode when already opened.
...(billDTO.open &&
!oldBill?.openedAt && {
openedAt: moment().toMySqlDateTime(),
}),
userId: authorizedUser.id,
};
return R.compose(
this.branchDTOTransform.transformDTO(tenantId),
this.warehouseDTOTransform.transformDTO(tenantId)
)(initialDTO);
}
/**
* Creates a new bill and stored it to the storage.
* ----
* Precedures.
* ----
* - Insert bill transactions to the storage.
* - Insert bill entries to the storage.
* - Increment the given vendor id.
* - Record bill journal transactions on the given accounts.
* - Record bill items inventory transactions.
* ----
* @param {number} tenantId - The given tenant id.
* @param {IBillDTO} billDTO -
* @return {Promise<IBill>}
*/
public async createBill(
tenantId: number,
billDTO: IBillDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill, Contact } = this.tenancy.models(tenantId);
// Retrieves the given bill vendor or throw not found error.
const vendor = await Contact.query()
.modify('vendor')
.findById(billDTO.vendorId)
.throwIfNotFound();
// Validate the bill number uniqiness on the storage.
await this.validateBillNumberExists(tenantId, billDTO.billNumber);
// Validate items IDs existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
billDTO.entries
);
// Validate non-purchasable items.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(
tenantId,
billDTO.entries
);
// Validates the cost entries should be with inventory items.
await this.validateCostEntriesShouldBeInventoryItems(
tenantId,
billDTO.entries
);
// Transform the bill DTO to model object.
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
vendor,
authorizedUser
);
// Write new bill transaction with associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillCreating` event.
await this.eventPublisher.emitAsync(events.bill.onCreating, {
trx,
billDTO,
tenantId,
} as IBillCreatingPayload);
// Inserts the bill graph object to the storage.
const bill = await Bill.query(trx).upsertGraph(billObj);
// Triggers `onBillCreated` event.
await this.eventPublisher.emitAsync(events.bill.onCreated, {
tenantId,
bill,
billId: bill.id,
trx,
} as IBillCreatedPayload);
return bill;
});
}
/**
* Edits details of the given bill id with associated entries.
*
* Precedures:
* -------
* - Update the bill transaction on the storage.
* - Update the bill entries on the storage and insert the not have id and delete
* once that not presented.
* - Increment the diff amount on the given vendor id.
* - Re-write the inventory transactions.
* - Re-write the bill journal transactions.
* ------
* @param {number} tenantId - The given tenant id.
* @param {Integer} billId - The given bill id.
* @param {IBillEditDTO} billDTO - The given new bill details.
* @return {Promise<IBill>}
*/
public async editBill(
tenantId: number,
billId: number,
billDTO: IBillEditDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill, Contact } = this.tenancy.models(tenantId);
const oldBill = await this.getBillOrThrowError(tenantId, billId);
// Retrieve vendor details or throw not found service error.
const vendor = await Contact.query()
.findById(billDTO.vendorId)
.modify('vendor')
.throwIfNotFound();
// Validate bill number uniqiness on the storage.
if (billDTO.billNumber) {
await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId);
}
// Validate the entries ids existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
billId,
'Bill',
billDTO.entries
);
// Validate the items ids existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
billDTO.entries
);
// Accept the purchasable items only.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(
tenantId,
billDTO.entries
);
// Transforms the bill DTO to model object.
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
vendor,
authorizedUser,
oldBill
);
// Validate landed cost entries that have allocated cost could not be deleted.
await this.entriesService.validateLandedCostEntriesNotDeleted(
oldBill.entries,
billObj.entries
);
// Validate new landed cost entries should be bigger than new entries.
await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries(
oldBill.entries,
billObj.entries
);
// Edits bill transactions and associated transactions under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillEditing` event.
await this.eventPublisher.emitAsync(events.bill.onEditing, {
trx,
tenantId,
oldBill,
billDTO,
} as IBillEditingPayload);
// Update the bill transaction.
const bill = await Bill.query(trx).upsertGraph({
id: billId,
...billObj,
});
// Triggers event `onBillEdited`.
await this.eventPublisher.emitAsync(events.bill.onEdited, {
tenantId,
billId,
oldBill,
bill,
trx,
} as IBillEditedPayload);
return bill;
});
}
/**
* Deletes the bill with associated entries.
* @param {Integer} billId
* @return {void}
*/
public async deleteBill(tenantId: number, billId: number) {
const { ItemEntry, Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await this.getBillOrThrowError(tenantId, billId);
// Validate the givne bill has no associated landed cost transactions.
await this.validateBillHasNoLandedCost(tenantId, billId);
// Validate the purchase bill has no assocaited payments transactions.
await this.validateBillHasNoEntries(tenantId, billId);
// Validate the given bill has no associated reconciled with vendor credits.
await this.validateBillHasNoAppliedToCredit(tenantId, billId);
// Deletes bill transaction with associated transactions under
// unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillDeleting` event.
await this.eventPublisher.emitAsync(events.bill.onDeleting, {
trx,
tenantId,
oldBill,
} as IBillEventDeletingPayload);
// Delete all associated bill entries.
await ItemEntry.query(trx)
.where('reference_type', 'Bill')
.where('reference_id', billId)
.delete();
// Delete the bill transaction.
await Bill.query(trx).findById(billId).delete();
// Triggers `onBillDeleted` event.
await this.eventPublisher.emitAsync(events.bill.onDeleted, {
tenantId,
billId,
oldBill,
trx,
} as IBIllEventDeletedPayload);
});
}
validateBillHasNoAppliedToCredit = async (
tenantId: number,
billId: number
) => {
const { VendorCreditAppliedBill } = this.tenancy.models(tenantId);
const appliedTransactions = await VendorCreditAppliedBill.query().where(
'billId',
billId
);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_APPLIED_TO_VENDOR_CREDIT);
}
};
/**
* Parses bills list filter DTO.
* @param filterDTO -
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve bills data table list.
* @param {number} tenantId -
* @param {IBillsFilter} billsFilter -
*/
public async getBills(
tenantId: number,
filterDTO: IBillsFilter
): Promise<{
bills: IBill;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Bill } = this.tenancy.models(tenantId);
// Parses bills list filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Bill,
filter
);
const { results, pagination } = await Bill.query()
.onBuild((builder) => {
builder.withGraphFetched('vendor');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Tranform the bills to POJO.
const bills = await this.transformer.transform(
tenantId,
results,
new PurchaseInvoiceTransformer()
);
return {
bills,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Retrieve all due bills or for specific given vendor id.
* @param {number} tenantId -
* @param {number} vendorId -
*/
public async getDueBills(
tenantId: number,
vendorId?: number
): Promise<IBill[]> {
const { Bill } = this.tenancy.models(tenantId);
const dueBills = await Bill.query().onBuild((query) => {
query.orderBy('bill_date', 'DESC');
query.modify('dueBills');
if (vendorId) {
query.where('vendor_id', vendorId);
}
});
return dueBills;
}
/**
* Retrieve the given bill details with associated items entries.
* @param {Integer} billId - Specific bill.
* @returns {Promise<IBill>}
*/
public async getBill(tenantId: number, billId: number): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
const bill = await Bill.query()
.findById(billId)
.withGraphFetched('vendor')
.withGraphFetched('entries.item')
.withGraphFetched('branch');
if (!bill) {
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
}
return this.transformer.transform(
tenantId,
bill,
new PurchaseInvoiceTransformer()
);
}
/**
* Mark the bill as open.
* @param {number} tenantId
* @param {number} billId
*/
public async openBill(tenantId: number, billId: number): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await this.getBillOrThrowError(tenantId, billId);
if (oldBill.isOpen) {
throw new ServiceError(ERRORS.BILL_ALREADY_OPEN);
}
//
return this.uow.withTransaction(tenantId, async (trx) => {
// Record the bill opened at on the storage.
await Bill.query(trx).findById(billId).patch({
openedAt: moment().toMySqlDateTime(),
});
});
}
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async recordInventoryTransactions(
tenantId: number,
billId: number,
override?: boolean,
trx?: Knex.Transaction
): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
// Retireve bill with assocaited entries and allocated cost entries.
const bill = await Bill.query(trx)
.findById(billId)
.withGraphFetched('entries.allocatedCostEntries');
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
bill.entries
);
const transaction = {
transactionId: bill.id,
transactionType: 'Bill',
exchangeRate: bill.exchangeRate,
date: bill.billDate,
direction: 'IN',
entries: inventoryEntries,
createdAt: bill.createdAt,
warehouseId: bill.warehouseId,
};
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId,
transaction,
override,
trx
);
}
/**
* Reverts the inventory transactions of the given bill id.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
billId: number,
trx?: Knex.Transaction
) {
// Deletes the inventory transactions by the given reference id and type.
await this.inventoryService.deleteInventoryTransactions(
tenantId,
billId,
'Bill',
trx
);
}
/**
* Validate the given vendor has no associated bills transactions.
* @param {number} tenantId
* @param {number} vendorId - Vendor id.
*/
public async validateVendorHasNoBills(tenantId: number, vendorId: number) {
const { Bill } = this.tenancy.models(tenantId);
const bills = await Bill.query().where('vendor_id', vendorId);
if (bills.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_BILLS);
}
}
}

View File

@@ -0,0 +1,219 @@
import moment from 'moment';
import { sumBy } from 'lodash';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { AccountNormal, IBill, IItemEntry, ILedgerEntry } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
@Service()
export class BillGLEntries {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private ledgerStorage: LedgerStorageService;
/**
* Creates bill GL entries.
* @param {number} tenantId -
* @param {number} billId -
* @param {Knex.Transaction} trx -
*/
public writeBillGLEntries = async (
tenantId: number,
billId: number,
trx?: Knex.Transaction
) => {
const { accountRepository } = this.tenancy.repositories(tenantId);
const { Bill } = this.tenancy.models(tenantId);
// Retrieves bill with associated entries and landed costs.
const bill = await Bill.query(trx)
.findById(billId)
.withGraphFetched('entries.item')
.withGraphFetched('entries.allocatedCostEntries')
.withGraphFetched('locatedLandedCosts.allocateEntries');
// Finds or create a A/P account based on the given currency.
const APAccount = await accountRepository.findOrCreateAccountsPayable(
bill.currencyCode,
{},
trx
);
const billLedger = this.getBillLedger(bill, APAccount.id);
// Commit the GL enties on the storage.
await this.ledgerStorage.commit(tenantId, billLedger, trx);
};
/**
* Reverts the given bill GL entries.
* @param {number} tenantId
* @param {number} billId
* @param {Knex.Transaction} trx
*/
public revertBillGLEntries = async (
tenantId: number,
billId: number,
trx?: Knex.Transaction
) => {
await this.ledgerStorage.deleteByReference(tenantId, billId, 'Bill', trx);
};
/**
* Rewrites the given bill GL entries.
* @param {number} tenantId
* @param {number} billId
* @param {Knex.Transaction} trx
*/
public rewriteBillGLEntries = async (
tenantId: number,
billId: number,
trx?: Knex.Transaction
) => {
// Reverts the bill GL entries.
await this.revertBillGLEntries(tenantId, billId, trx);
// Writes the bill GL entries.
await this.writeBillGLEntries(tenantId, billId, trx);
};
/**
* Retrieves the bill common entry.
* @param {IBill} bill
* @returns {ILedgerEntry}
*/
private getBillCommonEntry = (bill: IBill) => {
return {
debit: 0,
credit: 0,
currencyCode: bill.currencyCode,
exchangeRate: bill.exchangeRate || 1,
transactionId: bill.id,
transactionType: 'Bill',
date: moment(bill.billDate).format('YYYY-MM-DD'),
userId: bill.userId,
referenceNumber: bill.referenceNo,
transactionNumber: bill.billNumber,
branchId: bill.branchId,
projectId: bill.projectId,
createdAt: bill.createdAt,
};
};
/**
* Retrieves the bill item inventory/cost entry.
* @param {IBill} bill -
* @param {IItemEntry} entry -
* @param {number} index -
*/
private getBillItemEntry = R.curry(
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
const localAmount = bill.exchangeRate * entry.amount;
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
return {
...commonJournalMeta,
debit: localAmount + landedCostAmount,
accountId:
['inventory'].indexOf(entry.item.type) !== -1
? entry.item.inventoryAccountId
: entry.costAccountId,
index: index + 1,
indexGroup: 10,
itemId: entry.itemId,
itemQuantity: entry.quantity,
accountNormal: AccountNormal.DEBIT,
};
}
);
/**
* Retrieves the bill landed cost entry.
* @param {IBill} bill -
* @param {} landedCost -
* @param {number} index -
*/
private getBillLandedCostEntry = R.curry(
(bill: IBill, landedCost, index: number): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
return {
...commonJournalMeta,
credit: landedCost.amount,
accountId: landedCost.costAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 20,
};
}
);
/**
* Retrieves the bill payable entry.
* @param {number} payableAccountId
* @param {IBill} bill
* @returns {ILedgerEntry}
*/
private getBillPayableEntry = (
payableAccountId: number,
bill: IBill
): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
return {
...commonJournalMeta,
credit: bill.localAmount,
accountId: payableAccountId,
contactId: bill.vendorId,
accountNormal: AccountNormal.CREDIT,
index: 1,
indexGroup: 5,
};
};
/**
* Retrieves the given bill GL entries.
* @param {IBill} bill
* @param {number} payableAccountId
* @returns {ILedgerEntry[]}
*/
private getBillGLEntries = (
bill: IBill,
payableAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
const itemEntryTransformer = this.getBillItemEntry(bill);
const landedCostTransformer = this.getBillLandedCostEntry(bill);
const itemsEntries = bill.entries.map(itemEntryTransformer);
const landedCostEntries = bill.locatedLandedCosts.map(
landedCostTransformer
);
// Allocate cost entries journal entries.
return [payableEntry, ...itemsEntries, ...landedCostEntries];
};
/**
* Retrieves the given bill ledger.
* @param {IBill} bill
* @param {number} payableAccountId
* @returns {Ledger}
*/
private getBillLedger = (bill: IBill, payableAccountId: number) => {
const entries = this.getBillGLEntries(bill, payableAccountId);
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,70 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import BillsService from '@/services/Purchases/Bills';
import {
IBillCreatedPayload,
IBillEditedPayload,
IBIllEventDeletedPayload,
} from '@/interfaces';
import { BillGLEntries } from './BillGLEntries';
@Service()
export class BillGLEntriesSubscriber {
@Inject()
tenancy: TenancyService;
@Inject()
billGLEntries: BillGLEntries;
/**
* Attachs events with handles.
*/
attach(bus) {
bus.subscribe(
events.bill.onCreated,
this.handlerWriteJournalEntriesOnCreate
);
bus.subscribe(
events.bill.onEdited,
this.handleOverwriteJournalEntriesOnEdit
);
bus.subscribe(events.bill.onDeleted, this.handlerDeleteJournalEntries);
}
/**
* Handles writing journal entries once bill created.
* @param {IBillCreatedPayload} payload -
*/
private handlerWriteJournalEntriesOnCreate = async ({
tenantId,
billId,
trx,
}: IBillCreatedPayload) => {
await this.billGLEntries.writeBillGLEntries(tenantId, billId, trx);
};
/**
* Handles the overwriting journal entries once bill edited.
* @param {IBillEditedPayload} payload -
*/
private handleOverwriteJournalEntriesOnEdit = async ({
tenantId,
billId,
trx,
}: IBillEditedPayload) => {
await this.billGLEntries.rewriteBillGLEntries(tenantId, billId, trx);
};
/**
* Handles revert journal entries on bill deleted.
* @param {IBIllEventDeletedPayload} payload -
*/
private handlerDeleteJournalEntries = async ({
tenantId,
billId,
trx,
}: IBIllEventDeletedPayload) => {
await this.billGLEntries.revertBillGLEntries(tenantId, billId, trx);
};
}

View File

@@ -0,0 +1,76 @@
import { Knex } from 'knex';
import async from 'async';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { BillPaymentGLEntries } from '../BillPayments/BillPaymentGLEntries';
@Service()
export class BillPaymentsGLEntriesRewrite {
@Inject()
public tenancy: HasTenancyService;
@Inject()
public paymentGLEntries: BillPaymentGLEntries;
/**
* Rewrites payments GL entries that associated to the given bill.
* @param {number} tenantId
* @param {number} billId
* @param {Knex.Transaction} trx
*/
public rewriteBillPaymentsGLEntries = async (
tenantId: number,
billId: number,
trx?: Knex.Transaction
) => {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
const billPaymentEntries = await BillPaymentEntry.query().where(
'billId',
billId
);
const paymentsIds = billPaymentEntries.map((e) => e.billPaymentId);
await this.rewritePaymentsGLEntriesQueue(tenantId, paymentsIds, trx);
};
/**
* Rewrites the payments GL entries under async queue.
* @param {number} tenantId
* @param {number[]} paymentsIds
* @param {Knex.Transaction} trx
*/
public rewritePaymentsGLEntriesQueue = async (
tenantId: number,
paymentsIds: number[],
trx?: Knex.Transaction
) => {
// Initiate a new queue for accounts balance mutation.
const rewritePaymentGL = async.queue(this.rewritePaymentsGLEntriesTask, 10);
paymentsIds.forEach((paymentId: number) => {
rewritePaymentGL.push({ paymentId, trx, tenantId });
});
//
if (paymentsIds.length > 0) await rewritePaymentGL.drain();
};
/**
* Rewrites the payments GL entries task.
* @param {number} tenantId -
* @param {number} paymentId -
* @param {Knex.Transaction} trx -
* @returns {Promise<void>}
*/
public rewritePaymentsGLEntriesTask = async ({
tenantId,
paymentId,
trx,
}) => {
await this.paymentGLEntries.rewritePaymentGLEntries(
tenantId,
paymentId,
trx
);
};
}

View File

@@ -0,0 +1,36 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { IBillEditedPayload } from '@/interfaces';
import { BillPaymentsGLEntriesRewrite } from './BillPaymentsGLEntriesRewrite';
@Service()
export class BillPaymentsGLEntriesRewriteSubscriber {
@Inject()
private billPaymentGLEntriesRewrite: BillPaymentsGLEntriesRewrite;
/**
* Attachs events with handles.
*/
attach(bus) {
bus.subscribe(
events.bill.onEdited,
this.handlerRewritePaymentsGLOnBillEdited
);
}
/**
* Handles writing journal entries once bill created.
* @param {IBillCreatedPayload} payload -
*/
private handlerRewritePaymentsGLOnBillEdited = async ({
tenantId,
billId,
trx,
}: IBillEditedPayload) => {
await this.billPaymentGLEntriesRewrite.rewriteBillPaymentsGLEntries(
tenantId,
billId,
trx
);
};
}

View File

@@ -0,0 +1,101 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
IAllocatedLandedCostCreatedPayload,
IBillLandedCost,
ILandedCostDTO,
} from '@/interfaces';
import BaseLandedCostService from './BaseLandedCost';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
export default class AllocateLandedCost extends BaseLandedCostService {
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* =================================
* - Allocate landed cost.
* =================================
* - Validates the allocate cost not the same purchase invoice id.
* - Get the given bill (purchase invoice) or throw not found error.
* - Get the given landed cost transaction or throw not found error.
* - Validate landed cost transaction has enough unallocated cost amount.
* - Validate landed cost transaction entry has enough unallocated cost amount.
* - Validate allocate entries existance and associated with cost bill transaction.
* - Writes inventory landed cost transaction.
* - Increment the allocated landed cost transaction.
* - Increment the allocated landed cost transaction entry.
* --------------------------------
* @param {ILandedCostDTO} landedCostDTO - Landed cost DTO.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Purchase invoice id.
*/
public allocateLandedCost = async (
tenantId: number,
allocateCostDTO: ILandedCostDTO,
billId: number
): Promise<IBillLandedCost> => {
const { BillLandedCost } = this.tenancy.models(tenantId);
// Retrieve total cost of allocated items.
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
// Retrieve the purchase invoice or throw not found error.
const bill = await this.billsService.getBillOrThrowError(tenantId, billId);
// Retrieve landed cost transaction or throw not found service error.
const costTransaction = await this.getLandedCostOrThrowError(
tenantId,
allocateCostDTO.transactionType,
allocateCostDTO.transactionId
);
// Retrieve landed cost transaction entries.
const costTransactionEntry = await this.getLandedCostEntry(
tenantId,
allocateCostDTO.transactionType,
allocateCostDTO.transactionId,
allocateCostDTO.transactionEntryId
);
// Validates allocate cost items association with the purchase invoice entries.
this.validateAllocateCostItems(bill.entries, allocateCostDTO.items);
// Validate the amount of cost with unallocated landed cost.
this.validateLandedCostEntryAmount(
costTransactionEntry.unallocatedCostAmount,
amount
);
// Transformes DTO to bill landed cost model object.
const billLandedCostObj = this.transformToBillLandedCost(
allocateCostDTO,
bill,
costTransaction,
costTransactionEntry
);
// Saves landed cost transactions with associated tranasctions under
// unit-of-work eniverment.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Save the bill landed cost model.
const billLandedCost = await BillLandedCost.query(trx).insertGraph(
billLandedCostObj
);
// Triggers `onBillLandedCostCreated` event.
await this.eventPublisher.emitAsync(events.billLandedCost.onCreated, {
tenantId,
bill,
billLandedCostId: billLandedCost.id,
billLandedCost,
costTransaction,
costTransactionEntry,
trx,
} as IAllocatedLandedCostCreatedPayload);
return billLandedCost;
});
};
}

View File

@@ -0,0 +1,200 @@
import { Inject, Service } from 'typedi';
import { difference, sumBy } from 'lodash';
import BillsService from '../Bills';
import { ServiceError } from '@/exceptions';
import {
IItemEntry,
IBill,
ILandedCostItemDTO,
ILandedCostDTO,
IBillLandedCostTransaction,
ILandedCostTransaction,
ILandedCostTransactionEntry,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import TransactionLandedCost from './TransctionLandedCost';
import { ERRORS } from './utils';
import { CONFIG } from './utils';
@Service()
export default class BaseLandedCostService {
@Inject()
public billsService: BillsService;
@Inject()
public tenancy: HasTenancyService;
@Inject()
public transactionLandedCost: TransactionLandedCost;
/**
* Validates allocate cost items association with the purchase invoice entries.
* @param {IItemEntry[]} purchaseInvoiceEntries
* @param {ILandedCostItemDTO[]} landedCostItems
*/
protected validateAllocateCostItems = (
purchaseInvoiceEntries: IItemEntry[],
landedCostItems: ILandedCostItemDTO[]
): void => {
// Purchase invoice entries items ids.
const purchaseInvoiceItems = purchaseInvoiceEntries.map((e) => e.id);
const landedCostItemsIds = landedCostItems.map((item) => item.entryId);
// Not found items ids.
const notFoundItemsIds = difference(
purchaseInvoiceItems,
landedCostItemsIds
);
// Throw items ids not found service error.
if (notFoundItemsIds.length > 0) {
throw new ServiceError(ERRORS.LANDED_COST_ITEMS_IDS_NOT_FOUND);
}
};
/**
* Transformes DTO to bill landed cost model object.
* @param {ILandedCostDTO} landedCostDTO
* @param {IBill} bill
* @param {ILandedCostTransaction} costTransaction
* @param {ILandedCostTransactionEntry} costTransactionEntry
* @returns
*/
protected transformToBillLandedCost(
landedCostDTO: ILandedCostDTO,
bill: IBill,
costTransaction: ILandedCostTransaction,
costTransactionEntry: ILandedCostTransactionEntry
) {
const amount = sumBy(landedCostDTO.items, 'cost');
return {
billId: bill.id,
fromTransactionType: landedCostDTO.transactionType,
fromTransactionId: landedCostDTO.transactionId,
fromTransactionEntryId: landedCostDTO.transactionEntryId,
amount,
currencyCode: costTransaction.currencyCode,
exchangeRate: costTransaction.exchangeRate || 1,
allocationMethod: landedCostDTO.allocationMethod,
allocateEntries: landedCostDTO.items,
description: landedCostDTO.description,
costAccountId: costTransactionEntry.costAccountId,
};
}
/**
* Retrieve the cost transaction or throw not found error.
* @param {number} tenantId
* @param {transactionType} transactionType -
* @param {transactionId} transactionId -
*/
public getLandedCostOrThrowError = async (
tenantId: number,
transactionType: string,
transactionId: number
) => {
const Model = this.transactionLandedCost.getModel(
tenantId,
transactionType
);
const model = await Model.query().findById(transactionId);
if (!model) {
throw new ServiceError(ERRORS.LANDED_COST_TRANSACTION_NOT_FOUND);
}
return this.transactionLandedCost.transformToLandedCost(
transactionType,
model
);
};
/**
* Retrieve the landed cost entries.
* @param {number} tenantId
* @param {string} transactionType
* @param {number} transactionId
* @returns
*/
public getLandedCostEntry = async (
tenantId: number,
transactionType: string,
transactionId: number,
transactionEntryId: number
): Promise<any> => {
const Model = this.transactionLandedCost.getModel(
tenantId,
transactionType
);
const relation = CONFIG.COST_TYPES[transactionType].entries;
const entry = await Model.relatedQuery(relation)
.for(transactionId)
.findOne('id', transactionEntryId)
.where('landedCost', true)
.onBuild((q) => {
if (transactionType === 'Bill') {
q.withGraphFetched('item');
} else if (transactionType === 'Expense') {
q.withGraphFetched('expenseAccount');
}
});
if (!entry) {
throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND);
}
return this.transactionLandedCost.transformToLandedCostEntry(
transactionType,
entry
);
};
/**
* Retrieve allocate items cost total.
* @param {ILandedCostDTO} landedCostDTO
* @returns {number}
*/
protected getAllocateItemsCostTotal = (
landedCostDTO: ILandedCostDTO
): number => {
return sumBy(landedCostDTO.items, 'cost');
};
/**
* Validates the landed cost entry amount.
* @param {number} unallocatedCost -
* @param {number} amount -
*/
protected validateLandedCostEntryAmount = (
unallocatedCost: number,
amount: number
): void => {
if (unallocatedCost < amount) {
throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT);
}
};
/**
* Retrieve the give bill landed cost or throw not found service error.
* @param {number} tenantId - Tenant id.
* @param {number} landedCostId - Landed cost id.
* @returns {Promise<IBillLandedCost>}
*/
public getBillLandedCostOrThrowError = async (
tenantId: number,
landedCostId: number
): Promise<IBillLandedCostTransaction> => {
const { BillLandedCost } = this.tenancy.models(tenantId);
// Retrieve the bill landed cost model.
const billLandedCost = await BillLandedCost.query().findById(landedCostId);
if (!billLandedCost) {
throw new ServiceError(ERRORS.BILL_LANDED_COST_NOT_FOUND);
}
return billLandedCost;
};
}

View File

@@ -0,0 +1,170 @@
import { Inject, Service } from 'typedi';
import { omit } from 'lodash';
import * as R from 'ramda';
import * as qim from 'qim';
import { IBillLandedCostTransaction } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { formatNumber } from 'utils';
import I18nService from '@/services/I18n/I18nService';
@Service()
export default class BillAllocatedLandedCostTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private i18nService: I18nService;
/**
* Retrieve the bill associated landed cost transactions.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<IBillLandedCostTransaction>}
*/
public getBillLandedCostTransactions = async (
tenantId: number,
billId: number
): Promise<IBillLandedCostTransaction> => {
const { BillLandedCost, Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill id or throw not found service error.
const bill = await Bill.query().findById(billId).throwIfNotFound();
// Retrieve the bill associated allocated landed cost with bill and expense entry.
const landedCostTransactions = await BillLandedCost.query()
.where('bill_id', billId)
.withGraphFetched('allocateEntries')
.withGraphFetched('allocatedFromBillEntry.item')
.withGraphFetched('allocatedFromExpenseEntry.expenseAccount')
.withGraphFetched('bill');
const transactionsJson = this.i18nService.i18nApply(
[[qim.$each, 'allocationMethodFormatted']],
landedCostTransactions.map((a) => a.toJSON()),
tenantId
);
return this.transformBillLandedCostTransactions(transactionsJson);
};
/**
*
* @param {IBillLandedCostTransaction[]} landedCostTransactions
* @returns
*/
private transformBillLandedCostTransactions = (
landedCostTransactions: IBillLandedCostTransaction[]
) => {
return landedCostTransactions.map(this.transformBillLandedCostTransaction);
};
/**
*
* @param {IBillLandedCostTransaction} transaction
* @returns
*/
private transformBillLandedCostTransaction = (
transaction: IBillLandedCostTransaction
) => {
const getTransactionName = R.curry(this.condBillLandedTransactionName)(
transaction.fromTransactionType
);
const getTransactionDesc = R.curry(
this.condBillLandedTransactionDescription
)(transaction.fromTransactionType);
return {
formattedAmount: formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode,
}),
...omit(transaction, [
'allocatedFromBillEntry',
'allocatedFromExpenseEntry',
]),
name: getTransactionName(transaction),
description: getTransactionDesc(transaction),
formattedLocalAmount: formatNumber(transaction.localAmount, {
currencyCode: 'USD',
}),
};
};
/**
* Retrieve bill landed cost tranaction name based on the given transaction type.
* @param transactionType
* @param transaction
* @returns
*/
private condBillLandedTransactionName = (
transactionType: string,
transaction
) => {
return R.cond([
[
R.always(R.equals(transactionType, 'Bill')),
this.getLandedBillTransactionName,
],
[
R.always(R.equals(transactionType, 'Expense')),
this.getLandedExpenseTransactionName,
],
])(transaction);
};
/**
*
* @param transaction
* @returns
*/
private getLandedBillTransactionName = (transaction): string => {
return transaction.allocatedFromBillEntry.item.name;
};
/**
*
* @param transaction
* @returns
*/
private getLandedExpenseTransactionName = (transaction): string => {
return transaction.allocatedFromExpenseEntry.expenseAccount.name;
};
/**
* Retrieve landed cost.
* @param transaction
* @returns
*/
private getLandedBillTransactionDescription = (transaction): string => {
return transaction.allocatedFromBillEntry.description;
};
/**
*
* @param transaction
* @returns
*/
private getLandedExpenseTransactionDescription = (transaction): string => {
return transaction.allocatedFromExpenseEntry.description;
};
/**
* Retrieve the bill landed cost transaction description based on transaction type.
* @param {string} tranasctionType
* @param transaction
* @returns
*/
private condBillLandedTransactionDescription = (
tranasctionType: string,
transaction
) => {
return R.cond([
[
R.always(R.equals(tranasctionType, 'Bill')),
this.getLandedBillTransactionDescription,
],
[
R.always(R.equals(tranasctionType, 'Expense')),
this.getLandedExpenseTransactionDescription,
],
])(transaction);
};
}

View File

@@ -0,0 +1,58 @@
import { Service } from 'typedi';
import { isEmpty } from 'lodash';
import {
IBill,
IItem,
ILandedCostTransactionEntry,
ILandedCostTransaction,
IItemEntry,
} from '@/interfaces';
@Service()
export default class BillLandedCost {
/**
* Retrieve the landed cost transaction from the given bill transaction.
* @param {IBill} bill - Bill transaction.
* @returns {ILandedCostTransaction} - Landed cost transaction.
*/
public transformToLandedCost = (bill: IBill): ILandedCostTransaction => {
const name = bill.billNumber || bill.referenceNo;
return {
id: bill.id,
name,
allocatedCostAmount: bill.allocatedCostAmount,
amount: bill.landedCostAmount,
unallocatedCostAmount: bill.unallocatedCostAmount,
transactionType: 'Bill',
currencyCode: bill.currencyCode,
exchangeRate: bill.exchangeRate,
...(!isEmpty(bill.entries) && {
entries: bill.entries.map(this.transformToLandedCostEntry),
}),
};
};
/**
* Transformes bill entry to landed cost entry.
* @param {IBill} bill - Bill model.
* @param {IItemEntry} billEntry - Bill entry.
* @return {ILandedCostTransactionEntry}
*/
public transformToLandedCostEntry(
billEntry: IItemEntry & { item: IItem }
): ILandedCostTransactionEntry {
return {
id: billEntry.id,
name: billEntry.item.name,
code: billEntry.item.code,
amount: billEntry.amount,
unallocatedCostAmount: billEntry.unallocatedCostAmount,
allocatedCostAmount: billEntry.allocatedCostAmount,
description: billEntry.description,
costAccountId: billEntry.costAccountId || billEntry.item.costAccountId,
};
}
}

View File

@@ -0,0 +1,59 @@
import { Service } from 'typedi';
import { isEmpty } from 'lodash';
import * as R from 'ramda';
import {
IExpense,
ILandedCostTransactionEntry,
IExpenseCategory,
IAccount,
ILandedCostTransaction,
} from '@/interfaces';
@Service()
export default class ExpenseLandedCost {
/**
* Retrieve the landed cost transaction from the given expense transaction.
* @param {IExpense} expense
* @returns {ILandedCostTransaction}
*/
public transformToLandedCost = (
expense: IExpense
): ILandedCostTransaction => {
const name = 'EXP-100';
return {
id: expense.id,
name,
amount: expense.landedCostAmount,
allocatedCostAmount: expense.allocatedCostAmount,
unallocatedCostAmount: expense.unallocatedCostAmount,
transactionType: 'Expense',
currencyCode: expense.currencyCode,
exchangeRate: expense.exchangeRate || 1,
...(!isEmpty(expense.categories) && {
entries: expense.categories.map(this.transformToLandedCostEntry),
}),
};
};
/**
* Transformes expense entry to landed cost entry.
* @param {IExpenseCategory & { expenseAccount: IAccount }} expenseEntry -
* @return {ILandedCostTransactionEntry}
*/
public transformToLandedCostEntry = (
expenseEntry: IExpenseCategory & { expenseAccount: IAccount }
): ILandedCostTransactionEntry => {
return {
id: expenseEntry.id,
name: expenseEntry.expenseAccount.name,
code: expenseEntry.expenseAccount.code,
amount: expenseEntry.amount,
description: expenseEntry.description,
allocatedCostAmount: expenseEntry.allocatedCostAmount,
unallocatedCostAmount: expenseEntry.unallocatedCostAmount,
costAccountId: expenseEntry.expenseAccount.id,
};
};
}

View File

@@ -0,0 +1,249 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import {
AccountNormal,
IBill,
IBillLandedCost,
IBillLandedCostEntry,
ILandedCostTransactionEntry,
ILedger,
ILedgerEntry,
} from '@/interfaces';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import { Service, Inject } from 'typedi';
import LedgerRepository from '@/services/Ledger/LedgerRepository';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import BaseLandedCostService from './BaseLandedCost';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export default class LandedCostGLEntries extends BaseLandedCostService {
@Inject()
private journalService: JournalPosterService;
@Inject()
private ledgerRepository: LedgerRepository;
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the landed cost GL common entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @returns
*/
private getLandedCostGLCommonEntry = (
bill: IBill,
allocatedLandedCost: IBillLandedCost
) => {
return {
date: bill.billDate,
currencyCode: allocatedLandedCost.currencyCode,
exchangeRate: allocatedLandedCost.exchangeRate,
transactionType: 'LandedCost',
transactionId: allocatedLandedCost.id,
transactionNumber: bill.billNumber,
referenceNumber: bill.referenceNo,
credit: 0,
debit: 0,
};
};
/**
* Retrieves the landed cost GL inventory entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {IBillLandedCostEntry} allocatedEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLInventoryEntry = (
bill: IBill,
allocatedLandedCost: IBillLandedCost,
allocatedEntry: IBillLandedCostEntry
): ILedgerEntry => {
const commonEntry = this.getLandedCostGLCommonEntry(
bill,
allocatedLandedCost
);
return {
...commonEntry,
debit: allocatedLandedCost.localAmount,
accountId: allocatedEntry.itemEntry.item.inventoryAccountId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the landed cost GL cost entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLCostEntry = (
bill: IBill,
allocatedLandedCost: IBillLandedCost,
fromTransactionEntry: ILandedCostTransactionEntry
): ILedgerEntry => {
const commonEntry = this.getLandedCostGLCommonEntry(
bill,
allocatedLandedCost
);
return {
...commonEntry,
credit: allocatedLandedCost.localAmount,
accountId: fromTransactionEntry.costAccountId,
index: 2,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieve allocated landed cost entry GL entries.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @param {IBillLandedCostEntry} allocatedEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLAllocateEntry = R.curry(
(
bill: IBill,
allocatedLandedCost: IBillLandedCost,
fromTransactionEntry: ILandedCostTransactionEntry,
allocatedEntry: IBillLandedCostEntry
): ILedgerEntry[] => {
const inventoryEntry = this.getLandedCostGLInventoryEntry(
bill,
allocatedLandedCost,
allocatedEntry
);
const costEntry = this.getLandedCostGLCostEntry(
bill,
allocatedLandedCost,
fromTransactionEntry
);
return [inventoryEntry, costEntry];
}
);
/**
* Compose the landed cost GL entries.
* @param {IBillLandedCost} allocatedLandedCost
* @param {IBill} bill
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedgerEntry[]}
*/
public getLandedCostGLEntries = (
allocatedLandedCost: IBillLandedCost,
bill: IBill,
fromTransactionEntry: ILandedCostTransactionEntry
): ILedgerEntry[] => {
const getEntry = this.getLandedCostGLAllocateEntry(
bill,
allocatedLandedCost,
fromTransactionEntry
);
return allocatedLandedCost.allocateEntries.map(getEntry).flat();
};
/**
* Retrieves the landed cost GL ledger.
* @param {IBillLandedCost} allocatedLandedCost
* @param {IBill} bill
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedger}
*/
public getLandedCostLedger = (
allocatedLandedCost: IBillLandedCost,
bill: IBill,
fromTransactionEntry: ILandedCostTransactionEntry
): ILedger => {
const entries = this.getLandedCostGLEntries(
allocatedLandedCost,
bill,
fromTransactionEntry
);
return new Ledger(entries);
};
/**
* Writes landed cost GL entries to the storage layer.
* @param {number} tenantId -
*/
public writeLandedCostGLEntries = async (
tenantId: number,
allocatedLandedCost: IBillLandedCost,
bill: IBill,
fromTransactionEntry: ILandedCostTransactionEntry,
trx?: Knex.Transaction
) => {
const ledgerEntries = this.getLandedCostGLEntries(
allocatedLandedCost,
bill,
fromTransactionEntry
);
await this.ledgerRepository.saveLedgerEntries(tenantId, ledgerEntries, trx);
};
/**
* Generates and writes GL entries of the given landed cost.
* @param {number} tenantId
* @param {number} billLandedCostId
* @param {Knex.Transaction} trx
*/
public createLandedCostGLEntries = async (
tenantId: number,
billLandedCostId: number,
trx?: Knex.Transaction
) => {
const { BillLandedCost } = this.tenancy.models(tenantId);
// Retrieve the bill landed cost transacion with associated
// allocated entries and items.
const allocatedLandedCost = await BillLandedCost.query(trx)
.findById(billLandedCostId)
.withGraphFetched('bill')
.withGraphFetched('allocateEntries.itemEntry.item');
// Retrieve the allocated from transactione entry.
const transactionEntry = await this.getLandedCostEntry(
tenantId,
allocatedLandedCost.fromTransactionType,
allocatedLandedCost.fromTransactionId,
allocatedLandedCost.fromTransactionEntryId
);
// Writes the given landed cost GL entries to the storage layer.
await this.writeLandedCostGLEntries(
tenantId,
allocatedLandedCost,
allocatedLandedCost.bill,
transactionEntry,
trx
);
};
/**
* Reverts GL entries of the given allocated landed cost transaction.
* @param {number} tenantId
* @param {number} landedCostId
* @param {Knex.Transaction} trx
*/
public revertLandedCostGLEntries = async (
tenantId: number,
landedCostId: number,
trx: Knex.Transaction
) => {
await this.journalService.revertJournalTransactions(
tenantId,
landedCostId,
'LandedCost',
trx
);
};
}

View File

@@ -0,0 +1,57 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import LandedCostGLEntries from './LandedCostGLEntries';
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '@/interfaces';
@Service()
export default class LandedCostGLEntriesSubscriber {
@Inject()
billLandedCostGLEntries: LandedCostGLEntries;
attach(bus) {
bus.subscribe(
events.billLandedCost.onCreated,
this.writeGLEntriesOnceLandedCostCreated
);
bus.subscribe(
events.billLandedCost.onDeleted,
this.revertGLEnteriesOnceLandedCostDeleted
);
}
/**
* Writes GL entries once landed cost transaction created.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
private writeGLEntriesOnceLandedCostCreated = async ({
tenantId,
billLandedCost,
trx,
}: IAllocatedLandedCostCreatedPayload) => {
await this.billLandedCostGLEntries.createLandedCostGLEntries(
tenantId,
billLandedCost.id,
trx
);
};
/**
* Reverts GL entries associated to landed cost transaction once deleted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
private revertGLEnteriesOnceLandedCostDeleted = async ({
tenantId,
oldBillLandedCost,
billId,
trx,
}: IAllocatedLandedCostDeletedPayload) => {
await this.billLandedCostGLEntries.revertLandedCostGLEntries(
tenantId,
oldBillLandedCost.id,
trx
);
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IBill, IBillLandedCostTransaction } from '@/interfaces';
import InventoryService from '@/services/Inventory/Inventory';
import { mergeLocatedWithBillEntries } from './utils';
@Service()
export default class LandedCostInventoryTransactions {
@Inject()
public inventoryService: InventoryService;
/**
* Records inventory transactions.
* @param {number} tenantId
* @param {IBillLandedCostTransaction} billLandedCost
* @param {IBill} bill -
*/
public recordInventoryTransactions = async (
tenantId: number,
billLandedCost: IBillLandedCostTransaction,
bill: IBill,
trx?: Knex.Transaction
) => {
// Retrieve the merged allocated entries with bill entries.
const allocateEntries = mergeLocatedWithBillEntries(
billLandedCost.allocateEntries,
bill.entries
);
// Mappes the allocate cost entries to inventory transactions.
const inventoryTransactions = allocateEntries.map((allocateEntry) => ({
date: bill.billDate,
itemId: allocateEntry.entry.itemId,
direction: 'IN',
quantity: null,
rate: allocateEntry.cost,
transactionType: 'LandedCost',
transactionId: billLandedCost.id,
entryId: allocateEntry.entryId,
}));
// Writes inventory transactions.
return this.inventoryService.recordInventoryTransactions(
tenantId,
inventoryTransactions,
false,
trx
);
};
/**
* Deletes the inventory transaction.
* @param {number} tenantId - Tenant id.
* @param {number} landedCostId - Landed cost id.
* @param {Knex.Transaction} trx - Knex transactions.
* @returns
*/
public removeInventoryTransactions = (
tenantId: number,
landedCostId: number,
trx?: Knex.Transaction
) => {
return this.inventoryService.deleteInventoryTransactions(
tenantId,
landedCostId,
'LandedCost',
trx
);
};
}

View File

@@ -0,0 +1,63 @@
import { Inject, Service } from 'typedi';
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import LandedCostInventoryTransactions from './LandedCostInventoryTransactions';
@Service()
export default class LandedCostInventoryTransactionsSubscriber {
@Inject()
landedCostInventory: LandedCostInventoryTransactions;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.billLandedCost.onCreated,
this.writeInventoryTransactionsOnceCreated
);
bus.subscribe(
events.billLandedCost.onDeleted,
this.revertInventoryTransactionsOnceDeleted
);
}
/**
* Writes inventory transactions of the landed cost transaction once created.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
private writeInventoryTransactionsOnceCreated = async ({
billLandedCost,
tenantId,
trx,
bill,
}: IAllocatedLandedCostCreatedPayload) => {
// Records the inventory transactions.
await this.landedCostInventory.recordInventoryTransactions(
tenantId,
billLandedCost,
bill,
trx
);
};
/**
* Reverts inventory transactions of the landed cost transaction once deleted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
private revertInventoryTransactionsOnceDeleted = async ({
tenantId,
oldBillLandedCost,
trx,
}: IAllocatedLandedCostDeletedPayload) => {
// Removes the inventory transactions.
await this.landedCostInventory.removeInventoryTransactions(
tenantId,
oldBillLandedCost.id,
trx
);
};
}

View File

@@ -0,0 +1,76 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import TransactionLandedCost from './TransctionLandedCost';
import { CONFIG } from './utils';
@Service()
export default class LandedCostSyncCostTransactions {
@Inject()
transactionLandedCost: TransactionLandedCost;
/**
* Allocate the landed cost amount to cost transactions.
* @param {number} tenantId -
* @param {string} transactionType
* @param {number} transactionId
*/
public incrementLandedCostAmount = async (
tenantId: number,
transactionType: string,
transactionId: number,
transactionEntryId: number,
amount: number,
trx?: Knex.Transaction
): Promise<void> => {
const Model = this.transactionLandedCost.getModel(
tenantId,
transactionType
);
const relation = CONFIG.COST_TYPES[transactionType].entries;
// Increment the landed cost transaction amount.
await Model.query(trx)
.where('id', transactionId)
.increment('allocatedCostAmount', amount);
// Increment the landed cost entry.
await Model.relatedQuery(relation, trx)
.for(transactionId)
.where('id', transactionEntryId)
.increment('allocatedCostAmount', amount);
};
/**
* Reverts the landed cost amount to cost transaction.
* @param {number} tenantId - Tenant id.
* @param {string} transactionType - Transaction type.
* @param {number} transactionId - Transaction id.
* @param {number} transactionEntryId - Transaction entry id.
* @param {number} amount - Amount
*/
public revertLandedCostAmount = async (
tenantId: number,
transactionType: string,
transactionId: number,
transactionEntryId: number,
amount: number,
trx?: Knex.Transaction
) => {
const Model = this.transactionLandedCost.getModel(
tenantId,
transactionType
);
const relation = CONFIG.COST_TYPES[transactionType].entries;
// Decrement the allocate cost amount of cost transaction.
await Model.query(trx)
.where('id', transactionId)
.decrement('allocatedCostAmount', amount);
// Decrement the allocated cost amount cost transaction entry.
await Model.relatedQuery(relation, trx)
.for(transactionId)
.where('id', transactionEntryId)
.decrement('allocatedCostAmount', amount);
};
}

View File

@@ -0,0 +1,67 @@
import { Service, Inject } from 'typedi';
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import LandedCostSyncCostTransactions from './LandedCostSyncCostTransactions';
@Service()
export default class LandedCostSyncCostTransactionsSubscriber {
@Inject()
landedCostSyncCostTransaction: LandedCostSyncCostTransactions;
/**
* Attaches events with handlers.
*/
attach(bus) {
bus.subscribe(
events.billLandedCost.onCreated,
this.incrementCostTransactionsOnceCreated
);
bus.subscribe(
events.billLandedCost.onDeleted,
this.decrementCostTransactionsOnceDeleted
);
}
/**
* Increment cost transactions once the landed cost allocated.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
private incrementCostTransactionsOnceCreated = async ({
tenantId,
billLandedCost,
trx,
}: IAllocatedLandedCostCreatedPayload) => {
// Increment landed cost amount on transaction and entry.
await this.landedCostSyncCostTransaction.incrementLandedCostAmount(
tenantId,
billLandedCost.fromTransactionType,
billLandedCost.fromTransactionId,
billLandedCost.fromTransactionEntryId,
billLandedCost.amount,
trx
);
};
/**
* Decrement cost transactions once the allocated landed cost reverted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
private decrementCostTransactionsOnceDeleted = async ({
oldBillLandedCost,
tenantId,
trx,
}: IAllocatedLandedCostDeletedPayload) => {
// Reverts the landed cost amount to the cost transaction.
await this.landedCostSyncCostTransaction.revertLandedCostAmount(
tenantId,
oldBillLandedCost.fromTransactionType,
oldBillLandedCost.fromTransactionId,
oldBillLandedCost.fromTransactionEntryId,
oldBillLandedCost.amount,
trx
);
};
}

View File

@@ -0,0 +1,140 @@
import { Inject, Service } from 'typedi';
import { ref } from 'objection';
import * as R from 'ramda';
import {
ILandedCostTransactionsQueryDTO,
ILandedCostTransaction,
ILandedCostTransactionDOJO,
ILandedCostTransactionEntry,
ILandedCostTransactionEntryDOJO,
} from '@/interfaces';
import TransactionLandedCost from './TransctionLandedCost';
import BillsService from '../Bills';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { formatNumber } from 'utils';
@Service()
export default class LandedCostTranasctions {
@Inject()
transactionLandedCost: TransactionLandedCost;
@Inject()
billsService: BillsService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the landed costs based on the given query.
* @param {number} tenantId
* @param {ILandedCostTransactionsQueryDTO} query
* @returns {Promise<ILandedCostTransaction[]>}
*/
public getLandedCostTransactions = async (
tenantId: number,
query: ILandedCostTransactionsQueryDTO
): Promise<ILandedCostTransaction[]> => {
const { transactionType } = query;
const Model = this.transactionLandedCost.getModel(
tenantId,
query.transactionType
);
// Retrieve the model entities.
const transactions = await Model.query().onBuild((q) => {
q.where('allocated_cost_amount', '<', ref('landed_cost_amount'));
if (query.transactionType === 'Bill') {
q.withGraphFetched('entries.item');
} else if (query.transactionType === 'Expense') {
q.withGraphFetched('categories.expenseAccount');
}
});
const transformLandedCost =
this.transactionLandedCost.transformToLandedCost(transactionType);
return R.compose(
this.transformLandedCostTransactions,
R.map(transformLandedCost)
)(transactions);
};
/**
*
* @param transactions
* @returns
*/
public transformLandedCostTransactions = (
transactions: ILandedCostTransaction[]
) => {
return R.map(this.transformLandedCostTransaction)(transactions);
};
/**
* Transformes the landed cost transaction.
* @param {ILandedCostTransaction} transaction
*/
public transformLandedCostTransaction = (
transaction: ILandedCostTransaction
): ILandedCostTransactionDOJO => {
const { currencyCode } = transaction;
// Formatted transaction amount.
const formattedAmount = formatNumber(transaction.amount, { currencyCode });
// Formatted transaction unallocated cost amount.
const formattedUnallocatedCostAmount = formatNumber(
transaction.unallocatedCostAmount,
{ currencyCode }
);
// Formatted transaction allocated cost amount.
const formattedAllocatedCostAmount = formatNumber(
transaction.allocatedCostAmount,
{ currencyCode }
);
return {
...transaction,
formattedAmount,
formattedUnallocatedCostAmount,
formattedAllocatedCostAmount,
entries: R.map(this.transformLandedCostEntry(transaction))(
transaction.entries
),
};
};
/**
*
* @param {ILandedCostTransaction} transaction
* @param {ILandedCostTransactionEntry} entry
* @returns {ILandedCostTransactionEntryDOJO}
*/
public transformLandedCostEntry = R.curry(
(
transaction: ILandedCostTransaction,
entry: ILandedCostTransactionEntry
): ILandedCostTransactionEntryDOJO => {
const { currencyCode } = transaction;
// Formatted entry amount.
const formattedAmount = formatNumber(entry.amount, { currencyCode });
// Formatted entry unallocated cost amount.
const formattedUnallocatedCostAmount = formatNumber(
entry.unallocatedCostAmount,
{ currencyCode }
);
// Formatted entry allocated cost amount.
const formattedAllocatedCostAmount = formatNumber(
entry.allocatedCostAmount,
{ currencyCode }
);
return {
...entry,
formattedAmount,
formattedUnallocatedCostAmount,
formattedAllocatedCostAmount,
};
}
);
}

View File

@@ -0,0 +1,78 @@
import Knex from 'knex';
import { Service, Inject } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import BaseLandedCost from './BaseLandedCost';
import { IAllocatedLandedCostDeletedPayload } from '@/interfaces';
@Service()
export default class RevertAllocatedLandedCost extends BaseLandedCost {
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* Deletes the allocated landed cost.
* ==================================
* - Delete bill landed cost transaction with associated allocate entries.
* - Delete the associated inventory transactions.
* - Decrement allocated amount of landed cost transaction and entry.
* - Revert journal entries.
* ----------------------------------
* @param {number} tenantId - Tenant id.
* @param {number} landedCostId - Landed cost id.
* @return {Promise<void>}
*/
public deleteAllocatedLandedCost = async (
tenantId: number,
landedCostId: number
): Promise<{
landedCostId: number;
}> => {
// Retrieves the bill landed cost.
const oldBillLandedCost = await this.getBillLandedCostOrThrowError(
tenantId,
landedCostId
);
// Deletes landed cost with associated transactions.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Delete landed cost transaction with assocaited locate entries.
await this.deleteLandedCost(tenantId, landedCostId, trx);
// Triggers the event `onBillLandedCostCreated`.
await this.eventPublisher.emitAsync(events.billLandedCost.onDeleted, {
tenantId,
oldBillLandedCost: oldBillLandedCost,
billId: oldBillLandedCost.billId,
trx,
} as IAllocatedLandedCostDeletedPayload);
return { landedCostId };
});
};
/**
* Deletes the landed cost transaction with assocaited allocate entries.
* @param {number} tenantId - Tenant id.
* @param {number} landedCostId - Landed cost id.
*/
public deleteLandedCost = async (
tenantId: number,
landedCostId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { BillLandedCost, BillLandedCostEntry } =
this.tenancy.models(tenantId);
// Deletes the bill landed cost allocated entries associated to landed cost.
await BillLandedCostEntry.query(trx)
.where('bill_located_cost_id', landedCostId)
.delete();
// Delete the bill landed cost from the storage.
await BillLandedCost.query(trx).where('id', landedCostId).delete();
};
}

View File

@@ -0,0 +1,88 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { Model } from 'objection';
import {
IBill,
IExpense,
ILandedCostTransaction,
ILandedCostTransactionEntry,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import BillLandedCost from './BillLandedCost';
import ExpenseLandedCost from './ExpenseLandedCost';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './utils';
@Service()
export default class TransactionLandedCost {
@Inject()
billLandedCost: BillLandedCost;
@Inject()
expenseLandedCost: ExpenseLandedCost;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the cost transaction code model.
* @param {number} tenantId - Tenant id.
* @param {string} transactionType - Transaction type.
* @returns
*/
public getModel = (tenantId: number, transactionType: string): Model => {
const Models = this.tenancy.models(tenantId);
const Model = Models[transactionType];
if (!Model) {
throw new ServiceError(ERRORS.COST_TYPE_UNDEFINED);
}
return Model;
};
/**
* Mappes the given expense or bill transaction to landed cost transaction.
* @param {string} transactionType - Transaction type.
* @param {IBill|IExpense} transaction - Expense or bill transaction.
* @returns {ILandedCostTransaction}
*/
public transformToLandedCost = R.curry(
(
transactionType: string,
transaction: IBill | IExpense
): ILandedCostTransaction => {
return R.compose(
R.when(
R.always(transactionType === 'Bill'),
this.billLandedCost.transformToLandedCost
),
R.when(
R.always(transactionType === 'Expense'),
this.expenseLandedCost.transformToLandedCost
)
)(transaction);
}
);
/**
* Transformes the given expense or bill entry to landed cost transaction entry.
* @param {string} transactionType
* @param {} transactionEntry
* @returns {ILandedCostTransactionEntry}
*/
public transformToLandedCostEntry = (
transactionType: 'Bill' | 'Expense',
transactionEntry
): ILandedCostTransactionEntry => {
return R.compose(
R.when(
R.always(transactionType === 'Bill'),
this.billLandedCost.transformToLandedCostEntry
),
R.when(
R.always(transactionType === 'Expense'),
this.expenseLandedCost.transformToLandedCostEntry
)
)(transactionEntry);
};
}

View File

@@ -0,0 +1,46 @@
import { IItemEntry, IBillLandedCostTransactionEntry } from '@/interfaces';
import { transformToMap } from 'utils';
export const ERRORS = {
COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED',
LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND',
COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE:
'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE',
BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND',
COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND',
LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND',
LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND',
COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT:
'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT',
ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL',
};
/**
* Merges item entry to bill located landed cost entry.
* @param {IBillLandedCostTransactionEntry[]} locatedEntries -
* @param {IItemEntry[]} billEntries -
* @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]}
*/
export const mergeLocatedWithBillEntries = (
locatedEntries: IBillLandedCostTransactionEntry[],
billEntries: IItemEntry[]
): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => {
const billEntriesByEntryId = transformToMap(billEntries, 'id');
return locatedEntries.map((entry) => ({
...entry,
entry: billEntriesByEntryId.get(entry.entryId),
}));
};
export const CONFIG = {
COST_TYPES: {
Expense: {
entries: 'categories',
},
Bill: {
entries: 'entries',
},
},
};

View File

@@ -0,0 +1,86 @@
import { IBill } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class PurchaseInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedBillDate',
'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount',
'formattedBalance',
'formattedDueAmount',
'formattedExchangeRate',
];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedBillDate = (bill: IBill): string => {
return this.formatDate(bill.billDate);
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedDueDate = (bill: IBill): string => {
return this.formatDate(bill.dueDate);
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedAmount = (bill): string => {
return formatNumber(bill.amount, { currencyCode: bill.currencyCode });
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedPaymentAmount = (bill): string => {
return formatNumber(bill.paymentAmount, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedDueAmount = (bill): string => {
return formatNumber(bill.dueAmount, { currencyCode: bill.currencyCode });
};
/**
* Retrieve formatted bill balance.
* @param {IBill} bill
* @returns {string}
*/
protected formattedBalance = (bill): string => {
return formatNumber(bill.balance, { currencyCode: bill.currencyCode });
};
/**
* Retrieve the formatted exchange rate.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedExchangeRate = (invoice): string => {
return formatNumber(invoice.exchangeRate, { money: false });
};
}

View File

@@ -0,0 +1,27 @@
import { Service, Inject } from 'typedi';
import TransactionsLockingValidator from '@/services/TransactionsLocking/TransactionsLockingGuard';
import { TransactionsLockingGroup } from '@/interfaces';
@Service()
export default class PurchasesTransactionsLocking {
@Inject()
transactionLockingValidator: TransactionsLockingValidator;
/**
* Validates the all and partial purchases transactions locking.
* @param {number} tenantId
* @param {Date} transactionDate
* @throws {ServiceError(TRANSACTIONS_DATE_LOCKED)}
*/
public transactionLockingGuard = (
tenantId: number,
transactionDate: Date
) => {
// Validates the all transcation locking.
this.transactionLockingValidator.validateTransactionsLocking(
tenantId,
transactionDate,
TransactionsLockingGroup.Purchases
);
};
}

View File

@@ -0,0 +1,52 @@
import { Service, Inject } from 'typedi';
import Knex from 'knex';
import { IVendorCreditAppliedBill } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Bluebird from 'bluebird';
@Service()
export default class ApplyVendorCreditSyncBills {
@Inject()
tenancy: HasTenancyService;
/**
* Increment bills credited amount.
* @param {number} tenantId
* @param {IVendorCreditAppliedBill[]} vendorCreditAppliedBills
* @param {Knex.Transaction} trx
*/
public incrementBillsCreditedAmount = async (
tenantId: number,
vendorCreditAppliedBills: IVendorCreditAppliedBill[],
trx?: Knex.Transaction
) => {
const { Bill } = this.tenancy.models(tenantId);
await Bluebird.each(
vendorCreditAppliedBills,
(vendorCreditAppliedBill: IVendorCreditAppliedBill) => {
return Bill.query(trx)
.where('id', vendorCreditAppliedBill.billId)
.increment('creditedAmount', vendorCreditAppliedBill.amount);
}
);
};
/**
* Decrement bill credited amount.
* @param {number} tenantId
* @param {IVendorCreditAppliedBill} vendorCreditAppliedBill
* @param {Knex.Transaction} trx
*/
public decrementBillCreditedAmount = async (
tenantId: number,
vendorCreditAppliedBill: IVendorCreditAppliedBill,
trx?: Knex.Transaction
) => {
const { Bill } = this.tenancy.models(tenantId);
await Bill.query(trx)
.findById(vendorCreditAppliedBill.billId)
.decrement('creditedAmount', vendorCreditAppliedBill.amount);
};
}

View File

@@ -0,0 +1,65 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import {
IVendorCreditApplyToBillDeletedPayload,
IVendorCreditApplyToBillsCreatedPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import ApplyVendorCreditSyncBills from './ApplyVendorCreditSyncBills';
@Service()
export default class ApplyVendorCreditSyncBillsSubscriber {
@Inject()
tenancy: HasTenancyService;
@Inject()
syncBillsWithVendorCredit: ApplyVendorCreditSyncBills;
/**
* Attaches events with handlers.
*/
attach(bus) {
bus.subscribe(
events.vendorCredit.onApplyToInvoicesCreated,
this.incrementAppliedBillsOnceCreditCreated
);
bus.subscribe(
events.vendorCredit.onApplyToInvoicesDeleted,
this.decrementAppliedBillsOnceCreditDeleted
);
}
/**
* Increment credited amount of applied bills once the vendor credit
* transaction created.
* @param {IVendorCreditApplyToBillsCreatedPayload} paylaod -
*/
private incrementAppliedBillsOnceCreditCreated = async ({
tenantId,
vendorCreditAppliedBills,
trx,
}: IVendorCreditApplyToBillsCreatedPayload) => {
await this.syncBillsWithVendorCredit.incrementBillsCreditedAmount(
tenantId,
vendorCreditAppliedBills,
trx
);
};
/**
* Decrement credited amount of applied bills once the vendor credit
* transaction delted.
* @param {IVendorCreditApplyToBillDeletedPayload} payload
*/
private decrementAppliedBillsOnceCreditDeleted = async ({
oldCreditAppliedToBill,
tenantId,
trx,
}: IVendorCreditApplyToBillDeletedPayload) => {
await this.syncBillsWithVendorCredit.decrementBillCreditedAmount(
tenantId,
oldCreditAppliedToBill,
trx
);
};
}

View File

@@ -0,0 +1,48 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class ApplyVendorCreditSyncInvoiced {
@Inject()
tenancy: HasTenancyService;
/**
* Increment vendor credit invoiced amount.
* @param {number} tenantId
* @param {number} vendorCreditId
* @param {number} amount
* @param {Knex.Transaction} trx
*/
public incrementVendorCreditInvoicedAmount = async (
tenantId: number,
vendorCreditId: number,
amount: number,
trx?: Knex.Transaction
) => {
const { VendorCredit } = this.tenancy.models(tenantId);
await VendorCredit.query(trx)
.findById(vendorCreditId)
.increment('invoicedAmount', amount);
};
/**
* Decrement credit note invoiced amount.
* @param {number} tenantId
* @param {number} creditNoteId
* @param {number} invoicesAppliedAmount
*/
public decrementVendorCreditInvoicedAmount = async (
tenantId: number,
vendorCreditId: number,
amount: number,
trx?: Knex.Transaction
) => {
const { VendorCredit } = this.tenancy.models(tenantId);
await VendorCredit.query(trx)
.findById(vendorCreditId)
.decrement('invoicedAmount', amount);
};
}

View File

@@ -0,0 +1,70 @@
import { Service, Inject } from 'typedi';
import { sumBy } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import ApplyVendorCreditSyncInvoiced from './ApplyVendorCreditSyncInvoiced';
import events from '@/subscribers/events';
import {
IVendorCreditApplyToBillDeletedPayload,
IVendorCreditApplyToBillsCreatedPayload,
} from '@/interfaces';
@Service()
export default class ApplyVendorCreditSyncInvoicedSubscriber {
@Inject()
tenancy: HasTenancyService;
@Inject()
syncCreditWithInvoiced: ApplyVendorCreditSyncInvoiced;
/**
* Attaches events with handlers.
*/
attach(bus) {
bus.subscribe(
events.vendorCredit.onApplyToInvoicesCreated,
this.incrementBillInvoicedOnceCreditApplied
);
bus.subscribe(
events.vendorCredit.onApplyToInvoicesDeleted,
this.decrementBillInvoicedOnceCreditApplyDeleted
);
}
/**
* Increment vendor credit invoiced amount once the apply transaction created.
* @param {IVendorCreditApplyToBillsCreatedPayload} payload -
*/
private incrementBillInvoicedOnceCreditApplied = async ({
vendorCredit,
tenantId,
vendorCreditAppliedBills,
trx,
}: IVendorCreditApplyToBillsCreatedPayload) => {
const amount = sumBy(vendorCreditAppliedBills, 'amount');
await this.syncCreditWithInvoiced.incrementVendorCreditInvoicedAmount(
tenantId,
vendorCredit.id,
amount,
trx
);
};
/**
* Decrement vendor credit invoiced amount once the apply transaction deleted.
* @param {IVendorCreditApplyToBillDeletedPayload} payload -
*/
private decrementBillInvoicedOnceCreditApplyDeleted = async ({
tenantId,
vendorCredit,
oldCreditAppliedToBill,
trx,
}: IVendorCreditApplyToBillDeletedPayload) => {
await this.syncCreditWithInvoiced.decrementVendorCreditInvoicedAmount(
tenantId,
oldCreditAppliedToBill.vendorCreditId,
oldCreditAppliedToBill.amount,
trx
);
};
}

View File

@@ -0,0 +1,127 @@
import { Service, Inject } from 'typedi';
import Knex from 'knex';
import { sumBy } from 'lodash';
import {
IVendorCredit,
IVendorCreditApplyToBillsCreatedPayload,
IVendorCreditApplyToInvoicesDTO,
IVendorCreditApplyToInvoicesModel,
IBill,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import VendorCredit from '../BaseVendorCredit';
import BillPaymentsService from '@/services/Purchases/BillPayments/BillPayments';
import { ServiceError } from '@/exceptions';
import { ERRORS } from '../constants';
@Service()
export default class ApplyVendorCreditToBills extends VendorCredit {
@Inject('PaymentReceives')
paymentReceive: PaymentReceiveService;
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
@Inject()
billPayment: BillPaymentsService;
/**
* Apply credit note to the given invoices.
* @param {number} tenantId
* @param {number} creditNoteId
* @param {IApplyCreditToInvoicesDTO} applyCreditToInvoicesDTO
*/
public applyVendorCreditToBills = async (
tenantId: number,
vendorCreditId: number,
applyCreditToBillsDTO: IVendorCreditApplyToInvoicesDTO
): Promise<void> => {
const { VendorCreditAppliedBill } = this.tenancy.models(tenantId);
// Retrieves the vendor credit or throw not found service error.
const vendorCredit = await this.getVendorCreditOrThrowError(
tenantId,
vendorCreditId
);
// Transfomes credit apply to bills DTO to model object.
const vendorCreditAppliedModel = this.transformApplyDTOToModel(
applyCreditToBillsDTO,
vendorCredit
);
// Validate bills entries existance.
const appliedBills = await this.billPayment.validateBillsExistance(
tenantId,
vendorCreditAppliedModel.entries,
vendorCredit.vendorId
);
// Validate bills has remaining amount to apply.
this.validateBillsRemainingAmount(
appliedBills,
vendorCreditAppliedModel.amount
);
// Validate vendor credit remaining credit amount.
this.validateCreditRemainingAmount(
vendorCredit,
vendorCreditAppliedModel.amount
);
// Saves vendor credit applied to bills under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Inserts vendor credit applied to bills graph to the storage layer.
const vendorCreditAppliedBills =
await VendorCreditAppliedBill.query().insertGraph(
vendorCreditAppliedModel.entries
);
// Triggers `IVendorCreditApplyToBillsCreatedPayload` event.
await this.eventPublisher.emitAsync(
events.vendorCredit.onApplyToInvoicesCreated,
{
trx,
tenantId,
vendorCredit,
vendorCreditAppliedBills,
} as IVendorCreditApplyToBillsCreatedPayload
);
});
};
/**
* Transformes apply DTO to model.
* @param {IApplyCreditToInvoicesDTO} applyDTO
* @param {ICreditNote} creditNote
* @returns {IVendorCreditApplyToInvoicesModel}
*/
private transformApplyDTOToModel = (
applyDTO: IVendorCreditApplyToInvoicesDTO,
vendorCredit: IVendorCredit
): IVendorCreditApplyToInvoicesModel => {
const entries = applyDTO.entries.map((entry) => ({
billId: entry.billId,
amount: entry.amount,
vendorCreditId: vendorCredit.id,
}));
const amount = sumBy(applyDTO.entries, 'amount');
return {
amount,
entries,
};
};
/**
* Validate bills remaining amount.
* @param {IBill[]} bills
* @param {number} amount
*/
private validateBillsRemainingAmount = (bills: IBill[], amount: number) => {
const invalidBills = bills.filter((bill) => bill.dueAmount < amount);
if (invalidBills.length > 0) {
throw new ServiceError(ERRORS.BILLS_HAS_NO_REMAINING_AMOUNT);
}
};
}

View File

@@ -0,0 +1,61 @@
import { ServiceError } from '@/exceptions';
import { IVendorCreditApplyToBillDeletedPayload } from '@/interfaces';
import Knex from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { Service, Inject } from 'typedi';
import BaseVendorCredit from '../BaseVendorCredit';
import { ERRORS } from '../constants';
@Service()
export default class DeleteApplyVendorCreditToBill extends BaseVendorCredit {
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* Delete apply vendor credit to bill transaction.
* @param {number} tenantId
* @param {number} appliedCreditToBillId
* @returns {Promise<void>}
*/
public deleteApplyVendorCreditToBills = async (
tenantId: number,
appliedCreditToBillId: number
) => {
const { VendorCreditAppliedBill } = this.tenancy.models(tenantId);
const oldCreditAppliedToBill =
await VendorCreditAppliedBill.query().findById(appliedCreditToBillId);
if (!oldCreditAppliedToBill) {
throw new ServiceError(ERRORS.VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND);
}
// Retrieve the vendor credit or throw not found service error.
const vendorCredit = await this.getVendorCreditOrThrowError(
tenantId,
oldCreditAppliedToBill.vendorCreditId
);
// Deletes vendor credit apply under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Delete vendor credit applied to bill transaction.
await VendorCreditAppliedBill.query(trx)
.findById(appliedCreditToBillId)
.delete();
// Triggers `onVendorCreditApplyToInvoiceDeleted` event.
await this.eventPublisher.emitAsync(
events.vendorCredit.onApplyToInvoicesDeleted,
{
tenantId,
vendorCredit,
oldCreditAppliedToBill,
trx,
} as IVendorCreditApplyToBillDeletedPayload
);
});
};
}

View File

@@ -0,0 +1,36 @@
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { Service, Inject } from 'typedi';
import BaseVendorCredit from '../BaseVendorCredit';
import { VendorCreditAppliedBillTransformer } from './VendorCreditAppliedBillTransformer';
@Service()
export default class GetAppliedBillsToVendorCredit extends BaseVendorCredit {
@Inject()
private transformer: TransformerInjectable;
/**
*
* @param {number} tenantId
* @param {number} vendorCreditId
* @returns
*/
public getAppliedBills = async (tenantId: number, vendorCreditId: number) => {
const { VendorCreditAppliedBill } = this.tenancy.models(tenantId);
const vendorCredit = await this.getVendorCreditOrThrowError(
tenantId,
vendorCreditId
);
const appliedToBills = await VendorCreditAppliedBill.query()
.where('vendorCreditId', vendorCreditId)
.withGraphFetched('bill')
.withGraphFetched('vendorCredit');
// Transformes the models to POJO.
return this.transformer.transform(
tenantId,
appliedToBills,
new VendorCreditAppliedBillTransformer()
);
};
}

View File

@@ -0,0 +1,41 @@
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { Service, Inject } from 'typedi';
import BaseVendorCredit from '../BaseVendorCredit';
import { VendorCreditToApplyBillTransformer } from './VendorCreditToApplyBillTransformer';
@Service()
export default class GetVendorCreditToApplyBills extends BaseVendorCredit {
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve bills that valid apply to the given vendor credit.
* @param {number} tenantId
* @param {number} vendorCreditId
* @returns
*/
public getCreditToApplyBills = async (
tenantId: number,
vendorCreditId: number
) => {
const { Bill } = this.tenancy.models(tenantId);
// Retrieve vendor credit or throw not found service error.
const vendorCredit = await this.getVendorCreditOrThrowError(
tenantId,
vendorCreditId
);
// Retrieive open bills associated to the given vendor.
const openBills = await Bill.query()
.where('vendor_id', vendorCredit.vendorId)
.modify('dueBills')
.modify('published');
// Transformes the bills to POJO.
return this.transformer.transform(
tenantId,
openBills,
new VendorCreditToApplyBillTransformer()
);
};
}

View File

@@ -0,0 +1,62 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class VendorCreditAppliedBillTransformer extends Transformer {
/**
* Includeded attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'vendorCreditNumber',
'vendorCreditDate',
'billNumber',
'billReferenceNo',
'formattedVendorCreditDate',
'formattedBillDate',
];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['bill', 'vendorCredit'];
};
protected formattedAmount = (item) => {
return formatNumber(item.amount, {
currencyCode: item.vendorCredit.currencyCode,
});
};
protected vendorCreditNumber = (item) => {
return item.vendorCredit.vendorCreditNumber;
};
protected vendorCreditDate = (item) => {
return item.vendorCredit.vendorCreditDate;
};
protected formattedVendorCreditDate = (item) => {
return this.formatDate(item.vendorCredit.vendorCreditDate);
};
protected billNumber = (item) => {
return item.bill.billNo;
};
protected billReferenceNo = (item) => {
return item.bill.referenceNo;
};
protected BillDate = (item) => {
return item.bill.billDate;
};
protected formattedBillDate = (item) => {
return this.formatDate(item.bill.billDate);
};
}

View File

@@ -0,0 +1,70 @@
import { IBill } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class VendorCreditToApplyBillTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedBillDate',
'formattedDueDate',
'formattedAmount',
'formattedDueAmount',
'formattedPaymentAmount',
];
};
/**
* Retrieve formatted bill date.
* @param {IBill} bill
* @returns {String}
*/
protected formattedBillDate = (bill: IBill): string => {
return this.formatDate(bill.billDate);
};
/**
* Retrieve formatted due date.
* @param {IBill} bill
* @returns {string}
*/
protected formattedDueDate = (bill: IBill): string => {
return this.formatDate(bill.dueDate);
};
/**
* Retrieve formatted bill amount.
* @param {IBill} bill
* @returns {string}
*/
protected formattedAmount = (bill: IBill): string => {
return formatNumber(bill.amount, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieve formatted bill due amount.
* @param {IBill} bill
* @returns {string}
*/
protected formattedDueAmount = (bill: IBill): string => {
return formatNumber(bill.dueAmount, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieve formatted payment amount.
* @param {IBill} bill
* @returns {string}
*/
protected formattedPaymentAmount = (bill: IBill): string => {
return formatNumber(bill.paymentAmount, {
currencyCode: bill.currencyCode,
});
};
}

View File

@@ -0,0 +1,139 @@
import { Inject, Service } from 'typedi';
import moment from 'moment';
import { omit } from 'lodash';
import * as R from 'ramda';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { ServiceError } from '@/exceptions';
import {
IVendorCredit,
IVendorCreditCreateDTO,
IVendorCreditEditDTO,
} from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
@Service()
export default class BaseVendorCredit {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
/**
* Transformes the credit/edit vendor credit DTO to model.
* @param {number} tenantId -
* @param {IVendorCreditCreateDTO | IVendorCreditEditDTO} vendorCreditDTO
* @param {string} vendorCurrencyCode -
* @param {IVendorCredit} oldVendorCredit -
* @returns {IVendorCredit}
*/
public transformCreateEditDTOToModel = (
tenantId: number,
vendorCreditDTO: IVendorCreditCreateDTO | IVendorCreditEditDTO,
vendorCurrencyCode: string,
oldVendorCredit?: IVendorCredit
): IVendorCredit => {
// Calculates the total amount of items entries.
const amount = this.itemsEntriesService.getTotalItemsEntries(
vendorCreditDTO.entries
);
const entries = vendorCreditDTO.entries.map((entry) => ({
...entry,
referenceType: 'VendorCredit',
}));
// Retreive the next vendor credit number.
const autoNextNumber = this.getNextCreditNumber(tenantId);
// Detarmines the credit note number.
const vendorCreditNumber =
vendorCreditDTO.vendorCreditNumber ||
oldVendorCredit?.vendorCreditNumber ||
autoNextNumber;
const initialDTO = {
...omit(vendorCreditDTO, ['open']),
amount,
currencyCode: vendorCurrencyCode,
exchangeRate: vendorCreditDTO.exchangeRate || 1,
vendorCreditNumber,
entries,
...(vendorCreditDTO.open &&
!oldVendorCredit?.openedAt && {
openedAt: moment().toMySqlDateTime(),
}),
};
return R.compose(
this.branchDTOTransform.transformDTO<IVendorCredit>(tenantId),
this.warehouseDTOTransform.transformDTO<IVendorCredit>(tenantId)
)(initialDTO);
};
/**
* Retrieve the vendor credit or throw not found service error.
* @param {number} tenantId
* @param {number} vendorCreditId
*/
public getVendorCreditOrThrowError = async (
tenantId: number,
vendorCreditId: number
): Promise<IVendorCredit> => {
const { VendorCredit } = this.tenancy.models(tenantId);
const vendorCredit = await VendorCredit.query().findById(vendorCreditId);
if (!vendorCredit) {
throw new ServiceError(ERRORS.VENDOR_CREDIT_NOT_FOUND);
}
return vendorCredit;
};
/**
* Retrieve the next unique credit number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
private getNextCreditNumber = (tenantId: number): string => {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'vendor_credit'
);
};
/**
* Increment the vendor credit serial next number.
* @param {number} tenantId -
*/
public incrementSerialNumber = (tenantId: number) => {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'vendor_credit'
);
};
/**
* Validate the credit note remaining amount.
* @param {ICreditNote} creditNote
* @param {number} amount
*/
public validateCreditRemainingAmount = (
vendorCredit: IVendorCredit,
amount: number
) => {
if (vendorCredit.creditsRemaining < amount) {
throw new ServiceError(ERRORS.VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT);
}
};
}

View File

@@ -0,0 +1,85 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
IVendorCreditCreatedPayload,
IVendorCreditCreateDTO,
IVendorCreditCreatingPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import BaseVendorCredit from './BaseVendorCredit';
@Service()
export default class CreateVendorCredit extends BaseVendorCredit {
@Inject()
private uow: UnitOfWork;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a new vendor credit.
* @param {number} tenantId -
* @param {IVendorCreditCreateDTO} vendorCreditCreateDTO -
*/
public newVendorCredit = async (
tenantId: number,
vendorCreditCreateDTO: IVendorCreditCreateDTO
) => {
const { VendorCredit, Vendor } = this.tenancy.models(tenantId);
// Triggers `onVendorCreditCreate` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onCreate, {
tenantId,
vendorCreditCreateDTO,
});
// Retrieve the given vendor or throw not found service error.
const vendor = await Vendor.query()
.findById(vendorCreditCreateDTO.vendorId)
.throwIfNotFound();
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
vendorCreditCreateDTO.entries
);
// Transformes the credit DTO to storage layer.
const vendorCreditModel = this.transformCreateEditDTOToModel(
tenantId,
vendorCreditCreateDTO,
vendor.currencyCode
);
// Saves the vendor credit transactions under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onVendorCreditCreating` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onCreating, {
tenantId,
vendorCreditCreateDTO,
trx,
} as IVendorCreditCreatingPayload);
// Saves the vendor credit graph.
const vendorCredit = await VendorCredit.query(trx).upsertGraphAndFetch({
...vendorCreditModel,
});
// Triggers `onVendorCreditCreated` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onCreated, {
tenantId,
vendorCredit,
vendorCreditCreateDTO,
trx,
} as IVendorCreditCreatedPayload);
return vendorCredit;
});
};
}

View File

@@ -0,0 +1,57 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import { IVendorEventDeletingPayload } from '@/interfaces';
const ERRORS = {
VENDOR_HAS_TRANSACTIONS: 'VENDOR_HAS_TRANSACTIONS',
};
@Service()
export default class DeleteVendorAssociatedVendorCredit {
@Inject()
tenancy: TenancyService;
/**
* Attaches events with handlers.
* @param bus
*/
public attach = (bus) => {
bus.subscribe(
events.vendors.onDeleting,
this.validateVendorHasNoCreditsTransactionsOnceDeleting
);
};
/**
* Validate vendor has no assocaited credit transaction once the vendor deleting.
* @param {IVendorEventDeletingPayload} payload -
*/
public validateVendorHasNoCreditsTransactionsOnceDeleting = async ({
tenantId,
vendorId,
}: IVendorEventDeletingPayload) => {
await this.validateVendorHasNoCreditsTransactions(tenantId, vendorId);
};
/**
* Validate the given vendor has no associated vendor credit transactions.
* @param {number} tenantId
* @param {number} vendorId
*/
public validateVendorHasNoCreditsTransactions = async (
tenantId: number,
vendorId: number
): Promise<void> => {
const { VendorCredit } = this.tenancy.models(tenantId);
const associatedVendors = await VendorCredit.query().where(
'vendorId',
vendorId
);
if (associatedVendors.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_TRANSACTIONS);
}
};
}

View File

@@ -0,0 +1,119 @@
import { Service, Inject } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import BaseVendorCredit from './BaseVendorCredit';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import {
IVendorCreditDeletedPayload,
IVendorCreditDeletingPayload,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class DeleteVendorCredit extends BaseVendorCredit {
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
@Inject()
tenancy: HasTenancyService;
/**
* Deletes the given vendor credit.
* @param {number} tenantId - Tenant id.
* @param {number} vendorCreditId - Vendor credit id.
*/
public deleteVendorCredit = async (
tenantId: number,
vendorCreditId: number
) => {
const { VendorCredit, ItemEntry } = this.tenancy.models(tenantId);
// Retrieve the old vendor credit.
const oldVendorCredit = await this.getVendorCreditOrThrowError(
tenantId,
vendorCreditId
);
// Validates vendor credit has no associate refund transactions.
await this.validateVendorCreditHasNoRefundTransactions(
tenantId,
vendorCreditId
);
// Validates vendor credit has no associated applied to bills transactions.
await this.validateVendorCreditHasNoApplyBillsTransactions(
tenantId,
vendorCreditId
);
// Deletes the vendor credit transactions under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onVendorCreditEditing` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onDeleting, {
tenantId,
oldVendorCredit,
trx,
} as IVendorCreditDeletingPayload);
// Deletes the associated credit note entries.
await ItemEntry.query(trx)
.where('reference_id', vendorCreditId)
.where('reference_type', 'VendorCredit')
.delete();
// Deletes the credit note transaction.
await VendorCredit.query(trx).findById(vendorCreditId).delete();
// Triggers `onVendorCreditDeleted` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onDeleted, {
tenantId,
vendorCreditId,
oldVendorCredit,
trx,
} as IVendorCreditDeletedPayload);
});
};
/**
* Validates vendor credit has no refund transactions.
* @param {number} tenantId
* @param {number} vendorCreditId
*/
private validateVendorCreditHasNoRefundTransactions = async (
tenantId: number,
vendorCreditId: number
): Promise<void> => {
const { RefundVendorCredit } = this.tenancy.models(tenantId);
const refundCredits = await RefundVendorCredit.query().where(
'vendorCreditId',
vendorCreditId
);
if (refundCredits.length > 0) {
throw new ServiceError(ERRORS.VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS);
}
};
/**
* Validate vendor credit has no applied transactions to bills.
* @param {number} tenantId
* @param {number} vendorCreditId
*/
private validateVendorCreditHasNoApplyBillsTransactions = async (
tenantId: number,
vendorCreditId: number
): Promise<void> => {
const { VendorCreditAppliedBill } = this.tenancy.models(tenantId);
const appliedTransactions = await VendorCreditAppliedBill.query().where(
'vendorCreditId',
vendorCreditId
);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.VENDOR_CREDIT_HAS_APPLIED_BILLS);
}
};
}

View File

@@ -0,0 +1,98 @@
import { Service, Inject } from 'typedi';
import {
IVendorCreditEditDTO,
IVendorCreditEditedPayload,
IVendorCreditEditingPayload,
} from '@/interfaces';
import BaseVendorCredit from './BaseVendorCredit';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import events from '@/subscribers/events';
@Service()
export default class EditVendorCredit extends BaseVendorCredit {
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private itemsEntriesService: ItemsEntriesService;
/**
* Deletes the given vendor credit.
* @param {number} tenantId - Tenant id.
* @param {number} vendorCreditId - Vendor credit id.
*/
public editVendorCredit = async (
tenantId: number,
vendorCreditId: number,
vendorCreditDTO: IVendorCreditEditDTO
) => {
const { VendorCredit } = this.tenancy.models(tenantId);
// Retrieve the vendor credit or throw not found service error.
const oldVendorCredit = await this.getVendorCreditOrThrowError(
tenantId,
vendorCreditId
);
// Validate customer existance.
const vendor = await Contact.query()
.modify('vendor')
.findById(vendorCreditDTO.vendorId)
.throwIfNotFound();
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
vendorCreditDTO.entries
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
vendorCreditDTO.entries
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
vendorCreditId,
'VendorCredit',
vendorCreditDTO.entries
);
// Transformes edit DTO to model storage layer.
const vendorCreditModel = this.transformCreateEditDTOToModel(
tenantId,
vendorCreditDTO,
vendor.currencyCode,
oldVendorCredit
);
// Edits the vendor credit graph under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx) => {
// Triggers `onVendorCreditEditing` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onEditing, {
tenantId,
oldVendorCredit,
vendorCreditDTO,
trx,
} as IVendorCreditEditingPayload);
// Saves the vendor credit graph to the storage.
const vendorCredit = await VendorCredit.query(trx).upsertGraphAndFetch({
id: vendorCreditId,
...vendorCreditModel,
});
// Triggers `onVendorCreditEdited event.
await this.eventPublisher.emitAsync(events.vendorCredit.onEdited, {
tenantId,
oldVendorCredit,
vendorCredit,
vendorCreditId,
trx,
} as IVendorCreditEditedPayload);
return vendorCredit;
});
};
}

View File

@@ -0,0 +1,40 @@
import { ServiceError } from '@/exceptions';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { VendorCreditTransformer } from './VendorCreditTransformer';
import { Inject, Service } from 'typedi';
import { ERRORS } from './constants';
@Service()
export default class GetVendorCredit {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the given vendor credit.
* @param {number} tenantId - Tenant id.
* @param {number} vendorCreditId - Vendor credit id.
*/
public getVendorCredit = async (tenantId: number, vendorCreditId: number) => {
const { VendorCredit } = this.tenancy.models(tenantId);
// Retrieve the vendor credit model graph.
const vendorCredit = await VendorCredit.query()
.findById(vendorCreditId)
.withGraphFetched('entries.item')
.withGraphFetched('vendor')
.withGraphFetched('branch');
if (!vendorCredit) {
throw new ServiceError(ERRORS.VENDOR_CREDIT_NOT_FOUND);
}
return this.transformer.transform(
tenantId,
vendorCredit,
new VendorCreditTransformer()
);
};
}

View File

@@ -0,0 +1,66 @@
import * as R from 'ramda';
import { Service, Inject } from 'typedi';
import BaseVendorCredit from './BaseVendorCredit';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { IVendorCreditsQueryDTO } from '@/interfaces';
import { VendorCreditTransformer } from './VendorCreditTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class ListVendorCredits extends BaseVendorCredit {
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Parses the sale invoice list filter DTO.
* @param {IVendorCreditsQueryDTO} filterDTO
* @returns
*/
private parseListFilterDTO = (filterDTO: IVendorCreditsQueryDTO) => {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
};
/**
* Retrieve the vendor credits list.
* @param {number} tenantId - Tenant id.
* @param {IVendorCreditsQueryDTO} vendorCreditQuery -
*/
public getVendorCredits = async (
tenantId: number,
vendorCreditQuery: IVendorCreditsQueryDTO
) => {
const { VendorCredit } = this.tenancy.models(tenantId);
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(vendorCreditQuery);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
VendorCredit,
filter
);
const { results, pagination } = await VendorCredit.query()
.onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('vendor');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformes the vendor credits models to POJO.
const vendorCredits = await this.transformer.transform(
tenantId,
results,
new VendorCreditTransformer()
);
return {
vendorCredits,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
};
}

View File

@@ -0,0 +1,90 @@
import { ServiceError } from '@/exceptions';
import {
IVendorCredit,
IVendorCreditOpenedPayload,
IVendorCreditOpeningPayload,
IVendorCreditOpenPayload,
} from '@/interfaces';
import Knex from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { Inject, Service } from 'typedi';
import BaseVendorCredit from './BaseVendorCredit';
import { ERRORS } from './constants';
@Service()
export default class OpenVendorCredit extends BaseVendorCredit {
@Inject()
eventPublisher: EventPublisher;
@Inject()
uow: UnitOfWork;
/**
* Opens the given credit note.
* @param {number} tenantId -
* @param {ICreditNoteEditDTO} creditNoteEditDTO -
* @returns {Promise<ICreditNote>}
*/
public openVendorCredit = async (
tenantId: number,
vendorCreditId: number
): Promise<IVendorCredit> => {
const { VendorCredit } = this.tenancy.models(tenantId);
// Retrieve the vendor credit or throw not found service error.
const oldVendorCredit = await this.getVendorCreditOrThrowError(
tenantId,
vendorCreditId
);
// Throw service error if the credit note is already open.
this.throwErrorIfAlreadyOpen(oldVendorCredit);
// Triggers `onVendorCreditOpen` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onOpen, {
tenantId,
vendorCreditId,
oldVendorCredit,
} as IVendorCreditOpenPayload);
// Sales the credit note transactions with associated entries.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
const eventPayload = {
tenantId,
vendorCreditId,
oldVendorCredit,
trx,
} as IVendorCreditOpeningPayload;
// Triggers `onCreditNoteOpening` event.
await this.eventPublisher.emitAsync(
events.creditNote.onOpening,
eventPayload as IVendorCreditOpeningPayload
);
// Saves the vendor credit graph to the storage.
const vendorCredit = await VendorCredit.query(trx)
.findById(vendorCreditId)
.update({
openedAt: new Date(),
});
// Triggers `onVendorCreditOpened` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onOpened, {
...eventPayload,
vendorCredit,
} as IVendorCreditOpenedPayload);
return vendorCredit;
});
};
/**
* Throw error if the vendor credit is already open.
* @param {IVendorCredit} vendorCredit
*/
public throwErrorIfAlreadyOpen = (vendorCredit: IVendorCredit) => {
if (vendorCredit.openedAt) {
throw new ServiceError(ERRORS.VENDOR_CREDIT_ALREADY_OPENED);
}
};
}

View File

@@ -0,0 +1,128 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import * as R from 'ramda';
import {
IRefundVendorCredit,
IRefundVendorCreditCreatedPayload,
IRefundVendorCreditCreatingPayload,
IRefundVendorCreditDTO,
IVendorCredit,
IVendorCreditCreatePayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import RefundVendorCredit from './RefundVendorCredit';
import events from '@/subscribers/events';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
@Service()
export default class CreateRefundVendorCredit extends RefundVendorCredit {
@Inject()
tenancy: HasTenancyService;
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
/**
* Creates a refund vendor credit.
* @param {number} tenantId
* @param {number} vendorCreditId
* @param {IRefundVendorCreditDTO} refundVendorCreditDTO
* @returns {Promise<IRefundVendorCredit>}
*/
public createRefund = async (
tenantId: number,
vendorCreditId: number,
refundVendorCreditDTO: IRefundVendorCreditDTO
): Promise<IRefundVendorCredit> => {
const { RefundVendorCredit, Account, VendorCredit } =
this.tenancy.models(tenantId);
// Retrieve the vendor credit or throw not found service error.
const vendorCredit = await VendorCredit.query()
.findById(vendorCreditId)
.throwIfNotFound();
// Retrieve the deposit account or throw not found service error.
const depositAccount = await Account.query()
.findById(refundVendorCreditDTO.depositAccountId)
.throwIfNotFound();
// Validate vendor credit has remaining credit.
this.validateVendorCreditRemainingCredit(
vendorCredit,
refundVendorCreditDTO.amount
);
// Validate refund deposit account type.
this.validateRefundDepositAccountType(depositAccount);
// Triggers `onVendorCreditRefundCreate` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onRefundCreate, {
tenantId,
vendorCreditId,
refundVendorCreditDTO,
} as IVendorCreditCreatePayload);
const refundCreditObj = this.transformDTOToModel(
tenantId,
vendorCredit,
refundVendorCreditDTO
);
// Saves refund vendor credit with associated transactions.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
const eventPayload = {
vendorCredit,
trx,
tenantId,
refundVendorCreditDTO,
} as IRefundVendorCreditCreatingPayload;
// Triggers `onVendorCreditRefundCreating` event.
await this.eventPublisher.emitAsync(
events.vendorCredit.onRefundCreating,
eventPayload as IRefundVendorCreditCreatingPayload
);
// Inserts refund vendor credit to the storage layer.
const refundVendorCredit =
await RefundVendorCredit.query().insertAndFetch({
...refundCreditObj,
});
// Triggers `onVendorCreditCreated` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onRefundCreated, {
...eventPayload,
refundVendorCredit,
} as IRefundVendorCreditCreatedPayload);
return refundVendorCredit;
});
};
/**
* Transformes the refund DTO to refund vendor credit model.
* @param {IVendorCredit} vendorCredit -
* @param {IRefundVendorCreditDTO} vendorCreditDTO
* @returns {IRefundVendorCredit}
*/
public transformDTOToModel = (
tenantId: number,
vendorCredit: IVendorCredit,
vendorCreditDTO: IRefundVendorCreditDTO
) => {
const initialDTO = {
vendorCreditId: vendorCredit.id,
...vendorCreditDTO,
currencyCode: vendorCredit.currencyCode,
exchangeRate: vendorCreditDTO.exchangeRate || 1,
};
return R.compose(this.branchDTOTransform.transformDTO(tenantId))(
initialDTO
);
};
}

View File

@@ -0,0 +1,73 @@
import { Knex } from 'knex';
import {
IRefundVendorCreditDeletedPayload,
IRefundVendorCreditDeletePayload,
IRefundVendorCreditDeletingPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { Inject, Service } from 'typedi';
import RefundVendorCredit from './RefundVendorCredit';
@Service()
export default class DeleteRefundVendorCredit extends RefundVendorCredit {
@Inject()
tenancy: HasTenancyService;
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* Retrieve the credit note graph.
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
* @returns {Promise<void>}
*/
public deleteRefundVendorCreditRefund = async (
tenantId: number,
refundCreditId: number
): Promise<void> => {
const { RefundVendorCredit } = this.tenancy.models(tenantId);
// Retrieve the old credit note or throw not found service error.
const oldRefundCredit = await this.getRefundVendorCreditOrThrowError(
tenantId,
refundCreditId
);
// Triggers `onVendorCreditRefundDelete` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onRefundDelete, {
refundCreditId,
oldRefundCredit,
tenantId,
} as IRefundVendorCreditDeletePayload);
// Deletes the refund vendor credit under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
const eventPayload = {
trx,
refundCreditId,
oldRefundCredit,
tenantId,
} as IRefundVendorCreditDeletingPayload;
// Triggers `onVendorCreditRefundDeleting` event.
await this.eventPublisher.emitAsync(
events.vendorCredit.onRefundDeleting,
eventPayload
);
// Deletes the refund vendor credit graph from the storage.
await RefundVendorCredit.query(trx).findById(refundCreditId).delete();
// Triggers `onVendorCreditRefundDeleted` event.
await this.eventPublisher.emitAsync(
events.vendorCredit.onRefundDeleted,
eventPayload as IRefundVendorCreditDeletedPayload
);
});
};
}

View File

@@ -0,0 +1,43 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { RefundVendorCreditTransformer } from './RefundVendorCreditTransformer';
import RefundVendorCredit from './RefundVendorCredit';
import { IRefundVendorCredit } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class GetRefundVendorCredit extends RefundVendorCredit {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve refund vendor credit transaction.
* @param {number} tenantId
* @param {number} refundId
* @returns {Promise<IRefundVendorCredit>}
*/
public getRefundCreditTransaction = async (
tenantId: number,
refundId: number
): Promise<IRefundVendorCredit> => {
const { RefundVendorCredit } = this.tenancy.models(tenantId);
await this.getRefundVendorCreditOrThrowError(tenantId, refundId);
// Retrieve refund transactions associated to the given vendor credit.
const refundVendorTransactions = await RefundVendorCredit.query()
.findById(refundId)
.withGraphFetched('vendorCredit')
.withGraphFetched('depositAccount');
// Transformes refund vendor credit models to POJO objects.
return this.transformer.transform(
tenantId,
refundVendorTransactions,
new RefundVendorCreditTransformer()
);
};
}

View File

@@ -0,0 +1,41 @@
import { IRefundVendorCreditPOJO } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import RefundVendorCredit from './RefundVendorCredit';
import { RefundVendorCreditTransformer } from './RefundVendorCreditTransformer';
@Service()
export default class ListVendorCreditRefunds extends RefundVendorCredit {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the credit note graph.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<IRefundCreditNotePOJO[]>}
*/
public getVendorCreditRefunds = async (
tenantId: number,
vendorCreditId: number
): Promise<IRefundVendorCreditPOJO[]> => {
const { RefundVendorCredit } = this.tenancy.models(tenantId);
// Retrieve refund transactions associated to the given vendor credit.
const refundVendorTransactions = await RefundVendorCredit.query()
.where('vendorCreditId', vendorCreditId)
.withGraphFetched('vendorCredit')
.withGraphFetched('depositAccount');
// Transformes refund vendor credit models to POJO objects.
return this.transformer.transform(
tenantId,
refundVendorTransactions,
new RefundVendorCreditTransformer()
);
};
}

View File

@@ -0,0 +1,47 @@
import Knex from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
@Service()
export default class RefundSyncCreditRefundedAmount {
@Inject()
tenancy: HasTenancyService;
/**
* Increment vendor credit refunded amount.
* @param {number} tenantId - Tenant id.
* @param {number} amount - Amount.
* @param {Knex.Transaction} trx - Knex transaction.
*/
public incrementCreditRefundedAmount = async (
tenantId: number,
vendorCreditId: number,
amount: number,
trx?: Knex.Transaction
): Promise<void> => {
const { VendorCredit } = this.tenancy.models(tenantId);
await VendorCredit.query(trx)
.findById(vendorCreditId)
.increment('refundedAmount', amount);
};
/**
* Decrement vendor credit refunded amount.
* @param {number} tenantId
* @param {number} amount
* @param {Knex.Transaction} trx
*/
public decrementCreditNoteRefundAmount = async (
tenantId: number,
vendorCreditId: number,
amount: number,
trx?: Knex.Transaction
): Promise<void> => {
const { VendorCredit } = this.tenancy.models(tenantId);
await VendorCredit.query(trx)
.findById(vendorCreditId)
.decrement('refundedAmount', amount);
};
}

View File

@@ -0,0 +1,48 @@
import Knex from 'knex';
import { IRefundVendorCredit } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
@Service()
export default class RefundSyncVendorCreditBalance {
@Inject()
tenancy: HasTenancyService;
/**
* Increment vendor credit refunded amount.
* @param {number} tenantId -
* @param {IRefundVendorCredit} refundCreditNote -
* @param {Knex.Transaction} trx -
*/
public incrementVendorCreditRefundAmount = async (
tenantId: number,
refundVendorCredit: IRefundVendorCredit,
trx?: Knex.Transaction
): Promise<void> => {
const { VendorCredit } = this.tenancy.models(tenantId);
await VendorCredit.query(trx).increment(
'refundedAmount',
refundVendorCredit.amount
);
};
/**
* Decrement vendor credit refunded amount.
* @param {number} tenantId
* @param {IRefundVendorCredit} refundCreditNote
* @param {Knex.Transaction} trx
*/
public decrementVendorCreditRefundAmount = async (
tenantId: number,
refundVendorCredit: IRefundVendorCredit,
trx?: Knex.Transaction
): Promise<void> => {
const { VendorCredit } = this.tenancy.models(tenantId);
await VendorCredit.query(trx).decrement(
'refundedAmount',
refundVendorCredit.amount
);
};
}

View File

@@ -0,0 +1,62 @@
import { Service, Inject } from 'typedi';
import {
IRefundVendorCreditCreatedPayload,
IRefundVendorCreditDeletedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import RefundSyncCreditRefundedAmount from './RefundSyncCreditRefundedAmount';
@Service()
export default class RefundSyncVendorCreditBalanceSubscriber {
@Inject()
refundSyncCreditRefunded: RefundSyncCreditRefundedAmount;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.vendorCredit.onRefundCreated,
this.incrementRefundedAmountOnceRefundCreated
);
bus.subscribe(
events.vendorCredit.onRefundDeleted,
this.decrementRefundedAmountOnceRefundDeleted
);
};
/**
* Increment refunded vendor credit amount once refund transaction created.
* @param {IRefundVendorCreditCreatedPayload} payload -
*/
private incrementRefundedAmountOnceRefundCreated = async ({
refundVendorCredit,
vendorCredit,
tenantId,
trx,
}: IRefundVendorCreditCreatedPayload) => {
await this.refundSyncCreditRefunded.incrementCreditRefundedAmount(
tenantId,
refundVendorCredit.vendorCreditId,
refundVendorCredit.amount,
trx
);
};
/**
* Decrement refunded vendor credit amount once refund transaction deleted.
* @param {IRefundVendorCreditDeletedPayload} payload -
*/
private decrementRefundedAmountOnceRefundDeleted = async ({
trx,
oldRefundCredit,
tenantId,
}: IRefundVendorCreditDeletedPayload) => {
await this.refundSyncCreditRefunded.decrementCreditNoteRefundAmount(
tenantId,
oldRefundCredit.vendorCreditId,
oldRefundCredit.amount,
trx
);
};
}

View File

@@ -0,0 +1,55 @@
import { ServiceError } from '@/exceptions';
import { IAccount, IVendorCredit } from '@/interfaces';
import { Service, Inject } from 'typedi';
import BaseVendorCredit from '../BaseVendorCredit';
import { ERRORS } from './constants';
@Service()
export default class RefundVendorCredit extends BaseVendorCredit {
/**
* Retrieve the vendor credit refund or throw not found service error.
* @param {number} tenantId
* @param {number} vendorCreditId
* @returns
*/
public getRefundVendorCreditOrThrowError = async (
tenantId: number,
refundVendorCreditId: number
) => {
const { RefundVendorCredit } = this.tenancy.models(tenantId);
const refundCredit = await RefundVendorCredit.query().findById(
refundVendorCreditId
);
if (!refundCredit) {
throw new ServiceError(ERRORS.REFUND_VENDOR_CREDIT_NOT_FOUND);
}
return refundCredit;
};
/**
* Validate the deposit refund account type.
* @param {IAccount} account
*/
public validateRefundDepositAccountType = (account: IAccount): void => {
const supportedTypes = ['bank', 'cash', 'fixed-asset'];
if (supportedTypes.indexOf(account.accountType) === -1) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_INVALID_TYPE);
}
};
/**
* Validate vendor credit has remaining credits.
* @param {IVendorCredit} vendorCredit
* @param {number} amount
*/
public validateVendorCreditRemainingCredit = (
vendorCredit: IVendorCredit,
amount: number
) => {
if (vendorCredit.creditsRemaining < amount) {
throw new ServiceError(ERRORS.VENDOR_CREDIT_HAS_NO_CREDITS_REMAINING);
}
};
}

View File

@@ -0,0 +1,159 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { AccountNormal, ILedgerEntry } from '@/interfaces';
import { IRefundVendorCredit } from '@/interfaces';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import LedgerRepository from '@/services/Ledger/LedgerRepository';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class RefundVendorCreditGLEntries {
@Inject()
private journalService: JournalPosterService;
@Inject()
private ledgerRepository: LedgerRepository;
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the refund credit common GL entry.
* @param {IRefundVendorCredit} refundCredit
*/
private getRefundCreditGLCommonEntry = (
refundCredit: IRefundVendorCredit
) => {
return {
exchangeRate: refundCredit.exchangeRate,
currencyCode: refundCredit.currencyCode,
transactionType: 'RefundVendorCredit',
transactionId: refundCredit.id,
date: refundCredit.date,
userId: refundCredit.userId,
referenceNumber: refundCredit.referenceNo,
createdAt: refundCredit.createdAt,
indexGroup: 10,
credit: 0,
debit: 0,
note: refundCredit.description,
branchId: refundCredit.branchId,
};
};
/**
* Retrieves the refund credit payable GL entry.
* @param {IRefundVendorCredit} refundCredit
* @param {number} APAccountId
* @returns {ILedgerEntry}
*/
private getRefundCreditGLPayableEntry = (
refundCredit: IRefundVendorCredit,
APAccountId: number
): ILedgerEntry => {
const commonEntry = this.getRefundCreditGLCommonEntry(refundCredit);
return {
...commonEntry,
credit: refundCredit.amount,
accountId: APAccountId,
contactId: refundCredit.vendorCredit.vendorId,
index: 1,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieves the refund credit deposit GL entry.
* @param {IRefundVendorCredit} refundCredit
* @returns {ILedgerEntry}
*/
private getRefundCreditGLDepositEntry = (
refundCredit: IRefundVendorCredit
): ILedgerEntry => {
const commonEntry = this.getRefundCreditGLCommonEntry(refundCredit);
return {
...commonEntry,
debit: refundCredit.amount,
accountId: refundCredit.depositAccountId,
index: 2,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieve refund vendor credit GL entries.
* @param {IRefundVendorCredit} refundCredit
* @param {number} APAccountId
* @returns {ILedgerEntry[]}
*/
public getRefundCreditGLEntries = (
refundCredit: IRefundVendorCredit,
APAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getRefundCreditGLPayableEntry(
refundCredit,
APAccountId
);
const depositEntry = this.getRefundCreditGLDepositEntry(refundCredit);
return [payableEntry, depositEntry];
};
/**
* Saves refund credit note GL entries.
* @param {number} tenantId
* @param {IRefundVendorCredit} refundCredit -
* @param {Knex.Transaction} trx -
* @return {Promise<void>}
*/
public saveRefundCreditGLEntries = async (
tenantId: number,
refundCreditId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { Account, RefundVendorCredit } = this.tenancy.models(tenantId);
// Retireve refund with associated vendor credit entity.
const refundCredit = await RefundVendorCredit.query()
.findById(refundCreditId)
.withGraphFetched('vendorCredit');
const payableAccount = await Account.query().findOne(
'slug',
'accounts-payable'
);
// Generates the GL entries of the given refund credit.
const entries = this.getRefundCreditGLEntries(
refundCredit,
payableAccount.id
);
// Saves the ledegr to the storage.
await this.ledgerRepository.saveLedgerEntries(tenantId, entries, trx);
};
/**
* Reverts refund credit note GL entries.
* @param {number} tenantId
* @param {number} refundCreditId
* @param {Knex.Transaction} trx
* @return {Promise<void>}
*/
public revertRefundCreditGLEntries = async (
tenantId: number,
refundCreditId: number,
trx?: Knex.Transaction
) => {
await this.journalService.revertJournalTransactions(
tenantId,
refundCreditId,
'RefundVendorCredit',
trx
);
};
}

View File

@@ -0,0 +1,59 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import RefundVendorCreditGLEntries from './RefundVendorCreditGLEntries';
import {
IRefundCreditNoteDeletedPayload,
IRefundVendorCreditCreatedPayload,
} from '@/interfaces';
@Service()
export default class RefundVendorCreditGLEntriesSubscriber {
@Inject()
refundVendorGLEntries: RefundVendorCreditGLEntries;
/**
* Attaches events with handlers.
*/
attach(bus) {
bus.subscribe(
events.vendorCredit.onRefundCreated,
this.writeRefundVendorCreditGLEntriesOnceCreated
);
bus.subscribe(
events.vendorCredit.onRefundDeleted,
this.revertRefundVendorCreditOnceDeleted
);
}
/**
* Writes refund vendor credit GL entries once the transaction created.
* @param {IRefundCreditNoteCreatedPayload} payload -
*/
private writeRefundVendorCreditGLEntriesOnceCreated = async ({
tenantId,
trx,
refundVendorCredit,
}: IRefundVendorCreditCreatedPayload) => {
await this.refundVendorGLEntries.saveRefundCreditGLEntries(
tenantId,
refundVendorCredit.id,
trx
);
};
/**
* Reverts refund vendor credit GL entries once the transaction deleted.
* @param {IRefundCreditNoteDeletedPayload} payload -
*/
private revertRefundVendorCreditOnceDeleted = async ({
tenantId,
trx,
refundCreditId,
}: IRefundCreditNoteDeletedPayload) => {
await this.refundVendorGLEntries.revertRefundCreditGLEntries(
tenantId,
refundCreditId,
trx
);
};
}

View File

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

View File

@@ -0,0 +1,5 @@
export const ERRORS = {
REFUND_VENDOR_CREDIT_NOT_FOUND: 'REFUND_VENDOR_CREDIT_NOT_FOUND',
DEPOSIT_ACCOUNT_INVALID_TYPE: 'DEPOSIT_ACCOUNT_INVALID_TYPE',
VENDOR_CREDIT_HAS_NO_CREDITS_REMAINING: 'VENDOR_CREDIT_HAS_NO_CREDITS_REMAINING'
}

View File

@@ -0,0 +1,27 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import BaseVendorCredit from './BaseVendorCredit';
import { IVendorCreditCreatedPayload } from '@/interfaces';
@Service()
export default class VendorCreditAutoSerialSubscriber {
@Inject()
vendorCreditService: BaseVendorCredit;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(events.vendorCredit.onCreated, this.autoIncrementOnceCreated);
}
/**
* Auto serial increment once the vendor credit created.
* @param {IVendorCreditCreatedPayload} payload
*/
private autoIncrementOnceCreated = ({
tenantId,
}: IVendorCreditCreatedPayload) => {
this.vendorCreditService.incrementSerialNumber(tenantId);
};
}

View File

@@ -0,0 +1,189 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import * as R from 'ramda';
import {
IVendorCredit,
ILedgerEntry,
AccountNormal,
IItemEntry,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export default class VendorCreditGLEntries {
@Inject()
private ledgerStorage: LedgerStorageService;
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieve the vendor credit GL common entry.
* @param {IVendorCredit} vendorCredit
* @returns {}
*/
public getVendorCreditGLCommonEntry = (vendorCredit: IVendorCredit) => {
return {
date: vendorCredit.vendorCreditDate,
currencyCode: vendorCredit.currencyCode,
exchangeRate: vendorCredit.exchangeRate,
transactionId: vendorCredit.id,
transactionType: 'VendorCredit',
transactionNumber: vendorCredit.vendorCreditNumber,
referenceNumber: vendorCredit.referenceNo,
credit: 0,
debit: 0,
branchId: vendorCredit.branchId,
};
};
/**
* Retrieves the vendor credit payable GL entry.
* @param {IVendorCredit} vendorCredit
* @param {number} APAccountId
* @returns {ILedgerEntry}
*/
public getVendorCreditPayableGLEntry = (
vendorCredit: IVendorCredit,
APAccountId: number
): ILedgerEntry => {
const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit);
return {
...commonEntity,
debit: vendorCredit.localAmount,
accountId: APAccountId,
contactId: vendorCredit.vendorId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the vendor credit item GL entry.
* @param {IVendorCredit} vendorCredit
* @param {IItemEntry} entry
* @returns {ILedgerEntry}
*/
public getVendorCreditGLItemEntry = R.curry(
(
vendorCredit: IVendorCredit,
entry: IItemEntry,
index: number
): ILedgerEntry => {
const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit);
const localAmount = entry.amount * vendorCredit.exchangeRate;
return {
...commonEntity,
credit: localAmount,
index: index + 2,
itemId: entry.itemId,
itemQuantity: entry.quantity,
accountId:
'inventory' === entry.item.type
? entry.item.inventoryAccountId
: entry.costAccountId || entry.item.costAccountId,
accountNormal: AccountNormal.DEBIT,
};
}
);
/**
* Retrieve the vendor credit GL entries.
* @param {IVendorCredit} vendorCredit -
* @param {number} receivableAccount -
* @return {ILedgerEntry[]}
*/
public getVendorCreditGLEntries = (
vendorCredit: IVendorCredit,
payableAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getVendorCreditPayableGLEntry(
vendorCredit,
payableAccountId
);
const getItemEntry = this.getVendorCreditGLItemEntry(vendorCredit);
const itemsEntries = vendorCredit.entries.map(getItemEntry);
return [payableEntry, ...itemsEntries];
};
/**
* Reverts the vendor credit associated GL entries.
* @param {number} tenantId
* @param {number} vendorCreditId
* @param {Knex.Transaction} trx
*/
public revertVendorCreditGLEntries = async (
tenantId: number,
vendorCreditId: number,
trx?: Knex.Transaction
): Promise<void> => {
await this.ledgerStorage.deleteByReference(
tenantId,
vendorCreditId,
'VendorCredit',
trx
);
};
/**
* Creates vendor credit associated GL entries.
* @param {number} tenantId
* @param {number} vendorCreditId
* @param {Knex.Transaction} trx
*/
public writeVendorCreditGLEntries = async (
tenantId: number,
vendorCreditId: number,
trx?: Knex.Transaction
) => {
const { accountRepository } = this.tenancy.repositories(tenantId);
const { VendorCredit } = this.tenancy.models(tenantId);
// Vendor credit with entries items.
const vendorCredit = await VendorCredit.query(trx)
.findById(vendorCreditId)
.withGraphFetched('entries.item');
// Retrieve the payable account (A/P) account.
const APAccount = await accountRepository.findOrCreateAccountsPayable(
vendorCredit.currencyCode,
{},
trx
);
// Saves the vendor credit GL entries.
const ledgerEntries = this.getVendorCreditGLEntries(
vendorCredit,
APAccount.id
);
const ledger = new Ledger(ledgerEntries);
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
/**
* Edits vendor credit associated GL entries.
* @param {number} tenantId
* @param {number} vendorCreditId
* @param {Knex.Transaction} trx
*/
public rewriteVendorCreditGLEntries = async (
tenantId: number,
vendorCreditId: number,
trx?: Knex.Transaction
) => {
// Reverts the GL entries.
await this.revertVendorCreditGLEntries(tenantId, vendorCreditId, trx);
// Re-write the GL entries.
await this.writeVendorCreditGLEntries(tenantId, vendorCreditId, trx);
};
}

View File

@@ -0,0 +1,110 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import {
IVendorCreditCreatedPayload,
IVendorCreditDeletedPayload,
IVendorCreditEditedPayload,
IVendorCreditOpenedPayload,
} from '@/interfaces';
import VendorCreditGLEntries from './VendorCreditGLEntries';
@Service()
export default class VendorCreditGlEntriesSubscriber {
@Inject()
private vendorCreditGLEntries: VendorCreditGLEntries;
/***
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.vendorCredit.onCreated,
this.writeGLEntriesOnceVendorCreditCreated
);
bus.subscribe(
events.vendorCredit.onOpened,
this.writeGLEntgriesOnceVendorCreditOpened
);
bus.subscribe(
events.vendorCredit.onEdited,
this.editGLEntriesOnceVendorCreditEdited
);
bus.subscribe(
events.vendorCredit.onDeleted,
this.revertGLEntriesOnceDeleted
);
}
/**
* Writes GL entries of vendor credit once the transaction created.
* @param {IVendorCreditCreatedPayload} payload -
*/
private writeGLEntriesOnceVendorCreditCreated = async ({
tenantId,
vendorCredit,
trx,
}: IVendorCreditCreatedPayload): Promise<void> => {
// Can't continue if the vendor credit is not open yet.
if (!vendorCredit.isPublished) return;
await this.vendorCreditGLEntries.writeVendorCreditGLEntries(
tenantId,
vendorCredit.id,
trx
);
};
/**
* Writes Gl entries of vendor credit once the transaction opened.
* @param {IVendorCreditOpenedPayload} payload -
*/
private writeGLEntgriesOnceVendorCreditOpened = async ({
tenantId,
vendorCreditId,
trx,
}: IVendorCreditOpenedPayload) => {
await this.vendorCreditGLEntries.writeVendorCreditGLEntries(
tenantId,
vendorCreditId,
trx
);
};
/**
* Edits assocaited GL entries once vendor credit edited.
* @param {IVendorCreditEditedPayload} payload
*/
private editGLEntriesOnceVendorCreditEdited = async ({
tenantId,
vendorCreditId,
vendorCredit,
trx,
}: IVendorCreditEditedPayload) => {
// Can't continue if the vendor credit is not open yet.
if (!vendorCredit.isPublished) return;
await this.vendorCreditGLEntries.rewriteVendorCreditGLEntries(
tenantId,
vendorCreditId,
trx
);
};
/**
* Reverts the GL entries once vendor credit deleted.
* @param {IVendorCreditDeletedPayload} payload -
*/
private revertGLEntriesOnceDeleted = async ({
vendorCreditId,
tenantId,
oldVendorCredit,
}: IVendorCreditDeletedPayload): Promise<void> => {
// Can't continue of the vendor credit is not open yet.
if (!oldVendorCredit.isPublished) return;
await this.vendorCreditGLEntries.revertVendorCreditGLEntries(
tenantId,
vendorCreditId
);
};
}

View File

@@ -0,0 +1,92 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import { IVendorCredit } from '@/interfaces';
import InventoryService from '@/services/Inventory/Inventory';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
@Service()
export default class VendorCreditInventoryTransactions {
@Inject()
inventoryService: InventoryService;
@Inject()
itemsEntriesService: ItemsEntriesService;
/**
* Creates vendor credit associated inventory transactions.
* @param {number} tenantId
* @param {IVnedorCredit} vendorCredit
* @param {Knex.Transaction} trx
*/
public createInventoryTransactions = async (
tenantId: number,
vendorCredit: IVendorCredit,
trx: Knex.Transaction
): Promise<void> => {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
vendorCredit.entries
);
const transaction = {
transactionId: vendorCredit.id,
transactionType: 'VendorCredit',
transactionNumber: vendorCredit.vendorCreditNumber,
exchangeRate: vendorCredit.exchangeRate,
date: vendorCredit.vendorCreditDate,
direction: 'OUT',
entries: inventoryEntries,
warehouseId: vendorCredit.warehouseId,
createdAt: vendorCredit.createdAt,
};
// Writes inventory tranactions.
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId,
transaction,
false,
trx
);
};
/**
* Edits vendor credit assocaited inventory transactions.
* @param {number} tenantId
* @param {number} creditNoteId
* @param {ICreditNote} creditNote
* @param {Knex.Transactions} trx
*/
public editInventoryTransactions = async (
tenantId: number,
vendorCreditId: number,
vendorCredit: IVendorCredit,
trx?: Knex.Transaction
): Promise<void> => {
// Deletes inventory transactions.
await this.deleteInventoryTransactions(tenantId, vendorCreditId, trx);
// Re-write inventory transactions.
await this.createInventoryTransactions(tenantId, vendorCredit, 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 (
tenantId: number,
vendorCreditId: number,
trx?: Knex.Transaction
): Promise<void> => {
// Deletes the inventory transactions by the given reference id and type.
await this.inventoryService.deleteInventoryTransactions(
tenantId,
vendorCreditId,
'VendorCredit',
trx
);
};
}

View File

@@ -0,0 +1,83 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
IVendorCreditCreatedPayload,
IVendorCreditDeletedPayload,
IVendorCreditEditedPayload,
} from '@/interfaces';
import VendorCreditInventoryTransactions from './VendorCreditInventoryTransactions';
@Service()
export default class VendorCreditInventoryTransactionsSubscriber {
@Inject()
inventoryTransactions: VendorCreditInventoryTransactions;
/**
* Attaches events with handlers.
* @param bus
*/
attach(bus) {
bus.subscribe(
events.vendorCredit.onCreated,
this.writeInventoryTransactionsOnceCreated
);
bus.subscribe(
events.vendorCredit.onEdited,
this.rewriteInventroyTransactionsOnceEdited
);
bus.subscribe(
events.vendorCredit.onDeleted,
this.revertInventoryTransactionsOnceDeleted
);
}
/**
* Writes inventory transactions once vendor created created.
* @param {IVendorCreditCreatedPayload} payload -
*/
private writeInventoryTransactionsOnceCreated = async ({
tenantId,
vendorCredit,
trx,
}: IVendorCreditCreatedPayload) => {
await this.inventoryTransactions.createInventoryTransactions(
tenantId,
vendorCredit,
trx
);
};
/**
* Rewrites inventory transactions once vendor credit edited.
* @param {IVendorCreditEditedPayload} payload -
*/
private rewriteInventroyTransactionsOnceEdited = async ({
tenantId,
vendorCreditId,
vendorCredit,
trx,
}: IVendorCreditEditedPayload) => {
await this.inventoryTransactions.editInventoryTransactions(
tenantId,
vendorCreditId,
vendorCredit,
trx
);
};
/**
* Reverts inventory transactions once vendor credit deleted.
* @param {IVendorCreditDeletedPayload} payload -
*/
private revertInventoryTransactionsOnceDeleted = async ({
tenantId,
vendorCreditId,
trx,
}: IVendorCreditDeletedPayload) => {
await this.inventoryTransactions.deleteInventoryTransactions(
tenantId,
vendorCreditId,
trx
);
};
}

View File

@@ -0,0 +1,47 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class VendorCreditTransformer extends Transformer {
/**
* Include these attributes to vendor credit object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedVendorCreditDate',
'formattedAmount',
'formattedCreditsRemaining',
];
};
/**
* Retrieve formatted vendor credit date.
* @param {IVendorCredit} credit
* @returns {String}
*/
protected formattedVendorCreditDate = (vendorCredit): string => {
return this.formatDate(vendorCredit.vendorCreditDate);
};
/**
* Retrieve formatted vendor credit amount.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected formattedAmount = (vendorCredit): string => {
return formatNumber(vendorCredit.amount, {
currencyCode: vendorCredit.currencyCode,
});
};
/**
* Retrieve formatted credits remaining.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected formattedCreditsRemaining = (credit) => {
return formatNumber(credit.creditsRemaining, {
currencyCode: credit.currencyCode,
});
};
}

View File

@@ -0,0 +1,64 @@
export const ERRORS = {
VENDOR_CREDIT_NOT_FOUND: 'VENDOR_CREDIT_NOT_FOUND',
VENDOR_CREDIT_ALREADY_OPENED: 'VENDOR_CREDIT_ALREADY_OPENED',
VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT: 'VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT',
VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND: 'VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND',
BILLS_HAS_NO_REMAINING_AMOUNT: 'BILLS_HAS_NO_REMAINING_AMOUNT',
VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS: 'VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS',
VENDOR_CREDIT_HAS_APPLIED_BILLS: 'VENDOR_CREDIT_HAS_APPLIED_BILLS'
};
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'vendor_credit.view.draft',
slug: 'draft',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'vendor_credit.view.published',
slug: 'published',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'published',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'vendor_credit.view.open',
slug: 'open',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'open',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'vendor_credit.view.closed',
slug: 'closed',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'closed',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
];

View File

@@ -0,0 +1,76 @@
export const ERRORS = {
BILL_NOT_FOUND: 'BILL_NOT_FOUND',
BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND',
BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE',
BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS',
BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED',
BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS',
BILL_HAS_ASSOCIATED_LANDED_COSTS: 'BILL_HAS_ASSOCIATED_LANDED_COSTS',
BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED:
'BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED',
LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES:
'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS:
'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS',
BILL_HAS_APPLIED_TO_VENDOR_CREDIT: 'BILL_HAS_APPLIED_TO_VENDOR_CREDIT',
};
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'Draft',
slug: 'draft',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Opened',
slug: 'opened',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'opened' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Unpaid',
slug: 'unpaid',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Overdue',
slug: 'overdue',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'overdue' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Partially paid',
slug: 'partially-paid',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'partially-paid',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
];