From a7644e64811ef15cfcf11840dcbfb55568d3ead2 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 11 Aug 2023 21:08:30 +0200 Subject: [PATCH] feat: tax rates on sale invoice service --- .../api/controllers/Sales/SalesInvoices.ts | 10 +++- .../20230810191606_create_tax_rates.js | 31 ++++++---- packages/server/src/interfaces/ItemEntry.ts | 6 ++ packages/server/src/interfaces/SaleInvoice.ts | 1 + packages/server/src/loaders/eventEmitter.ts | 2 + .../TaxRates/CommandTaxRatesValidators.ts | 26 ++++++++- .../server/src/services/TaxRates/constants.ts | 1 + .../SaleEstimateTaxRateValidateSubscriber.ts | 58 +++++++++++++++++++ .../SaleInvoiceTaxRateValidateSubscriber.ts | 56 ++++++++++++++++++ .../SaleReceiptTaxRateValidateSubscriber.ts | 56 ++++++++++++++++++ 10 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 packages/server/src/services/TaxRates/subscribers/SaleEstimateTaxRateValidateSubscriber.ts create mode 100644 packages/server/src/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber.ts create mode 100644 packages/server/src/services/TaxRates/subscribers/SaleReceiptTaxRateValidateSubscriber.ts diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index c9975b710..044ddc153 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -169,8 +169,9 @@ export default class SaleInvoicesController extends BaseController { check('branch_id').optional({ nullable: true }).isNumeric().toInt(), check('project_id').optional({ nullable: true }).isNumeric().toInt(), - check('entries').exists().isArray({ min: 1 }), + check('is_tax_exclusive').optional().isBoolean().toBoolean(), + check('entries').exists().isArray({ min: 1 }), check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), @@ -183,6 +184,8 @@ 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.*.warehouse_id') .optional({ nullable: true }) .isNumeric() @@ -756,6 +759,11 @@ export default class SaleInvoicesController extends BaseController { ], }); } + if (error.errorType === 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 5000 }], + }); + } } next(error); } diff --git a/packages/server/src/database/migrations/20230810191606_create_tax_rates.js b/packages/server/src/database/migrations/20230810191606_create_tax_rates.js index 802c275e4..63b486d2a 100644 --- a/packages/server/src/database/migrations/20230810191606_create_tax_rates.js +++ b/packages/server/src/database/migrations/20230810191606_create_tax_rates.js @@ -1,14 +1,25 @@ exports.up = (knex) => { - return knex.schema.createTable('tax_rates', (table) => { - table.increments(); - table.string('name'); - table.string('code'); - table.decimal('rate'); - table.boolean('is_non_recoverable'); - table.boolean('is_compound'); - table.integer('status'); - table.timestamps(); - }); + return knex.schema + .createTable('tax_rates', (table) => { + table.increments(); + table.string('name'); + table.string('code'); + table.decimal('rate'); + table.boolean('is_non_recoverable'); + table.boolean('is_compound'); + table.integer('status'); + table.timestamps(); + }) + .table('items_entries', (table) => { + table.boolean(['is_tax_exclusive']); + table.string('tax_code'); + table.decimal('tax_rate'); + table.decimal('tax_amount_withheld') + }) + .table('sales_invoices', (table) => { + table.boolean(['is_tax_exclusive']); + table.decimal('tax_amount_withheld') + }); }; exports.down = (knex) => { diff --git a/packages/server/src/interfaces/ItemEntry.ts b/packages/server/src/interfaces/ItemEntry.ts index 135f52e21..4b905668b 100644 --- a/packages/server/src/interfaces/ItemEntry.ts +++ b/packages/server/src/interfaces/ItemEntry.ts @@ -32,6 +32,9 @@ export interface IItemEntry { projectRefType?: ProjectLinkRefType; projectRefInvoicedAmount?: number; + taxCode: string; + taxRate: number; + item?: IItem; allocatedCostEntries?: IBillLandedCostEntry[]; @@ -46,6 +49,9 @@ export interface IItemEntryDTO { projectRefId?: number; projectRefType?: ProjectLinkRefType; projectRefInvoicedAmount?: number; + + taxCode: string; + taxRate: number; } export enum ProjectLinkRefType { diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 5dc91b062..f2283de43 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -44,6 +44,7 @@ export interface ISaleInvoiceDTO { exchangeRate?: number; invoiceMessage: string; termsConditions: string; + isTaxExclusive: boolean; entries: IItemEntryDTO[]; delivered: boolean; diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index 94ac8adcf..f42144cb0 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -80,6 +80,7 @@ import { ProjectBillableTasksSubscriber } from '@/services/Projects/Projects/Pro import { ProjectBillableExpensesSubscriber } from '@/services/Projects/Projects/ProjectBillableExpenseSubscriber'; import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber'; import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber'; +import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber'; export default () => { return new EventPublisher(); @@ -187,5 +188,6 @@ export const susbcribers = () => { ProjectBillableTasksSubscriber, ProjectBillableExpensesSubscriber, ProjectBillableBillSubscriber, + SaleInvoiceTaxRateValidateSubscriber ]; }; diff --git a/packages/server/src/services/TaxRates/CommandTaxRatesValidators.ts b/packages/server/src/services/TaxRates/CommandTaxRatesValidators.ts index 9d3c07229..dcd444e0a 100644 --- a/packages/server/src/services/TaxRates/CommandTaxRatesValidators.ts +++ b/packages/server/src/services/TaxRates/CommandTaxRatesValidators.ts @@ -1,8 +1,9 @@ import { ServiceError } from '@/exceptions'; import { Inject, Service } from 'typedi'; import HasTenancyService from '../Tenancy/TenancyService'; -import { ITaxRate } from '@/interfaces'; +import { IItemEntryDTO, ITaxRate } from '@/interfaces'; import { ERRORS } from './constants'; +import { difference } from 'lodash'; @Service() export class CommandTaxRatesValidators { @@ -33,4 +34,27 @@ export class CommandTaxRatesValidators { throw new ServiceError(ERRORS.TAX_CODE_NOT_UNIQUE); } } + + /** + * Validates the tax codes of the given item entries DTO. + * @param {number} tenantId + * @param {IItemEntryDTO[]} itemEntriesDTO + */ + public async validateItemEntriesTaxCode( + tenantId: number, + itemEntriesDTO: IItemEntryDTO[] + ) { + const { TaxRate } = this.tenancy.models(tenantId); + const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCode); + const taxCodes = filteredTaxEntries.map((e) => e.taxCode); + + const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes); + const foundCodes = foundTaxCodes.map((tax) => tax.code); + + const notFoundTaxCodes = difference(taxCodes, foundCodes); + + if (notFoundTaxCodes.length > 0) { + throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND); + } + } } diff --git a/packages/server/src/services/TaxRates/constants.ts b/packages/server/src/services/TaxRates/constants.ts index 71cf5e07e..5822d6a5d 100644 --- a/packages/server/src/services/TaxRates/constants.ts +++ b/packages/server/src/services/TaxRates/constants.ts @@ -1,4 +1,5 @@ export const ERRORS = { TAX_RATE_NOT_FOUND: 'TAX_RATE_NOT_FOUND', TAX_CODE_NOT_UNIQUE: 'TAX_CODE_NOT_UNIQUE', + ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', }; diff --git a/packages/server/src/services/TaxRates/subscribers/SaleEstimateTaxRateValidateSubscriber.ts b/packages/server/src/services/TaxRates/subscribers/SaleEstimateTaxRateValidateSubscriber.ts new file mode 100644 index 000000000..e5f04e4a3 --- /dev/null +++ b/packages/server/src/services/TaxRates/subscribers/SaleEstimateTaxRateValidateSubscriber.ts @@ -0,0 +1,58 @@ +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 + ); + }; +} diff --git a/packages/server/src/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber.ts b/packages/server/src/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber.ts new file mode 100644 index 000000000..afe085e2a --- /dev/null +++ b/packages/server/src/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber.ts @@ -0,0 +1,56 @@ +import { Inject, Service } from 'typedi'; +import { + ISaleInvoiceCreatingPaylaod, + ISaleInvoiceEditingPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators'; + +@Service() +export class SaleInvoiceTaxRateValidateSubscriber { + @Inject() + private taxRateDTOValidator: CommandTaxRatesValidators; + + /** + * Attaches events with handlers. + */ + public attach(bus) { + bus.subscribe( + events.saleInvoice.onCreating, + this.validateSaleInvoiceEntriesTaxCodeExistanceOnCreating + ); + bus.subscribe( + events.saleInvoice.onEditing, + this.validateSaleInvoiceEntriesTaxCodeExistanceOnEditing + ); + return bus; + } + + /** + * Validate invoice entries tax rate code existance. + * @param {ISaleInvoiceCreatingPaylaod} + */ + private validateSaleInvoiceEntriesTaxCodeExistanceOnCreating = async ({ + saleInvoiceDTO, + tenantId, + }: ISaleInvoiceCreatingPaylaod) => { + await this.taxRateDTOValidator.validateItemEntriesTaxCode( + tenantId, + saleInvoiceDTO.entries + ); + }; + + /** + * + * @param {ISaleInvoiceEditingPayload} + */ + private validateSaleInvoiceEntriesTaxCodeExistanceOnEditing = async ({ + tenantId, + saleInvoiceDTO, + }: ISaleInvoiceEditingPayload) => { + await this.taxRateDTOValidator.validateItemEntriesTaxCode( + tenantId, + saleInvoiceDTO.entries + ); + }; +} diff --git a/packages/server/src/services/TaxRates/subscribers/SaleReceiptTaxRateValidateSubscriber.ts b/packages/server/src/services/TaxRates/subscribers/SaleReceiptTaxRateValidateSubscriber.ts new file mode 100644 index 000000000..d4c8e21b9 --- /dev/null +++ b/packages/server/src/services/TaxRates/subscribers/SaleReceiptTaxRateValidateSubscriber.ts @@ -0,0 +1,56 @@ +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 + ); + }; +}