add server to monorepo.

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

View File

@@ -0,0 +1,93 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
ICreditNoteCreatedPayload,
ICreditNoteCreatingPayload,
ICreditNoteNewDTO,
ISystemUser,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import BaseCreditNotes from './CreditNotes';
@Service()
export default class CreateCreditNote extends BaseCreditNotes {
@Inject()
uow: UnitOfWork;
@Inject()
itemsEntriesService: ItemsEntriesService;
@Inject()
tenancy: HasTenancyService;
@Inject()
eventPublisher: EventPublisher;
/**
* Creates a new credit note.
* @param creditNoteDTO
*/
public newCreditNote = async (
tenantId: number,
creditNoteDTO: ICreditNoteNewDTO,
authorizedUser: ISystemUser
) => {
const { CreditNote, Contact } = this.tenancy.models(tenantId);
// Triggers `onCreditNoteCreate` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreate, {
tenantId,
creditNoteDTO,
});
// Validate customer existance.
const customer = await Contact.query()
.modify('customer')
.findById(creditNoteDTO.customerId)
.throwIfNotFound();
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
creditNoteDTO.entries
);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
creditNoteDTO.entries
);
// Transformes the given DTO to storage layer data.
const creditNoteModel = this.transformCreateEditDTOToModel(
tenantId,
creditNoteDTO,
customer.currencyCode
);
// Creates a new credit card transactions under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteCreating` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreating, {
tenantId,
creditNoteDTO,
trx,
} as ICreditNoteCreatingPayload);
// Upsert the credit note graph.
const creditNote = await CreditNote.query(trx).upsertGraph({
...creditNoteModel,
});
// Triggers `onCreditNoteCreated` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
tenantId,
creditNoteDTO,
creditNote,
creditNoteId: creditNote.id,
trx,
} as ICreditNoteCreatedPayload);
return creditNote;
});
};
}

View File

@@ -0,0 +1,101 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import {
ICreditNote,
ICreditNoteRefundDTO,
IRefundCreditNote,
IRefundCreditNoteCreatedPayload,
IRefundCreditNoteCreatingPayload,
} 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 RefundCreditNote from './RefundCreditNote';
@Service()
export default class CreateRefundCreditNote extends RefundCreditNote {
@Inject()
tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve the credit note graph.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<IRefundCreditNote>}
*/
public createCreditNoteRefund = async (
tenantId: number,
creditNoteId: number,
newCreditNoteDTO: ICreditNoteRefundDTO
): Promise<IRefundCreditNote> => {
const { RefundCreditNote, Account } = this.tenancy.models(tenantId);
// Retrieve the credit note or throw not found service error.
const creditNote = await this.getCreditNoteOrThrowError(
tenantId,
creditNoteId
);
// Retrieve the withdrawal account or throw not found service error.
const fromAccount = await Account.query()
.findById(newCreditNoteDTO.fromAccountId)
.throwIfNotFound();
// Validate the credit note remaining amount.
this.validateCreditRemainingAmount(creditNote, newCreditNoteDTO.amount);
// Validate the refund withdrawal account type.
this.validateRefundWithdrawwalAccountType(fromAccount);
// Creates a refund credit note transaction.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteRefundCreating` event.
await this.eventPublisher.emitAsync(events.creditNote.onRefundCreating, {
trx,
creditNote,
tenantId,
newCreditNoteDTO,
} as IRefundCreditNoteCreatingPayload);
// Stores the refund credit note graph to the storage layer.
const refundCreditNote = await RefundCreditNote.query(trx).insertAndFetch(
{
...this.transformDTOToModel(creditNote, newCreditNoteDTO),
}
);
// Triggers `onCreditNoteRefundCreated` event.
await this.eventPublisher.emitAsync(events.creditNote.onRefundCreated, {
trx,
refundCreditNote,
creditNote,
tenantId,
} as IRefundCreditNoteCreatedPayload);
return refundCreditNote;
});
};
/**
* Transformes the refund credit note DTO to model.
* @param {number} creditNoteId
* @param {ICreditNoteRefundDTO} creditNoteDTO
* @returns {ICreditNote}
*/
private transformDTOToModel = (
creditNote: ICreditNote,
creditNoteDTO: ICreditNoteRefundDTO
): IRefundCreditNote => {
return {
creditNoteId: creditNote.id,
currencyCode: creditNote.currencyCode,
...creditNoteDTO,
exchangeRate: creditNoteDTO.exchangeRate || 1,
};
};
}

View File

@@ -0,0 +1,53 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class CreditNoteAppliedInvoiceTransformer extends Transformer {
/**
* Includeded attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'formttedAmount',
'creditNoteNumber',
'creditNoteDate',
'invoiceNumber',
'invoiceReferenceNo',
'formattedCreditNoteDate',
];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['saleInvoice', 'creditNote'];
};
formttedAmount = (item) => {
return formatNumber(item.amount, {
currencyCode: item.creditNote.currencyCode,
});
};
creditNoteNumber = (item) => {
return item.creditNote.creditNoteNumber;
};
creditNoteDate = (item) => {
return item.creditNote.creditNoteDate;
};
invoiceNumber = (item) => {
return item.saleInvoice.invoiceNo;
};
invoiceReferenceNo = (item) => {
return item.saleInvoice.referenceNo;
};
formattedCreditNoteDate = (item) => {
return this.formatDate(item.creditNote.creditNoteDate);
};
}

View File

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

View File

@@ -0,0 +1,67 @@
import { Service, Inject } from 'typedi';
import { sumBy } from 'lodash';
import events from '@/subscribers/events';
import {
IApplyCreditToInvoicesCreatedPayload,
IApplyCreditToInvoicesDeletedPayload,
} from '@/interfaces';
import CreditNoteApplySyncCredit from './CreditNoteApplySyncCredit';
@Service()
export default class CreditNoteApplySyncCreditSubscriber {
@Inject()
syncInvoicedAmountWithCredit: CreditNoteApplySyncCredit;
/**
*
* @param bus
*/
attach(bus) {
bus.subscribe(
events.creditNote.onApplyToInvoicesCreated,
this.incrementCreditedAmountOnceApplyToInvoicesCreated
);
bus.subscribe(
events.creditNote.onApplyToInvoicesDeleted,
this.decrementCreditedAmountOnceApplyToInvoicesDeleted
);
}
/**
* Increment credited amount of credit note transaction once the transaction created.
* @param {IApplyCreditToInvoicesCreatedPayload} payload -
*/
private incrementCreditedAmountOnceApplyToInvoicesCreated = async ({
trx,
creditNote,
tenantId,
creditNoteAppliedInvoices,
}: IApplyCreditToInvoicesCreatedPayload) => {
const totalCredited = sumBy(creditNoteAppliedInvoices, 'amount');
await this.syncInvoicedAmountWithCredit.incrementCreditNoteInvoicedAmount(
tenantId,
creditNote.id,
totalCredited,
trx
);
};
/**
* Decrement credited amount of credit note transaction once the transaction deleted.
* @param {IApplyCreditToInvoicesDeletedPayload} payload -
*/
private decrementCreditedAmountOnceApplyToInvoicesDeleted = async ({
tenantId,
creditNote,
creditNoteAppliedToInvoice,
trx,
}: IApplyCreditToInvoicesDeletedPayload) => {
await this.syncInvoicedAmountWithCredit.decrementCreditNoteInvoicedAmount(
tenantId,
creditNote.id,
creditNoteAppliedToInvoice.amount,
trx
);
};
}

