Skip to content

Commit

Permalink
feat: assign default sell/purchase tax rates to items (#261)
Browse files Browse the repository at this point in the history
  • Loading branch information
abouolia authored Oct 8, 2023
1 parent d40de4d commit 1ed1c9e
Show file tree
Hide file tree
Showing 25 changed files with 400 additions and 18 deletions.
27 changes: 27 additions & 0 deletions packages/server/src/api/controllers/Items/Items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ export default class ItemsController extends BaseController {
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.TEXT }),
check('sell_tax_rate_id').optional({ nullable: true }).isInt().toInt(),
check('purchase_tax_rate_id')
.optional({ nullable: true })
.isInt()
.toInt(),
check('category_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
Expand Down Expand Up @@ -508,6 +513,28 @@ export default class ItemsController extends BaseController {
],
});
}
if (error.errorType === 'PURCHASE_TAX_RATE_NOT_FOUND') {
return res.status(400).send({
errors: [
{
type: 'PURCHASE_TAX_RATE_NOT_FOUND',
message: 'Purchase tax rate has not found.',
code: 410,
},
],
});
}
if (error.errorType === 'SELL_TAX_RATE_NOT_FOUND') {
return res.status(400).send({
errors: [
{
type: 'SELL_TAX_RATE_NOT_FOUND',
message: 'Sell tax rate is not found.',
code: 420,
},
],
});
}
}
next(error);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
exports.up = (knex) => {
return knex.schema.table('items', (table) => {
table
.integer('sell_tax_rate_id')
.unsigned()
.references('id')
.inTable('tax_rates');
table
.integer('purchase_tax_rate_id')
.unsigned()
.references('id')
.inTable('tax_rates');
});
};

