fix(server): pull-request nodes

This commit is contained in:
Ahmed Bouhuolia
2023-09-22 15:23:33 +02:00
parent eaf72d1608
commit ce41845bd7
23 changed files with 153 additions and 215 deletions

View File

@@ -40,7 +40,7 @@ export default class SalesTaxLiabilitySummary extends BaseFinancialReportControl
}
/*
*
* Retrieves the sales tax liability summary.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
@@ -50,7 +50,7 @@ export default class SalesTaxLiabilitySummary extends BaseFinancialReportControl
res: Response,
next: NextFunction
) {
const { tenantId, settings } = req;
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {

View File

@@ -184,8 +184,15 @@ export default class SaleInvoicesController extends BaseController {
.optional({ nullable: true })
.trim()
.escape(),
check('entries.*.tax_code').optional({ nullable: true }).trim().escape(),
check('entries.*.tax_rate').optional().isNumeric().toFloat(),
check('entries.*.tax_code')
.optional({ nullable: true })
.trim()
.escape()
.isString(),
check('entries.*.tax_rate_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()

View File

@@ -4,9 +4,10 @@ import { body, param } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import { TaxRatesApplication } from '@/services/TaxRates/TaxRatesApplication';
import { HookNextFunction } from 'mongoose';
import CheckAbilities from '@/api/middleware/CheckPolicies';
import { ServiceError } from '@/exceptions';
import { ERRORS } from '@/services/TaxRates/constants';
import { AbilitySubject, TaxRateAction } from '@/interfaces';
@Service()
export class TaxRatesController extends BaseController {
@@ -21,6 +22,7 @@ export class TaxRatesController extends BaseController {
router.post(
'/',
CheckAbilities(TaxRateAction.CREATE, AbilitySubject.TaxRate),
this.taxRateValidationSchema,
this.validationResult,
asyncMiddleware(this.createTaxRate.bind(this)),
@@ -28,6 +30,7 @@ export class TaxRatesController extends BaseController {
);
router.post(
'/:id',
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
[param('id').exists().toInt(), ...this.taxRateValidationSchema],
this.validationResult,
asyncMiddleware(this.editTaxRate.bind(this)),
@@ -35,6 +38,7 @@ export class TaxRatesController extends BaseController {
);
router.post(
'/:id/active',
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
[param('id').exists().toInt()],
this.validationResult,
asyncMiddleware(this.activateTaxRate.bind(this)),
@@ -42,6 +46,7 @@ export class TaxRatesController extends BaseController {
);
router.post(
'/:id/inactive',
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
[param('id').exists().toInt()],
this.validationResult,
asyncMiddleware(this.inactivateTaxRate.bind(this)),
@@ -49,6 +54,7 @@ export class TaxRatesController extends BaseController {
);
router.delete(
'/:id',
CheckAbilities(TaxRateAction.DELETE, AbilitySubject.TaxRate),
[param('id').exists().toInt()],
this.validationResult,
asyncMiddleware(this.deleteTaxRate.bind(this)),
@@ -56,6 +62,7 @@ export class TaxRatesController extends BaseController {
);
router.get(
'/:id',
CheckAbilities(TaxRateAction.VIEW, AbilitySubject.TaxRate),
[param('id').exists().toInt()],
this.validationResult,
asyncMiddleware(this.getTaxRate.bind(this)),
@@ -63,6 +70,7 @@ export class TaxRatesController extends BaseController {
);
router.get(
'/',
CheckAbilities(TaxRateAction.VIEW, AbilitySubject.TaxRate),
this.validationResult,
asyncMiddleware(this.getTaxRates.bind(this)),
this.handleServiceErrors
@@ -81,7 +89,7 @@ export class TaxRatesController extends BaseController {
body('description').optional().trim().isString(),
body('is_non_recoverable').optional().isBoolean().default(false),
body('is_compound').optional().isBoolean().default(false),
body('status').optional().toUpperCase().isIn(['ARCHIVED', 'ACTIVE']),
body('active').optional().isBoolean().default(false),
];
}
@@ -242,12 +250,7 @@ export class TaxRatesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
private handleServiceErrors(
error: Error,
req: Request,
res: Response,
next: HookNextFunction
) {
private handleServiceErrors(error: Error, req: Request, res: Response, next) {
if (error instanceof ServiceError) {
if (error.errorType === ERRORS.TAX_CODE_NOT_UNIQUE) {
return res.boom.badRequest(null, {

View File

@@ -19,8 +19,6 @@ exports.up = (knex) => {
.unsigned()
.references('id')
.inTable('tax_rates');
table.string('tax_code');
table.decimal('tax_rate');
})
.table('sales_invoices', (table) => {
table.boolean('is_inclusive_tax').defaultTo(false);
@@ -35,7 +33,7 @@ exports.up = (knex) => {
.inTable('tax_rates');
table.string('reference_type');
table.integer('reference_id');
table.decimal('tax_amount');
table.decimal('rate').unsigned();
table.integer('tax_account_id').unsigned();
})
.table('accounts_transactions', (table) => {

View File

@@ -1,7 +0,0 @@
exports.up = function (knex) {
return knex.table('sales_invoices', (table) => {
table.renameColumn('balance', 'amount');
});
};
exports.down = function (knex) {};

View File

@@ -153,3 +153,11 @@ export enum AccountAction {
VIEW = 'View',
TransactionsLocking = 'TransactionsLocking',
}
export enum TaxRateAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
}

View File

@@ -54,14 +54,6 @@ export interface IItemDTO {
sellDescription: string;
purchaseDescription: string;
// Used as an override if the default Tax Code for the selected `costAccountId` is not correct
purchaseTaxCode: string;
purchaseTaxId: string;
// Used as an override if the default Tax Code for the selected `sellAccountId` is not correct
saleTaxCode: string;
saleTaxId: string;
quantityOnHand: number;
note: string;

View File

@@ -38,7 +38,6 @@ export interface IItemEntry {
projectRefInvoicedAmount?: number;
taxRateId: number | null;
taxCode: string;
taxRate: number;
taxAmount: number;
@@ -57,9 +56,8 @@ export interface IItemEntryDTO {
projectRefType?: ProjectLinkRefType;
projectRefInvoicedAmount?: number;
taxCodeId?: number;
taxRateId?: number;
taxCode?: string;
taxRate?: number;
}
export enum ProjectLinkRefType {

View File

@@ -76,6 +76,13 @@ export interface ITaxTransaction {
taxRateId: number;
referenceType: string;
referenceId: number;
taxAmount: number;
rate: number;
taxAccountId: number;
}
export enum TaxRateAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
}

View File

@@ -80,8 +80,6 @@ import { ProjectBillableExpensesSubscriber } from '@/services/Projects/Projects/
import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber';
import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber';
import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber';
import { SaleEstimateTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleEstimateTaxRateValidateSubscriber';
import { SaleReceiptTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleReceiptTaxRateValidateSubscriber';
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
export default () => {
@@ -192,8 +190,6 @@ export const susbcribers = () => {
// Tax Rates
SaleInvoiceTaxRateValidateSubscriber,
SaleEstimateTaxRateValidateSubscriber,
SaleReceiptTaxRateValidateSubscriber,
WriteInvoiceTaxTransactionsSubscriber
WriteInvoiceTaxTransactionsSubscriber,
];
};

View File

@@ -29,30 +29,14 @@ export default class AccountTransaction extends TenantModel {
* Virtual attributes.
*/
static get virtualAttributes() {
return ['referenceTypeFormatted'];
}
/**
* Retrieves the credit amount in foreign currency.
* @return {number}
*/
get creditFcy() {
return this.credit;
}
/**
* Retrieves the debit amount in foreign currency.
* @return {number}
*/
get debitFcy() {
return this.debit;
return ['referenceTypeFormatted', 'creditLocal', 'debitLocal'];
}
/**
* Retrieves the credit amount in base currency.
* @return {number}
*/
get creditBcy() {
get creditLocal() {
return this.credit * this.exchangeRate;
}
@@ -60,26 +44,10 @@ export default class AccountTransaction extends TenantModel {
* Retrieves the debit amount in base currency.
* @return {number}
*/
get debitBcy() {
get debitLocal() {
return this.debit * this.exchangeRate;
}
/**
* Retrieves the tax amount in foreign currency.
* @return {number}
*/
get taxAmountFcy() {
return (this.creditFcy - this.debitFcy) * this.taxRate;
}
/**
* Retrieves the tax amount in base currency.
* @return {number}
*/
get taxAmountBcy() {
return (this.creditBcy - this.debitBcy) * this.taxRate;
}
/**
* Retrieve formatted reference type.
* @return {string}

View File

@@ -6,15 +6,11 @@ import {
SalesTaxLiabilitySummaryTotal,
} from '@/interfaces/SalesTaxLiabilitySummary';
import { tableRowMapper } from '@/utils';
import { ITableColumn, ITableColumnAccessor, ITableRow } from '@/interfaces';
import { ITableColumn, ITableRow } from '@/interfaces';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { FinancialTable } from '../FinancialTable';
import AgingReport from '../AgingSummary/AgingReport';
enum IROW_TYPE {
TaxRate = 'TaxRate',
Total = 'Total',
}
import { IROW_TYPE } from './_constants';
export class SalesTaxLiabilitySummaryTable extends R.compose(
FinancialSheetStructure,

View File

@@ -0,0 +1,4 @@
export enum IROW_TYPE {
TaxRate = 'TaxRate',
Total = 'Total',
}

View File

@@ -70,20 +70,25 @@ export class CommandSaleInvoiceDTOTransformer {
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
...entry,
}));
const entries = await composeAsync(
const asyncEntries = await composeAsync(
// Associate tax rate id from tax code to entries.
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries(tenantId),
// Sets default cost and sell account to invoice items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
)(initialEntries);
const entries = R.compose(
// Remove tax code from entries.
R.map(R.omit(['taxCode']))
)(asyncEntries);
const initialDTO = {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
['invoiceDate', 'dueDate']
),
// Avoid rewrite the deliver date in edit mode when already published.
amount,
balance: amount,
currencyCode: customer.currencyCode,
exchangeRate: saleInvoiceDTO.exchangeRate || 1,
...(saleInvoiceDTO.delivered &&

View File

@@ -1,12 +1,12 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import {
ITaxRateActivatedPayload,
ITaxRateActivatingPayload,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '../Tenancy/TenancyService';
import { Knex } from 'knex';
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
import events from '@/subscribers/events';
@@ -25,7 +25,7 @@ export class ActivateTaxRateService {
private validators: CommandTaxRatesValidators;
/**
* Edits the given tax rate.
* Activates the given tax rate.
* @param {number} tenantId
* @param {number} taxRateId
* @param {IEditTaxRateDTO} taxRateEditDTO

View File

@@ -59,6 +59,7 @@ export class CommandTaxRatesValidators {
* Validates the tax codes of the given item entries DTO.
* @param {number} tenantId
* @param {IItemEntryDTO[]} itemEntriesDTO
* @throws {ServiceError}
*/
public async validateItemEntriesTaxCode(
tenantId: number,
@@ -92,17 +93,17 @@ export class CommandTaxRatesValidators {
tenantId: number,
itemEntriesDTO: IItemEntryDTO[]
) {
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCodeId);
const taxCodes = filteredTaxEntries.map((e) => e.taxCodeId);
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxRateId);
const taxRatesIds = filteredTaxEntries.map((e) => e.taxRateId);
// Can't validate if there is no tax codes.
if (taxCodes.length === 0) return;
if (taxRatesIds.length === 0) return;
const { TaxRate } = this.tenancy.models(tenantId);
const foundTaxCodes = await TaxRate.query().whereIn('id', taxCodes);
const foundCodes = foundTaxCodes.map((tax) => tax.id);
const foundTaxCodes = await TaxRate.query().whereIn('id', taxRatesIds);
const foundTaxRatesIds = foundTaxCodes.map((tax) => tax.id);
const notFoundTaxCodes = difference(taxCodes, foundCodes);
const notFoundTaxCodes = difference(taxRatesIds, foundTaxRatesIds);
if (notFoundTaxCodes.length > 0) {
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND);

View File

@@ -49,7 +49,9 @@ export class CreateTaxRate {
trx,
} as ITaxRateCreatingPayload);
const taxRate = await TaxRate.query(trx).insert({ ...createTaxRateDTO });
const taxRate = await TaxRate.query(trx).insertAndFetch({
...createTaxRateDTO,
});
// Triggers `onTaxRateCreated` event.
await this.eventPublisher.emitAsync(events.taxRates.onCreated, {

View File

@@ -2,6 +2,7 @@ import { sumBy, chain, keyBy } from 'lodash';
import { IItemEntry, ITaxTransaction } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
@Service()
export class WriteTaxTransactionsItemEntries {
@@ -15,24 +16,51 @@ export class WriteTaxTransactionsItemEntries {
*/
public async writeTaxTransactionsFromItemEntries(
tenantId: number,
itemEntries: IItemEntry[]
itemEntries: IItemEntry[],
trx?: Knex.Transaction
) {
const { TaxRateTransaction, TaxRate } = this.tenancy.models(tenantId);
const aggregatedEntries = this.aggregateItemEntriesByTaxCode(itemEntries);
const entriesTaxRateIds = aggregatedEntries.map((entry) => entry.taxRateId);
const taxRates = await TaxRate.query().whereIn('id', entriesTaxRateIds);
const taxRates = await TaxRate.query(trx).whereIn('id', entriesTaxRateIds);
const taxRatesById = keyBy(taxRates, 'id');
const taxTransactions = aggregatedEntries.map((entry) => ({
taxRateId: entry.taxRateId,
referenceType: entry.referenceType,
referenceId: entry.referenceId,
taxAmount: entry.taxRate || taxRatesById[entry.taxRateId]?.rate,
rate: entry.taxRate || taxRatesById[entry.taxRateId]?.rate,
})) as ITaxTransaction[];
await TaxRateTransaction.query().upsertGraph(taxTransactions);
await TaxRateTransaction.query(trx).upsertGraph(taxTransactions);
}
/**
* Rewrites the tax rate transactions from the given item entries.
* @param {number} tenantId
* @param {IItemEntry[]} itemEntries
* @param {string} referenceType
* @param {number} referenceId
* @param {Knex.Transaction} trx
*/
public async rewriteTaxRateTransactionsFromItemEntries(
tenantId: number,
itemEntries: IItemEntry[],
referenceType: string,
referenceId: number,
trx?: Knex.Transaction
) {
await Promise.all([
this.removeTaxTransactionsFromItemEntries(
tenantId,
referenceId,
referenceType,
trx
),
this.writeTaxTransactionsFromItemEntries(tenantId, itemEntries, trx),
]);
}
/**
@@ -59,11 +87,12 @@ export class WriteTaxTransactionsItemEntries {
public async removeTaxTransactionsFromItemEntries(
tenantId: number,
referenceId: number,
referenceType: string
referenceType: string,
trx?: Knex.Transaction
) {
const { TaxRateTransaction } = this.tenancy.models(tenantId);
await TaxRateTransaction.query()
await TaxRateTransaction.query(trx)
.where({ referenceType, referenceId })
.delete();
}

View File

@@ -1,58 +0,0 @@
import { Inject, Service } from 'typedi';
import {
ISaleEstimateCreatingPayload,
ISaleEstimateEditingPayload,
ISaleInvoiceCreatingPaylaod,
ISaleInvoiceEditingPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
@Service()
export class SaleEstimateTaxRateValidateSubscriber {
@Inject()
private taxRateDTOValidator: CommandTaxRatesValidators;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.saleEstimate.onCreating,
this.validateSaleEstimateEntriesTaxCodeExistanceOnCreating
);
bus.subscribe(
events.saleEstimate.onEditing,
this.validateSaleEstimateEntriesTaxCodeExistanceOnEditing
);
return bus;
}
/**
* Validate invoice entries tax rate code existance.
* @param {ISaleInvoiceCreatingPaylaod}
*/
private validateSaleEstimateEntriesTaxCodeExistanceOnCreating = async ({
estimateDTO,
tenantId,
}: ISaleEstimateCreatingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
tenantId,
estimateDTO.entries
);
};
/**
*
* @param {ISaleInvoiceEditingPayload}
*/
private validateSaleEstimateEntriesTaxCodeExistanceOnEditing = async ({
tenantId,
estimateDTO,
}: ISaleEstimateEditingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
tenantId,
estimateDTO.entries
);
};
}

View File

@@ -27,6 +27,10 @@ export class SaleInvoiceTaxRateValidateSubscriber {
events.saleInvoice.onEditing,
this.validateSaleInvoiceEntriesTaxCodeExistanceOnEditing
);
bus.subscribe(
events.saleInvoice.onEditing,
this.validateSaleInvoiceEntriesTaxIdExistanceOnEditing
);
return bus;
}
@@ -71,4 +75,18 @@ export class SaleInvoiceTaxRateValidateSubscriber {
saleInvoiceDTO.entries
);
};
/**
* Validates the invoice entries tax rate id existance when editing.
* @param {ISaleInvoiceEditingPayload} payload -
*/
private validateSaleInvoiceEntriesTaxIdExistanceOnEditing = async ({
tenantId,
saleInvoiceDTO,
}: ISaleInvoiceEditingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
tenantId,
saleInvoiceDTO.entries
);
};
}