View File

@@ -0,0 +1,53 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Knex from 'knex';
import Bluebird from 'bluebird';
import { ICreditNoteAppliedToInvoice } from '@/interfaces';
@Service()
export default class CreditNoteApplySyncInvoicesCreditedAmount {
@Inject()
tenancy: HasTenancyService;
/**
* Increment invoices credited amount.
* @param {number} tenantId -
* @param {ICreditNoteAppliedToInvoice[]} creditNoteAppliedInvoices -
* @param {Knex.Transaction} trx -
*/
public incrementInvoicesCreditedAmount = async (
tenantId,
creditNoteAppliedInvoices: ICreditNoteAppliedToInvoice[],
trx?: Knex.Transaction
) => {
const { SaleInvoice } = this.tenancy.models(tenantId);
await Bluebird.each(
creditNoteAppliedInvoices,
(creditNoteAppliedInvoice: ICreditNoteAppliedToInvoice) => {
return SaleInvoice.query(trx)
.where('id', creditNoteAppliedInvoice.invoiceId)
.increment('creditedAmount', creditNoteAppliedInvoice.amount);
}
);
};
/**
*
* @param tenantId
* @param invoicesIds
* @param amount
*/
public decrementInvoiceCreditedAmount = async (
tenantId: number,
invoiceId: number,
amount: number,
trx?: Knex.Transaction
) => {
const { SaleInvoice } = this.tenancy.models(tenantId);
await SaleInvoice.query(trx)
.findById(invoiceId)
.decrement('creditedAmount', amount);
};
}

View File

@@ -0,0 +1,65 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import {
IApplyCreditToInvoicesCreatedPayload,
IApplyCreditToInvoicesDeletedPayload,
} from '@/interfaces';
import CreditNoteApplySyncInvoicesCreditedAmount from './CreditNoteApplySyncInvoices';
@Service()
export default class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
@Inject()
tenancy: HasTenancyService;
@Inject()
syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount;
/**
* Attaches events with handlers.
*/
attach(bus) {
bus.subscribe(
events.creditNote.onApplyToInvoicesCreated,
this.incrementAppliedInvoicesOnceCreditCreated
);
bus.subscribe(
events.creditNote.onApplyToInvoicesDeleted,
this.decrementAppliedInvoicesOnceCreditDeleted
);
}
/**
* Increment invoices credited amount once the credit note apply to invoices transaction
* @param {IApplyCreditToInvoicesCreatedPayload} payload -
*/
private incrementAppliedInvoicesOnceCreditCreated = async ({
trx,
tenantId,
creditNoteAppliedInvoices,
}: IApplyCreditToInvoicesCreatedPayload) => {
await this.syncInvoicesWithCreditNote.incrementInvoicesCreditedAmount(
tenantId,
creditNoteAppliedInvoices,
trx
);
};
/**
*
* @param {IApplyCreditToInvoicesDeletedPayload} payload -
*/
private decrementAppliedInvoicesOnceCreditDeleted = async ({
trx,
creditNoteAppliedToInvoice,
tenantId,
}: IApplyCreditToInvoicesDeletedPayload) => {
// Decrement invoice credited amount.
await this.syncInvoicesWithCreditNote.decrementInvoiceCreditedAmount(
tenantId,
creditNoteAppliedToInvoice.invoiceId,
creditNoteAppliedToInvoice.amount,
trx
);
};
}

View File

@@ -0,0 +1,131 @@
import { Service, Inject } from 'typedi';
import Knex from 'knex';
import { sumBy } from 'lodash';
import {
ICreditNote,
ICreditNoteAppliedToInvoice,
ICreditNoteAppliedToInvoiceModel,
ISaleInvoice,
} 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 BaseCreditNotes from './CreditNotes';
import {
IApplyCreditToInvoicesDTO,
IApplyCreditToInvoicesCreatedPayload,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
@Service()
export default class CreditNoteApplyToInvoices extends BaseCreditNotes {
@Inject('PaymentReceives')
paymentReceive: PaymentReceiveService;
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* Apply credit note to the given invoices.
* @param {number} tenantId
* @param {number} creditNoteId
* @param {IApplyCreditToInvoicesDTO} applyCreditToInvoicesDTO
*/
public applyCreditNoteToInvoices = async (
tenantId: number,
creditNoteId: number,
applyCreditToInvoicesDTO: IApplyCreditToInvoicesDTO
): Promise<ICreditNoteAppliedToInvoice[]> => {
const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId);
// Saves the credit note or throw not found service error.
const creditNote = await this.getCreditNoteOrThrowError(
tenantId,
creditNoteId
);
// Retrieve the applied invoices that associated to the credit note customer.
const appliedInvoicesEntries =
await this.paymentReceive.validateInvoicesIDsExistance(
tenantId,
creditNote.customerId,
applyCreditToInvoicesDTO.entries
);
// Transformes apply DTO to model.
const creditNoteAppliedModel = this.transformApplyDTOToModel(
applyCreditToInvoicesDTO,
creditNote
);
// Validate invoices has remaining amount to apply.
this.validateInvoicesRemainingAmount(
appliedInvoicesEntries,
creditNoteAppliedModel.amount
);
// Validate the credit note remaining amount.
this.validateCreditRemainingAmount(
creditNote,
creditNoteAppliedModel.amount
);
// Creates credit note apply to invoice transaction.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Saves the credit note apply to invoice graph to the storage layer.
const creditNoteAppliedInvoices =
await CreditNoteAppliedInvoice.query().insertGraph(
creditNoteAppliedModel.entries
);
// Triggers `onCreditNoteApplyToInvoiceCreated` event.
await this.eventPublisher.emitAsync(
events.creditNote.onApplyToInvoicesCreated,
{
tenantId,
creditNote,
creditNoteAppliedInvoices,
trx,
} as IApplyCreditToInvoicesCreatedPayload
);
return creditNoteAppliedInvoices;
});
};
/**
* Transformes apply DTO to model.
* @param {IApplyCreditToInvoicesDTO} applyDTO
* @param {ICreditNote} creditNote
* @returns
*/
private transformApplyDTOToModel = (
applyDTO: IApplyCreditToInvoicesDTO,
creditNote: ICreditNote
): ICreditNoteAppliedToInvoiceModel => {
const entries = applyDTO.entries.map((entry) => ({
invoiceId: entry.invoiceId,
amount: entry.amount,
creditNoteId: creditNote.id,
}));
return {
amount: sumBy(entries, 'amount'),
entries,
};
};
/**
* Validate the invoice remaining amount.
* @param {ISaleInvoice[]} invoices
* @param {number} amount
*/
private validateInvoicesRemainingAmount = (
invoices: ISaleInvoice[],
amount: number
) => {
const invalidInvoices = invoices.filter(
(invoice) => invoice.dueAmount < amount
);
if (invalidInvoices.length > 0) {
throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT);
}
};
}

