mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
add server to monorepo.
This commit is contained in:
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
@@ -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()
|
||||
);
|
||||
};
|
||||
}
|
||||
751
packages/server/src/services/Purchases/Bills.ts
Normal file
751
packages/server/src/services/Purchases/Bills.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
219
packages/server/src/services/Purchases/Bills/BillGLEntries.ts
Normal file
219
packages/server/src/services/Purchases/Bills/BillGLEntries.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
46
packages/server/src/services/Purchases/LandedCost/utils.ts
Normal file
46
packages/server/src/services/Purchases/LandedCost/utils.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
76
packages/server/src/services/Purchases/constants.ts
Normal file
76
packages/server/src/services/Purchases/constants.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user