View File

@@ -1,56 +0,0 @@
import { Inject, Service } from 'typedi';
import {
ISaleReceiptCreatingPayload,
ISaleReceiptEditingPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
@Service()
export class SaleReceiptTaxRateValidateSubscriber {
@Inject()
private taxRateDTOValidator: CommandTaxRatesValidators;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.saleReceipt.onCreating,
this.validateSaleReceiptEntriesTaxCodeExistanceOnCreating
);
bus.subscribe(
events.saleReceipt.onEditing,
this.validateSaleReceiptEntriesTaxCodeExistanceOnEditing
);
return bus;
}
/**
* Validate receipt entries tax rate code existance.
* @param {ISaleInvoiceCreatingPaylaod}
*/
private validateSaleReceiptEntriesTaxCodeExistanceOnCreating = async ({
tenantId,
saleReceiptDTO,
}: ISaleReceiptCreatingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
tenantId,
saleReceiptDTO.entries
);
};
/**
*
* @param {ISaleInvoiceEditingPayload}
*/
private validateSaleReceiptEntriesTaxCodeExistanceOnEditing = async ({
tenantId,
saleReceiptDTO,
}: ISaleReceiptEditingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
tenantId,
saleReceiptDTO.entries
);
};
}

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { WriteTaxTransactionsItemEntries } from '../WriteTaxTransactionsItemEntries';
@@ -19,6 +20,10 @@ export class WriteInvoiceTaxTransactionsSubscriber {
events.saleInvoice.onCreated,
this.writeInvoiceTaxTransactionsOnCreated
);
bus.subscribe(
events.saleInvoice.onEdited,
this.rewriteInvoiceTaxTransactionsOnEdited
);
bus.subscribe(
events.saleInvoice.onDelete,
this.removeInvoiceTaxTransactionsOnDeleted
@@ -27,16 +32,36 @@ export class WriteInvoiceTaxTransactionsSubscriber {
}
/**
* Validate receipt entries tax rate code existance.
* Writes the invoice tax transactions on invoice created.
* @param {ISaleInvoiceCreatingPaylaod}
*/
private writeInvoiceTaxTransactionsOnCreated = async ({
tenantId,
saleInvoice,
trx
}: ISaleInvoiceCreatedPayload) => {
await this.writeTaxTransactions.writeTaxTransactionsFromItemEntries(
tenantId,
saleInvoice.entries
saleInvoice.entries,
trx
);
};
/**
* Rewrites the invoice tax transactions on invoice edited.
* @param {ISaleInvoiceEditedPayload} payload -
*/
private rewriteInvoiceTaxTransactionsOnEdited = async ({
tenantId,
saleInvoice,
trx,
}: ISaleInvoiceEditedPayload) => {
await this.writeTaxTransactions.rewriteTaxRateTransactionsFromItemEntries(
tenantId,
saleInvoice.entries,
'SaleInvoice',
saleInvoice.id,
trx
);
};
@@ -47,11 +72,13 @@ export class WriteInvoiceTaxTransactionsSubscriber {
private removeInvoiceTaxTransactionsOnDeleted = async ({
tenantId,
oldSaleInvoice,
trx
}: ISaleInvoiceDeletedPayload) => {
await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries(
tenantId,
oldSaleInvoice.id,
'SaleInvoice'
'SaleInvoice',
trx
);
};
}