View File

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

View File

@@ -0,0 +1,226 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import * as R from 'ramda';
import {
AccountNormal,
IItemEntry,
ILedgerEntry,
ICreditNote,
ILedger,
ICreditNoteGLCommonEntry,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
@Service()
export default class CreditNoteGLEntries {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private ledgerStorage: LedgerStorageService;
/**
* Retrieves the credit note GL.
* @param {ICreditNote} creditNote
* @param {number} receivableAccount
* @returns {Ledger}
*/
private getCreditNoteGLedger = (
creditNote: ICreditNote,
receivableAccount: number
): Ledger => {
const ledgerEntries = this.getCreditNoteGLEntries(
creditNote,
receivableAccount
);
return new Ledger(ledgerEntries);
};
/**
* Saves credit note GL entries.
* @param {number} tenantId -
* @param {ICreditNote} creditNote - Credit note model.
* @param {number} payableAccount - Payable account id.
* @param {Knex.Transaction} trx
*/
public saveCreditNoteGLEntries = async (
tenantId: number,
creditNote: ICreditNote,
payableAccount: number,
trx?: Knex.Transaction
): Promise<void> => {
const ledger = this.getCreditNoteGLedger(creditNote, payableAccount);
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
/**
* Reverts the credit note associated GL entries.
* @param {number} tenantId
* @param {number} vendorCreditId
* @param {Knex.Transaction} trx
*/
public revertVendorCreditGLEntries = async (
tenantId: number,
creditNoteId: number,
trx?: Knex.Transaction
): Promise<void> => {
await this.ledgerStorage.deleteByReference(
tenantId,
creditNoteId,
'CreditNote',
trx
);
};
/**
* Writes vendor credit associated GL entries.
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
* @param {Knex.Transaction} trx - Knex transactions.
*/
public createVendorCreditGLEntries = async (
tenantId: number,
creditNoteId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { CreditNote } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Retrieve the credit note with associated entries and items.
const creditNoteWithItems = await CreditNote.query(trx)
.findById(creditNoteId)
.withGraphFetched('entries.item');
// Retreive the the `accounts receivable` account based on the given currency.
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
creditNoteWithItems.currencyCode
);
// Saves the credit note GL entries.
await this.saveCreditNoteGLEntries(
tenantId,
creditNoteWithItems,
ARAccount.id,
trx
);
};
/**
* Edits vendor credit associated GL entries.
* @param {number} tenantId
* @param {number} creditNoteId
* @param {Knex.Transaction} trx
*/
public editVendorCreditGLEntries = async (
tenantId: number,
creditNoteId: number,
trx?: Knex.Transaction
): Promise<void> => {
// Reverts vendor credit GL entries.
await this.revertVendorCreditGLEntries(tenantId, creditNoteId, trx);
// Creates vendor credit Gl entries.
await this.createVendorCreditGLEntries(tenantId, creditNoteId, trx);
};
/**
* Retrieve the credit note common entry.
* @param {ICreditNote} creditNote -
* @returns {ICreditNoteGLCommonEntry}
*/
private getCreditNoteCommonEntry = (
creditNote: ICreditNote
): ICreditNoteGLCommonEntry => {
return {
date: creditNote.creditNoteDate,
userId: creditNote.userId,
currencyCode: creditNote.currencyCode,
exchangeRate: creditNote.exchangeRate,
transactionType: 'CreditNote',
transactionId: creditNote.id,
transactionNumber: creditNote.creditNoteNumber,
referenceNumber: creditNote.referenceNo,
createdAt: creditNote.createdAt,
indexGroup: 10,
credit: 0,
debit: 0,
branchId: creditNote.branchId,
};
};
/**
* Retrieves the creidt note A/R entry.
* @param {ICreditNote} creditNote -
* @param {number} ARAccountId -
* @returns {ILedgerEntry}
*/
private getCreditNoteAREntry = (
creditNote: ICreditNote,
ARAccountId: number
): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
return {
...commonEntry,
credit: creditNote.localAmount,
accountId: ARAccountId,
contactId: creditNote.customerId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieve the credit note item entry.
* @param {ICreditNote} creditNote
* @param {IItemEntry} entry
* @param {number} index
* @returns {ILedgerEntry}
*/
private getCreditNoteItemEntry = R.curry(
(
creditNote: ICreditNote,
entry: IItemEntry,
index: number
): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
const localAmount = entry.amount * creditNote.exchangeRate;
return {
...commonEntry,
debit: localAmount,
accountId: entry.sellAccountId || entry.item.sellAccountId,
note: entry.description,
index: index + 2,
itemId: entry.itemId,
itemQuantity: entry.quantity,
accountNormal: AccountNormal.CREDIT,
};
}
);
/**
* Retrieve the credit note GL entries.
* @param {ICreditNote} creditNote - Credit note.
* @param {IAccount} receivableAccount - Receviable account.
* @returns {ILedgerEntry[]} - Ledger entries.
*/
public getCreditNoteGLEntries = (
creditNote: ICreditNote,
ARAccountId: number
): ILedgerEntry[] => {
const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId);
const getItemEntry = this.getCreditNoteItemEntry(creditNote);
const itemsEntries = creditNote.entries.map(getItemEntry);
return [AREntry, ...itemsEntries];
};
}

View File