exports.down = (knex) => {
return knex.schema.dropTableIfExists('tax_rates');
};
6 changes: 6 additions & 0 deletions packages/server/src/interfaces/Item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export interface IItem {
sellDescription: string;
purchaseDescription: string;

sellTaxRateId: number;
purchaseTaxRateId: number;

quantityOnHand: number;

note: string;
Expand Down Expand Up @@ -54,6 +57,9 @@ export interface IItemDTO {
sellDescription: string;
purchaseDescription: string;

sellTaxRateId: number;
purchaseTaxRateId: number;

quantityOnHand: number;

note: string;
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/interfaces/TaxRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface ITaxRateCreatedPayload {
}

export interface ITaxRateEditingPayload {
oldTaxRate: ITaxRate;
editTaxRateDTO: IEditTaxRateDTO;
tenantId: number;
trx: Knex.Transaction;
Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/loaders/eventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscr
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber';
import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber';
import { SyncItemTaxRateOnEditTaxSubscriber } from '@/services/TaxRates/SyncItemTaxRateOnEditTaxSubscriber';

export default () => {
return new EventPublisher();
Expand Down Expand Up @@ -197,5 +198,7 @@ export const susbcribers = () => {
// Tax Rates - Bills
BillTaxRateValidateSubscriber,
WriteBillTaxTransactionsSubscriber,

SyncItemTaxRateOnEditTaxSubscriber
];
};
27 changes: 26 additions & 1 deletion packages/server/src/models/Item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default class Item extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry');
const WarehouseTransferEntry = require('models/WarehouseTransferEntry');
const InventoryAdjustmentEntry = require('models/InventoryAdjustmentEntry');
const TaxRate = require('models/TaxRate');

return {
/**
Expand Down Expand Up @@ -178,11 +179,35 @@ export default class Item extends mixin(TenantModel, [
to: 'media.id',
},
},

/**
* Item may has sell tax rate.
*/
sellTaxRate: {
relation: Model.BelongsToOneRelation,
modelClass: TaxRate.default,
join: {
from: 'items.sellTaxRateId',
to: 'tax_rates.id',
},
},

/**
* Item may has purchase tax rate.
*/
purchaseTaxRate: {
relation: Model.BelongsToOneRelation,
modelClass: TaxRate.default,
join: {
from: 'items.purchaseTaxRateId',
to: 'tax_rates.id',
},
},
};
}

/**
*
*
*/
static get secureDeleteRelations() {
return [
Expand Down
12 changes: 12 additions & 0 deletions packages/server/src/services/Items/CreateItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ export class CreateItem {
itemDTO.inventoryAccountId
);
}
if (itemDTO.purchaseTaxRateId) {
await this.validators.validatePurchaseTaxRateExistance(
tenantId,
itemDTO.purchaseTaxRateId
);
}
if (itemDTO.sellTaxRateId) {
await this.validators.validateSellTaxRateExistance(
tenantId,
itemDTO.sellTaxRateId
);
}
}

/**
Expand Down
14 changes: 14 additions & 0 deletions packages/server/src/services/Items/EditItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ export class EditItem {
itemDTO.inventoryAccountId
);
}
// Validate the purchase tax rate id existance.
if (itemDTO.purchaseTaxRateId) {
await this.validators.validatePurchaseTaxRateExistance(
tenantId,
itemDTO.purchaseTaxRateId
);
}
// Validate the sell tax rate id existance.
if (itemDTO.sellTaxRateId) {
await this.validators.validateSellTaxRateExistance(
tenantId,
itemDTO.sellTaxRateId
);
}
// Validate inventory account should be modified in inventory item
// has inventory transactions.
await this.validators.validateItemInvnetoryAccountModified(
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/services/Items/GetItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export class GetItem {
.withGraphFetched('category')
.withGraphFetched('costAccount')
.withGraphFetched('itemWarehouses.warehouse')
.withGraphFetched('sellTaxRate')
.withGraphFetched('purchaseTaxRate')
.throwIfNotFound();

return this.transformer.transform(tenantId, item, new ItemTransformer());
Expand Down
36 changes: 36 additions & 0 deletions packages/server/src/services/Items/ItemValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,40 @@ export class ItemsValidators {
throw new ServiceError(ERRORS.ITEM_CANNOT_CHANGE_INVENTORY_TYPE);
}
}

/**
* Validate the purchase tax rate id existance.
* @param {number} tenantId -
* @param {number} taxRateId -
*/
public async validatePurchaseTaxRateExistance(
tenantId: number,
taxRateId: number
) {
const { TaxRate } = this.tenancy.models(tenantId);

const foundTaxRate = await TaxRate.query().findById(taxRateId);

if (!foundTaxRate) {
throw new ServiceError(ERRORS.PURCHASE_TAX_RATE_NOT_FOUND);
}
}

/**
* Validate the sell tax rate id existance.
* @param {number} tenantId
* @param {number} taxRateId
*/
public async validateSellTaxRateExistance(
tenantId: number,
taxRateId: number
) {
const { TaxRate } = this.tenancy.models(tenantId);

const foundTaxRate = await TaxRate.query().findById(taxRateId);

if (!foundTaxRate) {
throw new ServiceError(ERRORS.SELL_TAX_RATE_NOT_FOUND);
}
}
}
5 changes: 4 additions & 1 deletion packages/server/src/services/Items/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export const ERRORS = {
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',

ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS'
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',

PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
};

export const DEFAULT_VIEW_COLUMNS = [];
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/services/TaxRates/EditTaxRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class EditTaxRateService {
// Triggers `onTaxRateEdited` event.
await this.eventPublisher.emitAsync(events.taxRates.onEdited, {
editTaxRateDTO,
oldTaxRate,
taxRate,
tenantId,
trx,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';

@Service()
export class SyncItemTaxRateOnEditTaxRate {
@Inject()
private tenancy: HasTenancyService;

/**
* Syncs the new tax rate created to item default sell tax rate.
* @param {number} tenantId
* @param {number} itemId
* @param {number} sellTaxRateId
*/
public updateItemSellTaxRate = async (
tenantId: number,
oldSellTaxRateId: number,
sellTaxRateId: number,
trx?: Knex.Transaction
) => {
const { Item } = this.tenancy.models(tenantId);

// Can't continue if the old and new sell tax rate id are equal.
if (oldSellTaxRateId === sellTaxRateId) return;

await Item.query().where('sellTaxRateId', oldSellTaxRateId).update({
sellTaxRateId,
});
};

/**
* Syncs the new tax rate created to item default purchase tax rate.
* @param {number} tenantId
* @param {number} itemId
* @param {number} purchaseTaxRateId
*/
public updateItemPurchaseTaxRate = async (
tenantId: number,
oldPurchaseTaxRateId: number,
purchaseTaxRateId: number,
trx?: Knex.Transaction
) => {
const { Item } = this.tenancy.models(tenantId);

// Can't continue if the old and new sell tax rate id are equal.
if (oldPurchaseTaxRateId === purchaseTaxRateId) return;

await Item.query(trx)
.where('purchaseTaxRateId', oldPurchaseTaxRateId)
.update({
purchaseTaxRateId,
});
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { SyncItemTaxRateOnEditTaxRate } from './SyncItemTaxRateOnEditTaxRate';
import events from '@/subscribers/events';
import { ITaxRateEditedPayload } from '@/interfaces';
import { runAfterTransaction } from '../UnitOfWork/TransactionsHooks';

@Service()
export class SyncItemTaxRateOnEditTaxSubscriber {
@Inject()
private syncItemRateOnEdit: SyncItemTaxRateOnEditTaxRate;

/**
* Attaches events with handles.
*/
public attach(bus) {
bus.subscribe(
events.taxRates.onEdited,
this.handleSyncNewTaxRateToItemTaxRate
);
}

/**
* Syncs the new tax rate created to default item tax rates.
* @param {ITaxRateEditedPayload} payload -
*/
private handleSyncNewTaxRateToItemTaxRate = async ({
taxRate,
tenantId,
oldTaxRate,
trx,
}: ITaxRateEditedPayload) => {
runAfterTransaction(trx, async () => {
await this.syncItemRateOnEdit.updateItemPurchaseTaxRate(
tenantId,
oldTaxRate.id,
taxRate.id
);
await this.syncItemRateOnEdit.updateItemSellTaxRate(
tenantId,
oldTaxRate.id,
taxRate.id
);
});
};
}
Loading

0 comments on commit 1ed1c9e

Please sign in to comment.