@@ -0,0 +1,118 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import {
ICreditNoteCreatedPayload,
ICreditNoteDeletedPayload,
ICreditNoteEditedPayload,
ICreditNoteOpenedPayload,
IRefundCreditNoteOpenedPayload,
} from '@/interfaces';
import CreditNoteGLEntries from './CreditNoteGLEntries';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class CreditNoteGLEntriesSubscriber {
@Inject()
creditNoteGLEntries: CreditNoteGLEntries;
@Inject()
tenancy: HasTenancyService;
/**
* Attaches events with handlers.
* @param bus
*/
public attach(bus) {
bus.subscribe(
events.creditNote.onCreated,
this.writeGlEntriesOnceCreditNoteCreated
);
bus.subscribe(
events.creditNote.onOpened,
this.writeGLEntriesOnceCreditNoteOpened
);
bus.subscribe(
events.creditNote.onEdited,
this.editVendorCreditGLEntriesOnceEdited
);
bus.subscribe(
events.creditNote.onDeleted,
this.revertGLEntriesOnceCreditNoteDeleted
);
}
/**
* Writes the GL entries once the credit note transaction created or open.
* @private
* @param {ICreditNoteCreatedPayload|ICreditNoteOpenedPayload} payload -
*/
private writeGlEntriesOnceCreditNoteCreated = async ({
tenantId,
creditNote,
creditNoteId,
trx,
}: ICreditNoteCreatedPayload | ICreditNoteOpenedPayload) => {
// Can't continue if the credit note is not published yet.
if (!creditNote.isPublished) return;
await this.creditNoteGLEntries.createVendorCreditGLEntries(
tenantId,
creditNoteId,
trx
);
};
/**
* Writes the GL entries once the vendor credit transaction opened.
* @param {ICreditNoteOpenedPayload} payload
*/
private writeGLEntriesOnceCreditNoteOpened = async ({
tenantId,
creditNoteId,
trx,
}: ICreditNoteOpenedPayload) => {
await this.creditNoteGLEntries.createVendorCreditGLEntries(
tenantId,
creditNoteId,
trx
);
};
/**
* Reverts GL entries once credit note deleted.
*/
private revertGLEntriesOnceCreditNoteDeleted = async ({
tenantId,
oldCreditNote,
creditNoteId,
trx,
}: ICreditNoteDeletedPayload) => {
// Can't continue if the credit note is not published yet.
if (!oldCreditNote.isPublished) return;
await this.creditNoteGLEntries.revertVendorCreditGLEntries(
tenantId,
creditNoteId
);
};
/**
* Edits vendor credit associated GL entries once the transaction edited.
* @param {ICreditNoteEditedPayload} payload -
*/
private editVendorCreditGLEntriesOnceEdited = async ({
tenantId,
creditNote,
creditNoteId,
trx,
}: ICreditNoteEditedPayload) => {
// Can't continue if the credit note is not published yet.
if (!creditNote.isPublished) return;
await this.creditNoteGLEntries.editVendorCreditGLEntries(
tenantId,
creditNoteId,
trx
);
};
}

View File

@@ -0,0 +1,99 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import CreditNoteInventoryTransactions from './CreditNotesInventoryTransactions';
import {
ICreditNoteCreatedPayload,
ICreditNoteDeletedPayload,
ICreditNoteEditedPayload,
} from '@/interfaces';
@Service()
export default class CreditNoteInventoryTransactionsSubscriber {
@Inject()
inventoryTransactions: CreditNoteInventoryTransactions;
/**
* Attaches events with publisher.
*/
attach(bus) {
bus.subscribe(
events.creditNote.onCreated,
this.writeInventoryTranscationsOnceCreated
);
bus.subscribe(
events.creditNote.onEdited,
this.rewriteInventoryTransactionsOnceEdited
);
bus.subscribe(
events.creditNote.onDeleted,
this.revertInventoryTransactionsOnceDeleted
);
bus.subscribe(
events.creditNote.onOpened,
this.writeInventoryTranscationsOnceCreated
);
}
/**
* Writes inventory transactions once credit note created.
* @param {ICreditNoteCreatedPayload} payload -
*/
public writeInventoryTranscationsOnceCreated = async ({
tenantId,
creditNote,
trx,
}: ICreditNoteCreatedPayload) => {
// Can't continue if the credit note is open yet.
if (!creditNote.isOpen) {
return;
}
await this.inventoryTransactions.createInventoryTransactions(
tenantId,
creditNote,
trx
);
};
/**
* Rewrites inventory transactions once credit note edited.
* @param {ICreditNoteEditedPayload} payload -
*/
public rewriteInventoryTransactionsOnceEdited = async ({
tenantId,
creditNoteId,
creditNote,
trx,
}: ICreditNoteEditedPayload) => {
// Can't continue if the credit note is open yet.
if (!creditNote.isOpen) {
return;
}
await this.inventoryTransactions.editInventoryTransactions(
tenantId,
creditNoteId,
creditNote,
trx
);
};
/**
* Reverts inventory transactions once credit note deleted.
* @param {ICreditNoteDeletedPayload} payload -
*/
public revertInventoryTransactionsOnceDeleted = async ({
tenantId,
creditNoteId,
oldCreditNote,
trx,
}: ICreditNoteDeletedPayload) => {
// Can't continue if the credit note is open yet.
if (!oldCreditNote.isOpen) {
return;
}
await this.inventoryTransactions.deleteInventoryTransactions(
tenantId,
creditNoteId,
trx
);
};
}

View File

@@ -0,0 +1,59 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class CreditNoteTransformer extends Transformer {
/**
* Include these attributes to sale credit note object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedCreditsRemaining',
'formattedCreditNoteDate',
'formattedAmount',
'formattedCreditsUsed'
];
};
/**
* Retrieve formatted credit note date.
* @param {ICreditNote} credit
* @returns {String}
*/
protected formattedCreditNoteDate = (credit): string => {
return this.formatDate(credit.creditNoteDate);
};
/**
* Retrieve formatted invoice amount.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedAmount = (credit): string => {
return formatNumber(credit.amount, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieve formatted credits remaining.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedCreditsRemaining = (credit) => {
return formatNumber(credit.creditsRemaining, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieve formatted credits used.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedCreditsUsed = (credit) => {
return formatNumber(credit.creditsUsed, {
currencyCode: credit.currencyCode,
});
};
}

View File

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

View File

@@ -0,0 +1,134 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import { omit } from 'lodash';
import * as R from 'ramda';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { ICreditNote, ICreditNoteEditDTO, ICreditNoteNewDTO } from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
@Service()
export default class BaseCreditNotes {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
/**
* Transformes the credit/edit DTO to model.
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
* @param {string} customerCurrencyCode -
*/
protected transformCreateEditDTOToModel = (
tenantId: number,
creditNoteDTO: ICreditNoteNewDTO | ICreditNoteEditDTO,
customerCurrencyCode: string,
oldCreditNote?: ICreditNote
): ICreditNote => {
// Retrieve the total amount of the given items entries.
const amount = this.itemsEntriesService.getTotalItemsEntries(
creditNoteDTO.entries
);
const entries = creditNoteDTO.entries.map((entry) => ({
...entry,
referenceType: 'CreditNote',
}));
// Retreive the next credit note number.
const autoNextNumber = this.getNextCreditNumber(tenantId);
// Detarmines the credit note number.
const creditNoteNumber =
creditNoteDTO.creditNoteNumber ||
oldCreditNote?.creditNoteNumber ||
autoNextNumber;
const initialDTO = {
...omit(creditNoteDTO, ['open']),
creditNoteNumber,
amount,
currencyCode: customerCurrencyCode,
exchangeRate: creditNoteDTO.exchangeRate || 1,
entries,
...(creditNoteDTO.open &&
!oldCreditNote?.openedAt && {
openedAt: moment().toMySqlDateTime(),
}),
refundedAmount: 0,
invoicesAmount: 0,
};
return R.compose(
this.branchDTOTransform.transformDTO<ICreditNote>(tenantId),
this.warehouseDTOTransform.transformDTO<ICreditNote>(tenantId)
)(initialDTO);
};
/**
* Retrieve the given credit note or throw not found service error.
* @param {number} tenantId -
* @param {number} creditNoteId -
*/
protected getCreditNoteOrThrowError = async (
tenantId: number,
creditNoteId: number
) => {
const { CreditNote } = this.tenancy.models(tenantId);
const creditNote = await CreditNote.query().findById(creditNoteId);
if (!creditNote) {
throw new ServiceError(ERRORS.CREDIT_NOTE_NOT_FOUND);
}
return creditNote;
};
/**
* Retrieve the next unique credit number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
private getNextCreditNumber = (tenantId: number): string => {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'credit_note'
);
};
/**
* Increment the credit note serial next number.
* @param {number} tenantId -
*/
public incrementSerialNumber = (tenantId: number) => {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'credit_note'
);
};
/**
* Validate the credit note remaining amount.
* @param {ICreditNote} creditNote
* @param {number} amount
*/
public validateCreditRemainingAmount = (
creditNote: ICreditNote,
amount: number
) => {
if (creditNote.creditsRemaining < amount) {
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT);
}
};
}

View File

@@ -0,0 +1,90 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ICreditNote } from '@/interfaces';
import InventoryService from '@/services/Inventory/Inventory';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
@Service()
export default class CreditNoteInventoryTransactions {
@Inject()
inventoryService: InventoryService;
@Inject()
itemsEntriesService: ItemsEntriesService;
/**
* Creates credit note inventory transactions.
* @param {number} tenantId
* @param {ICreditNote} creditNote
*/
public createInventoryTransactions = async (
tenantId: number,
creditNote: ICreditNote,
trx?: Knex.Transaction
): Promise<void> => {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
creditNote.entries
);
const transaction = {
transactionId: creditNote.id,
transactionType: 'CreditNote',
transactionNumber: creditNote.creditNoteNumber,
exchangeRate: creditNote.exchangeRate,
date: creditNote.creditNoteDate,
direction: 'IN',
entries: inventoryEntries,
createdAt: creditNote.createdAt,
warehouseId: creditNote.warehouseId,
};
// Writes inventory tranactions.
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
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,
creditNoteId: number,
creditNote: ICreditNote,
trx?: Knex.Transaction
): Promise<void> => {
// Deletes inventory transactions.
await this.deleteInventoryTransactions(tenantId, creditNoteId, trx);
// Re-write inventory transactions.
await this.createInventoryTransactions(tenantId, creditNote, trx);
};
/**
* Deletes credit note associated inventory transactions.
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
* @param {Knex.Transaction} trx -
*/
public deleteInventoryTransactions = async (
tenantId: number,
creditNoteId: number,
trx?: Knex.Transaction
): Promise<void> => {
// Deletes the inventory transactions by the given reference id and type.
await this.inventoryService.deleteInventoryTransactions(
tenantId,
creditNoteId,
'CreditNote',
trx
);
};
}

View File

@@ -0,0 +1,119 @@
import Knex from 'knex';
import { Inject, Service } from 'typedi';
import UnitOfWork from '@/services/UnitOfWork';
import BaseCreditNotes from './CreditNotes';
import { ICreditNoteDeletedPayload, ICreditNoteDeletingPayload } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import RefundCreditNote from './RefundCreditNote';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
@Service()
export default class DeleteCreditNote extends BaseCreditNotes {
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
@Inject()
refundCreditNote: RefundCreditNote;
/**
* Deletes the given credit note transactions.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<void>}
*/
public deleteCreditNote = async (
tenantId: number,
creditNoteId: number
): Promise<void> => {
const { CreditNote, ItemEntry } = this.tenancy.models(tenantId);
// Retrieve the credit note or throw not found service error.
const oldCreditNote = await this.getCreditNoteOrThrowError(
tenantId,
creditNoteId
);
// Validate credit note has no refund transactions.
await this.validateCreditNoteHasNoRefundTransactions(
tenantId,
creditNoteId
);
// Validate credit note has no applied invoices transactions.
await this.validateCreditNoteHasNoApplyInvoiceTransactions(
tenantId,
creditNoteId
);
// Deletes the credit note transactions under unit-of-work transaction.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteDeleting` event.
await this.eventPublisher.emitAsync(events.creditNote.onDeleting, {
trx,
tenantId,
oldCreditNote
} as ICreditNoteDeletingPayload);
// Delets the associated credit note entries.
await ItemEntry.query(trx)
.where('reference_id', creditNoteId)
.where('reference_type', 'CreditNote')
.delete();
// Deletes the credit note transaction.
await CreditNote.query(trx).findById(creditNoteId).delete();
// Triggers `onCreditNoteDeleted` event.
await this.eventPublisher.emitAsync(events.creditNote.onDeleted, {
tenantId,
oldCreditNote,
creditNoteId,
trx,
} as ICreditNoteDeletedPayload);
});
};
/**
* Validates credit note has no associated refund transactions.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<void>}
*/
private validateCreditNoteHasNoRefundTransactions = async (
tenantId: number,
creditNoteId: number
): Promise<void> => {
const { RefundCreditNote } = this.tenancy.models(tenantId);
const refundTransactions = await RefundCreditNote.query().where(
'creditNoteId',
creditNoteId
);
if (refundTransactions.length > 0) {
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS);
}
};
/**
* Validate credit note has no associated applied invoices transactions.
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
* @returns {Promise<void>}
*/
private validateCreditNoteHasNoApplyInvoiceTransactions = async (
tenantId: number,
creditNoteId: number
) => {
const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId);
const appliedTransactions = await CreditNoteAppliedInvoice.query().where(
'creditNoteId',
creditNoteId
);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_APPLIED_INVOICES);
}
};
}

View File

@@ -0,0 +1,65 @@
import { Service, Inject } from 'typedi';
import Knex from 'knex';
import { IApplyCreditToInvoicesDeletedPayload } 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 BaseCreditNotes from './CreditNotes';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
@Service()
export default class DeletreCreditNoteApplyToInvoices extends BaseCreditNotes {
@Inject('PaymentReceives')
paymentReceive: PaymentReceiveService;
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* Apply credit note to the given invoices.
* @param {number} tenantId
* @param {number} creditNoteId
* @param {IApplyCreditToInvoicesDTO} applyCreditToInvoicesDTO
*/
public deleteApplyCreditNoteToInvoices = async (
tenantId: number,
applyCreditToInvoicesId: number
): Promise<void> => {
const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId);
const creditNoteAppliedToInvoice =
await CreditNoteAppliedInvoice.query().findById(applyCreditToInvoicesId);
if (!creditNoteAppliedToInvoice) {
throw new ServiceError(ERRORS.CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND);
}
// Retrieve the credit note or throw not found service error.
const creditNote = await this.getCreditNoteOrThrowError(
tenantId,
creditNoteAppliedToInvoice.creditNoteId
);
// Creates credit note apply to invoice transaction.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Delete credit note applied to invoices.
await CreditNoteAppliedInvoice.query(trx)
.findById(applyCreditToInvoicesId)
.delete();
// Triggers `onCreditNoteApplyToInvoiceDeleted` event.
await this.eventPublisher.emitAsync(
events.creditNote.onApplyToInvoicesDeleted,
{
trx,
creditNote,
creditNoteAppliedToInvoice,
tenantId,
} as IApplyCreditToInvoicesDeletedPayload
);
});
};
}

View File

@@ -0,0 +1,30 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
@Service()
export default class DeleteCustomerLinkedCreidtNote {
@Inject()
tenancy: TenancyService;
/**
* Validate the given customer has no linked credit note transactions.
* @param {number} tenantId
* @param {number} creditNoteId
*/
public validateCustomerHasNoCreditTransaction = async (
tenantId: number,
customerId: number
) => {
const { CreditNote } = this.tenancy.models(tenantId);
const associatedCredits = await CreditNote.query().where(
'customerId',
customerId
);
if (associatedCredits.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_LINKED_CREDIT_NOTES);
}
};
}

View File

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

View File

@@ -0,0 +1,73 @@
import Knex from 'knex';
import {
IRefundCreditNoteDeletedPayload,
IRefundCreditNoteDeletingPayload,
IRefundVendorCreditDeletedPayload,
} 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 RefundCreditNote from './RefundCreditNote';
@Service()
export default class DeleteRefundCreditNote extends RefundCreditNote {
@Inject()
tenancy: HasTenancyService;
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* Retrieve the credit note graph.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns
*/
public deleteCreditNoteRefund = async (
tenantId: number,
refundCreditId: number
) => {
const { RefundCreditNote } = this.tenancy.models(tenantId);
// Retrieve the old credit note or throw not found service error.
const oldRefundCredit = await this.getCreditNoteRefundOrThrowError(
tenantId,
refundCreditId
);
// Triggers `onCreditNoteRefundDeleted` event.
await this.eventPublisher.emitAsync(events.creditNote.onRefundDelete, {
refundCreditId,
oldRefundCredit,
tenantId,
} as IRefundCreditNoteDeletedPayload);
// Deletes refund credit note transactions with associated entries.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
const eventPayload = {
trx,
refundCreditId,
oldRefundCredit,
tenantId,
} as IRefundCreditNoteDeletedPayload | IRefundCreditNoteDeletingPayload;
// Triggers `onCreditNoteRefundDeleting` event.
await this.eventPublisher.emitAsync(
events.creditNote.onRefundDeleting,
eventPayload
);
// Deletes the refund credit note graph from the storage.
await RefundCreditNote.query(trx).findById(refundCreditId).delete();
// Triggers `onCreditNoteRefundDeleted` event.
await this.eventPublisher.emitAsync(
events.creditNote.onRefundDeleted,
eventPayload as IRefundVendorCreditDeletedPayload
);
});
};
}

View File

@@ -0,0 +1,100 @@
import {
ICreditNoteEditDTO,
ICreditNoteEditedPayload,
ICreditNoteEditingPayload,
} from '@/interfaces';
import { Knex } from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { Inject, Service } from 'typedi';
import BaseCreditNotes from './CreditNotes';
@Service()
export default class EditCreditNote extends BaseCreditNotes {
@Inject()
itemsEntriesService: ItemsEntriesService;
@Inject()
eventPublisher: EventPublisher;
@Inject()
uow: UnitOfWork;
/**
* Edits the given credit note.
* @param {number} tenantId -
* @param {ICreditNoteEditDTO} creditNoteEditDTO -
*/
public editCreditNote = async (
tenantId: number,
creditNoteId: number,
creditNoteEditDTO: ICreditNoteEditDTO
) => {
const { CreditNote, Contact } = this.tenancy.models(tenantId);
// Retrieve the sale invoice or throw not found service error.
const oldCreditNote = await this.getCreditNoteOrThrowError(
tenantId,
creditNoteId
);
// Validate customer existance.
const customer = await Contact.query()
.modify('customer')
.findById(creditNoteEditDTO.customerId)
.throwIfNotFound();
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
creditNoteEditDTO.entries
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
creditNoteEditDTO.entries
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
creditNoteId,
'CreditNote',
creditNoteEditDTO.entries
);
// Transformes the given DTO to storage layer data.
const creditNoteModel = this.transformCreateEditDTOToModel(
tenantId,
creditNoteEditDTO,
customer.currencyCode,
oldCreditNote
);
// Sales the credit note transactions with associated entries.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteEditing` event.
await this.eventPublisher.emitAsync(events.creditNote.onEditing, {
creditNoteEditDTO,
oldCreditNote,
trx,
tenantId,
} as ICreditNoteEditingPayload);
// Saves the credit note graph to the storage.
const creditNote = await CreditNote.query(trx).upsertGraph({
id: creditNoteId,
...creditNoteModel,
});
// Triggers `onCreditNoteEdited` event.
await this.eventPublisher.emitAsync(events.creditNote.onEdited, {
trx,
oldCreditNote,
creditNoteId,
creditNote,
creditNoteEditDTO,
tenantId,
} as ICreditNoteEditedPayload);
return creditNote;
});
};
}

View File

@@ -0,0 +1,43 @@
import { ServiceError } from '@/exceptions';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ERRORS } from './constants';
import BaseCreditNotes from './CreditNotes';
import { CreditNoteTransformer } from './CreditNoteTransformer';
@Service()
export default class GetCreditNote extends BaseCreditNotes {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the credit note graph.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns
*/
public getCreditNote = async (tenantId: number, creditNoteId: number) => {
const { CreditNote } = this.tenancy.models(tenantId);
// Retrieve the vendor credit model graph.
const creditNote = await CreditNote.query()
.findById(creditNoteId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('branch');
if (!creditNote) {
throw new ServiceError(ERRORS.CREDIT_NOTE_NOT_FOUND);
}
// Transforms the credit note model to POJO.
return this.transformer.transform(
tenantId,
creditNote,
new CreditNoteTransformer(),
);
};
}

View File

@@ -0,0 +1,41 @@
import { Inject, Service } from 'typedi';
import { ISaleInvoice } from '@/interfaces';
import BaseCreditNotes from './CreditNotes';
import { CreditNoteAppliedInvoiceTransformer } from './CreditNoteAppliedInvoiceTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class GetCreditNoteAssociatedAppliedInvoices extends BaseCreditNotes {
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve credit note associated invoices to apply.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<ISaleInvoice[]>}
*/
public getCreditAssociatedAppliedInvoices = async (
tenantId: number,
creditNoteId: number
): Promise<ISaleInvoice[]> => {
const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId);
// Retireve credit note or throw not found service error.
const creditNote = await this.getCreditNoteOrThrowError(
tenantId,
creditNoteId
);
const appliedToInvoices = await CreditNoteAppliedInvoice.query()
.where('credit_note_id', creditNoteId)
.withGraphFetched('saleInvoice')
.withGraphFetched('creditNote');
// Transformes credit note applied to invoices.
return this.transformer.transform(
tenantId,
appliedToInvoices,
new CreditNoteAppliedInvoiceTransformer()
);
};
}

View File

@@ -0,0 +1,42 @@
import { Service, Inject } from 'typedi';
import BaseCreditNotes from './CreditNotes';
import { ISaleInvoice } from '@/interfaces';
import { CreditNoteWithInvoicesToApplyTransformer } from './CreditNoteWithInvoicesToApplyTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class GetCreditNoteAssociatedInvoicesToApply extends BaseCreditNotes {
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve credit note associated invoices to apply.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<ISaleInvoice[]>}
*/
public getCreditAssociatedInvoicesToApply = async (
tenantId: number,
creditNoteId: number
): Promise<ISaleInvoice[]> => {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retireve credit note or throw not found service error.
const creditNote = await this.getCreditNoteOrThrowError(
tenantId,
creditNoteId
);
// Retrieves the published due invoices that associated to the given customer.
const saleInvoices = await SaleInvoice.query()
.where('customerId', creditNote.customerId)
.modify('dueInvoices')
.modify('published');
// Transformes the sale invoices models to POJO.
return this.transformer.transform(
tenantId,
saleInvoices,
new CreditNoteWithInvoicesToApplyTransformer()
);
};
}

View File

@@ -0,0 +1,37 @@
import { Inject, Service } from 'typedi';
import PdfService from '@/services/PDF/PdfService';
import { templateRender } from 'utils';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Tenant } from '@/system/models';
@Service()
export default class GetCreditNotePdf {
@Inject()
pdfService: PdfService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async getCreditNotePdf(tenantId: number, creditNote) {
const i18n = this.tenancy.i18n(tenantId);
const organization = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const htmlContent = templateRender('modules/credit-note-standard', {
organization,
organizationName: organization.metadata.name,
organizationEmail: organization.metadata.email,
creditNote,
...i18n,
});
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
return pdfContent;
}
}

View File

@@ -0,0 +1,37 @@
import { Inject, Service } from 'typedi';
import { IRefundCreditNote } from '@/interfaces';
import RefundCreditNote from './RefundCreditNote';
import RefundCreditNoteTransformer from './RefundCreditNoteTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class getRefundCreditNoteTransaction extends RefundCreditNote {
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve credit note associated invoices to apply.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<ISaleInvoice[]>}
*/
public getRefundCreditTransaction = async (
tenantId: number,
refundCreditId: number
): Promise<IRefundCreditNote> => {
const { RefundCreditNote } = this.tenancy.models(tenantId);
await this.getCreditNoteRefundOrThrowError(tenantId, refundCreditId);
const refundCreditNote = await RefundCreditNote.query()
.findById(refundCreditId)
.withGraphFetched('fromAccount')
.withGraphFetched('creditNote');
return this.transformer.transform(
tenantId,
refundCreditNote,
new RefundCreditNoteTransformer()
);
};
}

View File

@@ -0,0 +1,41 @@
import { IRefundCreditNotePOJO } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import BaseCreditNotes from './CreditNotes';
import RefundCreditNoteTransformer from './RefundCreditNoteTransformer';
@Service()
export default class ListCreditNoteRefunds extends BaseCreditNotes {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the credit note graph.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<IRefundCreditNotePOJO[]>}
*/
public getCreditNoteRefunds = async (
tenantId: number,
creditNoteId: number
): Promise<IRefundCreditNotePOJO[]> => {
const { RefundCreditNote } = this.tenancy.models(tenantId);
// Retrieve refund credit notes associated to the given credit note.
const refundCreditTransactions = await RefundCreditNote.query()
.where('creditNoteId', creditNoteId)
.withGraphFetched('creditNote')
.withGraphFetched('fromAccount');
// Transformes refund credit note models to POJO objects.
return this.transformer.transform(
tenantId,
refundCreditTransactions,
new RefundCreditNoteTransformer()
);
};
}

View File

@@ -0,0 +1,67 @@
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import { ICreditNotesQueryDTO } from '@/interfaces';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import BaseCreditNotes from './CreditNotes';
import { CreditNoteTransformer } from './CreditNoteTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class ListCreditNotes extends BaseCreditNotes {
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO = (filterDTO) => {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
};
/**
* Retrieves the paginated and filterable credit notes list.
* @param {number} tenantId -
* @param {ICreditNotesQueryDTO} creditNotesQuery -
*/
public getCreditNotesList = async (
tenantId: number,
creditNotesQuery: ICreditNotesQueryDTO
) => {
const { CreditNote } = this.tenancy.models(tenantId);
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(creditNotesQuery);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
CreditNote,
filter
);
const { results, pagination } = await CreditNote.query()
.onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transforomes the credit notes to POJO.
const creditNotes = await this.transformer.transform(
tenantId,
results,
new CreditNoteTransformer()
);
return {
creditNotes,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
};
}

View File

@@ -0,0 +1,92 @@
import { ServiceError } from '@/exceptions';
import {
ICreditNote,
ICreditNoteOpenedPayload,
ICreditNoteOpeningPayload,
} from '@/interfaces';
import { Knex } from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { Inject, Service } from 'typedi';
import BaseCreditNotes from './CreditNotes';
import { ERRORS } from './constants';
@Service()
export default class OpenCreditNote extends BaseCreditNotes {
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Opens the given credit note.
* @param {number} tenantId -
* @param {ICreditNoteEditDTO} creditNoteEditDTO -
* @returns {Promise<ICreditNote>}
*/
public openCreditNote = async (
tenantId: number,
creditNoteId: number
): Promise<ICreditNote> => {
const { CreditNote } = this.tenancy.models(tenantId);
// Retrieve the sale invoice or throw not found service error.
const oldCreditNote = await this.getCreditNoteOrThrowError(
tenantId,
creditNoteId
);
// Throw service error if the credit note is already open.
this.throwErrorIfAlreadyOpen(oldCreditNote);
// Triggers `onCreditNoteOpen` event.
this.eventPublisher.emitAsync(events.creditNote.onOpen, {
tenantId,
creditNoteId,
oldCreditNote,
});
// Sales the credit note transactions with associated entries.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
const eventPayload = {
tenantId,
creditNoteId,
oldCreditNote,
trx,
} as ICreditNoteOpeningPayload;
// Triggers `onCreditNoteOpening` event.
await this.eventPublisher.emitAsync(
events.creditNote.onOpening,
eventPayload
);
// Saves the credit note graph to the storage.
const creditNote = await CreditNote.query(trx)
.findById(creditNoteId)
.update({
openedAt: new Date(),
});
// Triggers `onCreditNoteOpened` event.
await this.eventPublisher.emitAsync(events.creditNote.onOpened, {
...eventPayload,
creditNote,
} as ICreditNoteOpenedPayload);
return creditNote;
});
};
/**
*
* @param creditNote
*/
public throwErrorIfAlreadyOpen = (creditNote) => {
if (creditNote.openedAt) {
throw new ServiceError(ERRORS.CREDIT_NOTE_ALREADY_OPENED);
}
};
}

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import { IAccount, IRefundCreditNote } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import BaseCreditNotes from './CreditNotes';
import { ERRORS } from './constants';
@Service()
export default class RefundCreditNote extends BaseCreditNotes {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the credit note graph.
* @param {number} tenantId
* @param {number} refundCreditId
* @returns {Promise<IRefundCreditNote>}
*/
public getCreditNoteRefundOrThrowError = async (
tenantId: number,
refundCreditId: number
): Promise<IRefundCreditNote> => {
const { RefundCreditNote } = this.tenancy.models(tenantId);
const refundCreditNote = await RefundCreditNote.query().findById(
refundCreditId
);
if (!refundCreditNote) {
throw new ServiceError(ERRORS.REFUND_CREDIT_NOTE_NOT_FOUND);
}
return refundCreditNote;
};
/**
* Validate the refund account type.
* @param {IAccount} account
*/
public validateRefundWithdrawwalAccountType = (account: IAccount): void => {
const supportedTypes = ['bank', 'cash', 'fixed-asset'];
if (supportedTypes.indexOf(account.accountType) === -1) {
throw new ServiceError(ERRORS.ACCOUNT_INVALID_TYPE);
}
};
}

View File

@@ -0,0 +1,160 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { AccountNormal, ILedgerEntry, IRefundCreditNote } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export default class RefundCreditNoteGLEntries {
@Inject()
ledgerStorage: LedgerStorageService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieves the refund credit common GL entry.
* @param {IRefundCreditNote} refundCreditNote
* @returns
*/
private getRefundCreditCommonGLEntry = (
refundCreditNote: IRefundCreditNote
) => {
return {
currencyCode: refundCreditNote.currencyCode,
exchangeRate: refundCreditNote.exchangeRate,
transactionType: 'RefundCreditNote',
transactionId: refundCreditNote.id,
date: refundCreditNote.date,
userId: refundCreditNote.userId,
referenceNumber: refundCreditNote.referenceNo,
createdAt: refundCreditNote.createdAt,
indexGroup: 10,
credit: 0,
debit: 0,
note: refundCreditNote.description,
branchId: refundCreditNote.branchId,
};
};
/**
* Retrieves the refudn credit receivable GL entry.
* @param {IRefundCreditNote} refundCreditNote
* @param {number} ARAccountId
* @returns {ILedgerEntry}
*/
private getRefundCreditGLReceivableEntry = (
refundCreditNote: IRefundCreditNote,
ARAccountId: number
): ILedgerEntry => {
const commonEntry = this.getRefundCreditCommonGLEntry(refundCreditNote);
return {
...commonEntry,
debit: refundCreditNote.amount,
accountId: ARAccountId,
contactId: refundCreditNote.creditNote.customerId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the refund credit withdrawal GL entry.
* @param {number} refundCreditNote
* @returns {ILedgerEntry}
*/
private getRefundCreditGLWithdrawalEntry = (
refundCreditNote: IRefundCreditNote
): ILedgerEntry => {
const commonEntry = this.getRefundCreditCommonGLEntry(refundCreditNote);
return {
...commonEntry,
credit: refundCreditNote.amount,
accountId: refundCreditNote.fromAccountId,
index: 2,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieve the refund credit note GL entries.
* @param {IRefundCreditNote} refundCreditNote
* @param {number} receivableAccount
* @returns {ILedgerEntry[]}
*/
public getRefundCreditGLEntries(
refundCreditNote: IRefundCreditNote,
ARAccountId: number
): ILedgerEntry[] {
const receivableEntry = this.getRefundCreditGLReceivableEntry(
refundCreditNote,
ARAccountId
);
const withdrawalEntry =
this.getRefundCreditGLWithdrawalEntry(refundCreditNote);
return [receivableEntry, withdrawalEntry];
}
/**
* Creates refund credit GL entries.
* @param {number} tenantId
* @param {IRefundCreditNote} refundCreditNote
* @param {Knex.Transaction} trx
*/
public createRefundCreditGLEntries = async (
tenantId: number,
refundCreditNoteId: number,
trx?: Knex.Transaction
) => {
const { Account, RefundCreditNote } = this.tenancy.models(tenantId);
// Retrieve the refund with associated credit note.
const refundCreditNote = await RefundCreditNote.query(trx)
.findById(refundCreditNoteId)
.withGraphFetched('creditNote');
// Receivable account A/R.
const receivableAccount = await Account.query().findOne(
'slug',
'accounts-receivable'
);
// Retrieve refund credit GL entries.
const refundGLEntries = this.getRefundCreditGLEntries(
refundCreditNote,
receivableAccount.id
);
const ledger = new Ledger(refundGLEntries);
// Saves refund ledger entries.
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
/**
* Reverts refund credit note GL entries.
* @param {number} tenantId
* @param {number} refundCreditNoteId
* @param {number} receivableAccount
* @param {Knex.Transaction} trx
*/
public revertRefundCreditGLEntries = async (
tenantId: number,
refundCreditNoteId: number,
trx?: Knex.Transaction
) => {
await this.ledgerStorage.deleteByReference(
tenantId,
refundCreditNoteId,
'RefundCreditNote',
trx
);
};
}

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
import Knex from 'knex';
import { IRefundCreditNote } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
@Service()
export default class RefundSyncCreditNoteBalance {
@Inject()
tenancy: HasTenancyService;
/**
*
* @param {number} tenantId
* @param {IRefundCreditNote} refundCreditNote
* @param {Knex.Transaction} trx
*/
public incrementCreditNoteRefundAmount = async (
tenantId: number,
creditNoteId: number,
amount: number,
trx?: Knex.Transaction
): Promise<void> => {
const { CreditNote } = this.tenancy.models(tenantId);
await CreditNote.query(trx)
.findById(creditNoteId)
.increment('refunded_amount', amount);
};
/**
*
* @param {number} tenantId
* @param {IRefundCreditNote} refundCreditNote
* @param {Knex.Transaction} trx
*/
public decrementCreditNoteRefundAmount = async (
tenantId: number,
creditNoteId: number,
amount: number,
trx?: Knex.Transaction
): Promise<void> => {
const { CreditNote } = this.tenancy.models(tenantId);
await CreditNote.query(trx)
.findById(creditNoteId)
.decrement('refunded_amount', amount);
};
}

View File

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

View File

@@ -0,0 +1,68 @@
export const ERRORS = {
CREDIT_NOTE_NOT_FOUND: 'CREDIT_NOTE_NOT_FOUND',
REFUND_CREDIT_NOTE_NOT_FOUND: 'REFUND_CREDIT_NOTE_NOT_FOUND',
CREDIT_NOTE_ALREADY_OPENED: 'CREDIT_NOTE_ALREADY_OPENED',
ACCOUNT_INVALID_TYPE: 'ACCOUNT_INVALID_TYPE',
CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT: 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT',
INVOICES_HAS_NO_REMAINING_AMOUNT: 'INVOICES_HAS_NO_REMAINING_AMOUNT',
CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND:
'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND',
CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS',
CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES',
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES'
};
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'credit_note.view.draft',
slug: 'draft',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'credit_note.view.published',
slug: 'published',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'published',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'credit_note.view.open',
slug: 'open',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'open',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'credit_note.view.closed',
slug: 'closed',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'closed',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
];