Add tax calculation helpers and integrate into existing schemas
This commit introduces a new `tax.js` module containing functions for tax rate resolution and amount calculation with tax. The `amountWithTax` and `resolveTaxRate` functions are integrated into various schemas, including `invoice`, `orderitem`, `shipment`, `courierservice`, `filament`, `part`, and `product`, enhancing their ability to compute amounts with applicable taxes. Additionally, corresponding tests are added to ensure the correctness of the new tax-related functionalities.
This commit is contained in:
parent
edb8282c41
commit
1f679825df
37
src/database/__tests__/tax.test.js
Normal file
37
src/database/__tests__/tax.test.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { amountWithTax, effectiveMarginPrice } from '../tax.js';
|
||||||
|
|
||||||
|
describe('amountWithTax', () => {
|
||||||
|
it('returns 0 for zero amount', () => {
|
||||||
|
expect(amountWithTax(0, { rateType: 'percentage', rate: 20 })).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies percentage tax', () => {
|
||||||
|
expect(amountWithTax(100, { rateType: 'percentage', rate: 20 })).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies fixed amount tax', () => {
|
||||||
|
expect(amountWithTax(100, { rateType: 'amount', rate: 5 })).toBe(105);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies fixed rate type from API schema', () => {
|
||||||
|
expect(amountWithTax(100, { rateType: 'fixed', rate: 5 })).toBe(105);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns base amount when tax rate is missing', () => {
|
||||||
|
expect(amountWithTax(100, null)).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('effectiveMarginPrice', () => {
|
||||||
|
it('computes price from margin mode', () => {
|
||||||
|
expect(
|
||||||
|
effectiveMarginPrice({ priceMode: 'margin', price: 50, cost: 100, margin: 20 })
|
||||||
|
).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns explicit price for amount mode', () => {
|
||||||
|
expect(
|
||||||
|
effectiveMarginPrice({ priceMode: 'amount', price: 150, cost: 100, margin: 20 })
|
||||||
|
).toBe(150);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory, editObject, getObject } from '../../database.js';
|
||||||
|
import { taxRateModel } from '../management/taxrate.schema.js';
|
||||||
|
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||||
|
|
||||||
const invoiceOrderItemSchema = new Schema(
|
const invoiceOrderItemSchema = new Schema(
|
||||||
{
|
{
|
||||||
@ -140,23 +142,41 @@ invoiceSchema.statics.recalculate = async function (invoice, user) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const invoiceOrderItems = [];
|
||||||
|
for (const item of invoice.invoiceOrderItems || []) {
|
||||||
|
const taxRate = await resolveTaxRate(item.taxRate, getObject, taxRateModel);
|
||||||
|
invoiceOrderItems.push({
|
||||||
|
...item,
|
||||||
|
invoiceAmountWithTax: amountWithTax(item.invoiceAmount, taxRate),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceShipments = [];
|
||||||
|
for (const item of invoice.invoiceShipments || []) {
|
||||||
|
const taxRate = await resolveTaxRate(item.taxRate, getObject, taxRateModel);
|
||||||
|
invoiceShipments.push({
|
||||||
|
...item,
|
||||||
|
invoiceAmountWithTax: amountWithTax(item.invoiceAmount, taxRate),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate totals from invoiceOrderItems
|
// Calculate totals from invoiceOrderItems
|
||||||
let totalAmount = 0;
|
let totalAmount = 0;
|
||||||
for (const item of invoice.invoiceOrderItems || []) {
|
for (const item of invoiceOrderItems) {
|
||||||
totalAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
totalAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
||||||
}
|
}
|
||||||
let totalAmountWithTax = 0;
|
let totalAmountWithTax = 0;
|
||||||
for (const item of invoice.invoiceOrderItems || []) {
|
for (const item of invoiceOrderItems) {
|
||||||
totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate shipping totals from invoiceShipments
|
// Calculate shipping totals from invoiceShipments
|
||||||
let shippingAmount = 0;
|
let shippingAmount = 0;
|
||||||
for (const item of invoice.invoiceShipments || []) {
|
for (const item of invoiceShipments) {
|
||||||
shippingAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
shippingAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
||||||
}
|
}
|
||||||
let shippingAmountWithTax = 0;
|
let shippingAmountWithTax = 0;
|
||||||
for (const item of invoice.invoiceShipments || []) {
|
for (const item of invoiceShipments) {
|
||||||
shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +188,8 @@ invoiceSchema.statics.recalculate = async function (invoice, user) {
|
|||||||
(parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount));
|
(parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount));
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
|
invoiceOrderItems,
|
||||||
|
invoiceShipments,
|
||||||
totalAmount: parseFloat(totalAmount).toFixed(2),
|
totalAmount: parseFloat(totalAmount).toFixed(2),
|
||||||
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
|
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
|
||||||
shippingAmount: parseFloat(shippingAmount).toFixed(2),
|
shippingAmount: parseFloat(shippingAmount).toFixed(2),
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
getObject,
|
getObject,
|
||||||
} from '../../database.js';
|
} from '../../database.js';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const skuModelsByItemType = {
|
const skuModelsByItemType = {
|
||||||
@ -194,17 +195,10 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let taxRate = orderItem.taxRate;
|
const taxRate = await resolveTaxRate(orderItem.taxRate, getObject, taxRateModel);
|
||||||
if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length === 1) {
|
|
||||||
taxRate = await getObject({
|
|
||||||
model: taxRateModel,
|
|
||||||
id: orderItem.taxRate._id,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderTotalAmount = effectiveItemAmount * orderItem.quantity;
|
const orderTotalAmount = effectiveItemAmount * orderItem.quantity;
|
||||||
const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100);
|
const orderTotalAmountWithTax = amountWithTax(orderTotalAmount, taxRate);
|
||||||
|
|
||||||
const orderItemUpdateData = {
|
const orderItemUpdateData = {
|
||||||
totalAmount: orderTotalAmount,
|
totalAmount: orderTotalAmount,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
editObject,
|
editObject,
|
||||||
getObject,
|
getObject,
|
||||||
} from '../../database.js';
|
} from '../../database.js';
|
||||||
|
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||||
|
|
||||||
const shipmentSchema = new Schema(
|
const shipmentSchema = new Schema(
|
||||||
{
|
{
|
||||||
@ -102,26 +103,17 @@ shipmentSchema.statics.recalculate = async function (shipment, user) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var taxRate = shipment.taxRate;
|
const taxRate = await resolveTaxRate(shipment.taxRate, getObject, taxRateModel);
|
||||||
|
|
||||||
if (shipment.taxRate?._id && Object.keys(shipment.taxRate).length == 1) {
|
const amountWithTaxValue = amountWithTax(shipment.amount || 0, taxRate);
|
||||||
taxRate = await getObject({
|
|
||||||
model: taxRateModel,
|
|
||||||
id: shipment.taxRate._id,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const amountWithTax = parseFloat(
|
|
||||||
(shipment.amount || 0) * (1 + (taxRate?.rate || 0) / 100)
|
|
||||||
).toFixed(2);
|
|
||||||
await editObject({
|
await editObject({
|
||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
id: shipment._id,
|
id: shipment._id,
|
||||||
updateData: {
|
updateData: {
|
||||||
amountWithTax: amountWithTax,
|
amountWithTax: amountWithTaxValue,
|
||||||
invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0),
|
invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0),
|
||||||
invoicedAmountWithTaxRemaining: amountWithTax - (shipment.invoicedAmountWithTax || 0),
|
invoicedAmountWithTaxRemaining:
|
||||||
|
amountWithTaxValue - (shipment.invoicedAmountWithTax || 0),
|
||||||
},
|
},
|
||||||
user,
|
user,
|
||||||
recalculate: false,
|
recalculate: false,
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { taxRateModel } from './taxrate.schema.js';
|
||||||
|
import { editObject, getObject } from '../../database.js';
|
||||||
|
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const marketplaceMappingSchema = new mongoose.Schema(
|
const marketplaceMappingSchema = new mongoose.Schema(
|
||||||
@ -39,4 +42,29 @@ courierServiceSchema.virtual('id').get(function () {
|
|||||||
|
|
||||||
courierServiceSchema.set('toJSON', { virtuals: true });
|
courierServiceSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
courierServiceSchema.statics.recalculate = async function (courierService, user) {
|
||||||
|
const costTaxRate = await resolveTaxRate(courierService.costTaxRate, getObject, taxRateModel);
|
||||||
|
const updateData = {};
|
||||||
|
|
||||||
|
if (courierService.cost != null) {
|
||||||
|
updateData.costWithTax = amountWithTax(courierService.cost, costTaxRate);
|
||||||
|
}
|
||||||
|
if (courierService.additionalCost != null) {
|
||||||
|
updateData.additionalCostWithTax = amountWithTax(
|
||||||
|
courierService.additionalCost,
|
||||||
|
costTaxRate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: courierService._id,
|
||||||
|
updateData,
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const courierServiceModel = mongoose.model('courierService', courierServiceSchema);
|
export const courierServiceModel = mongoose.model('courierService', courierServiceSchema);
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { taxRateModel } from '../management/taxrate.schema.js';
|
||||||
|
import { editObject, getObject } from '../../database.js';
|
||||||
|
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
// Filament base - cost and tax; color and cost override at FilamentSKU
|
// Filament base - cost and tax; color and cost override at FilamentSKU
|
||||||
@ -28,6 +31,24 @@ filamentSchema.virtual('id').get(function () {
|
|||||||
filamentSchema.set('toJSON', { virtuals: true });
|
filamentSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
filamentSchema.statics.recalculate = async function (filament, user) {
|
filamentSchema.statics.recalculate = async function (filament, user) {
|
||||||
|
const costTaxRate = await resolveTaxRate(filament.costTaxRate, getObject, taxRateModel);
|
||||||
|
const taxUpdateData = {};
|
||||||
|
|
||||||
|
if (filament.cost != null) {
|
||||||
|
taxUpdateData.costWithTax = amountWithTax(filament.cost, costTaxRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(taxUpdateData).length > 0) {
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: filament._id,
|
||||||
|
updateData: taxUpdateData,
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
Object.assign(filament, taxUpdateData);
|
||||||
|
}
|
||||||
|
|
||||||
const filamentSkuModel = mongoose.model('filamentSku');
|
const filamentSkuModel = mongoose.model('filamentSku');
|
||||||
const skus = await filamentSkuModel.find({ filament: filament._id }).select('_id').lean();
|
const skus = await filamentSkuModel.find({ filament: filament._id }).select('_id').lean();
|
||||||
for (const sku of skus) {
|
for (const sku of skus) {
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { filamentModel } from './filament.schema.js';
|
||||||
|
import { taxRateModel } from './taxrate.schema.js';
|
||||||
|
import { editObject, getObject } from '../../database.js';
|
||||||
|
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
// Define the main filament SKU schema - color and cost live at SKU level
|
// Define the main filament SKU schema - color and cost live at SKU level
|
||||||
@ -30,6 +34,34 @@ filamentSkuSchema.virtual('id').get(function () {
|
|||||||
filamentSkuSchema.set('toJSON', { virtuals: true });
|
filamentSkuSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
filamentSkuSchema.statics.recalculate = async function (filamentSku, user) {
|
filamentSkuSchema.statics.recalculate = async function (filamentSku, user) {
|
||||||
|
const parent = await getObject({
|
||||||
|
model: filamentModel,
|
||||||
|
id: filamentSku.filament?._id || filamentSku.filament,
|
||||||
|
cached: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const taxUpdateData = {};
|
||||||
|
|
||||||
|
if (filamentSku.overrideCost) {
|
||||||
|
const costTaxRate = await resolveTaxRate(filamentSku.costTaxRate, getObject, taxRateModel);
|
||||||
|
if (filamentSku.cost != null) {
|
||||||
|
taxUpdateData.costWithTax = amountWithTax(filamentSku.cost, costTaxRate);
|
||||||
|
}
|
||||||
|
} else if (parent?.costWithTax != null) {
|
||||||
|
taxUpdateData.costWithTax = parent.costWithTax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(taxUpdateData).length > 0) {
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: filamentSku._id,
|
||||||
|
updateData: taxUpdateData,
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
Object.assign(filamentSku, taxUpdateData);
|
||||||
|
}
|
||||||
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
const orderItemModel = mongoose.model('orderItem');
|
||||||
const skuId = filamentSku._id;
|
const skuId = filamentSku._id;
|
||||||
const draftOrderItems = await orderItemModel
|
const draftOrderItems = await orderItemModel
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { taxRateModel } from '../management/taxrate.schema.js';
|
||||||
|
import { editObject, getObject } from '../../database.js';
|
||||||
|
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
// Define the main part schema - cost/price and tax; override at PartSku
|
// Define the main part schema - cost/price and tax; override at PartSku
|
||||||
@ -32,6 +35,28 @@ partSchema.virtual('id').get(function () {
|
|||||||
partSchema.set('toJSON', { virtuals: true });
|
partSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
partSchema.statics.recalculate = async function (part, user) {
|
partSchema.statics.recalculate = async function (part, user) {
|
||||||
|
const costTaxRate = await resolveTaxRate(part.costTaxRate, getObject, taxRateModel);
|
||||||
|
const priceTaxRate = await resolveTaxRate(part.priceTaxRate, getObject, taxRateModel);
|
||||||
|
const taxUpdateData = {};
|
||||||
|
|
||||||
|
if (part.cost != null) {
|
||||||
|
taxUpdateData.costWithTax = amountWithTax(part.cost, costTaxRate);
|
||||||
|
}
|
||||||
|
if (part.price != null) {
|
||||||
|
taxUpdateData.priceWithTax = amountWithTax(part.price, priceTaxRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(taxUpdateData).length > 0) {
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: part._id,
|
||||||
|
updateData: taxUpdateData,
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
Object.assign(part, taxUpdateData);
|
||||||
|
}
|
||||||
|
|
||||||
const partSkuModel = mongoose.model('partSku');
|
const partSkuModel = mongoose.model('partSku');
|
||||||
const skus = await partSkuModel.find({ part: part._id }).select('_id').lean();
|
const skus = await partSkuModel.find({ part: part._id }).select('_id').lean();
|
||||||
for (const sku of skus) {
|
for (const sku of skus) {
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { partModel } from './part.schema.js';
|
||||||
|
import { taxRateModel } from './taxrate.schema.js';
|
||||||
|
import { editObject, getObject } from '../../database.js';
|
||||||
|
import {
|
||||||
|
amountWithTax,
|
||||||
|
effectiveMarginPrice,
|
||||||
|
resolveTaxRate,
|
||||||
|
} from '../../tax.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
// Define the main part SKU schema - pricing lives at SKU level
|
// Define the main part SKU schema - pricing lives at SKU level
|
||||||
@ -36,6 +44,54 @@ partSkuSchema.virtual('id').get(function () {
|
|||||||
partSkuSchema.set('toJSON', { virtuals: true });
|
partSkuSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
partSkuSchema.statics.recalculate = async function (partSku, user) {
|
partSkuSchema.statics.recalculate = async function (partSku, user) {
|
||||||
|
const parent = await getObject({
|
||||||
|
model: partModel,
|
||||||
|
id: partSku.part?._id || partSku.part,
|
||||||
|
cached: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const taxUpdateData = {};
|
||||||
|
|
||||||
|
if (partSku.overrideCost) {
|
||||||
|
const costTaxRate = await resolveTaxRate(partSku.costTaxRate, getObject, taxRateModel);
|
||||||
|
if (partSku.cost != null) {
|
||||||
|
taxUpdateData.costWithTax = amountWithTax(partSku.cost, costTaxRate);
|
||||||
|
}
|
||||||
|
} else if (parent?.costWithTax != null) {
|
||||||
|
taxUpdateData.costWithTax = parent.costWithTax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partSku.overridePrice) {
|
||||||
|
const priceTaxRate = await resolveTaxRate(
|
||||||
|
partSku.priceTaxRate ?? parent?.priceTaxRate,
|
||||||
|
getObject,
|
||||||
|
taxRateModel
|
||||||
|
);
|
||||||
|
const cost = partSku.overrideCost ? partSku.cost : parent?.cost;
|
||||||
|
const price = effectiveMarginPrice({
|
||||||
|
priceMode: partSku.priceMode ?? parent?.priceMode,
|
||||||
|
price: partSku.price,
|
||||||
|
cost,
|
||||||
|
margin: partSku.margin ?? parent?.margin,
|
||||||
|
});
|
||||||
|
if (price != null) {
|
||||||
|
taxUpdateData.priceWithTax = amountWithTax(price, priceTaxRate);
|
||||||
|
}
|
||||||
|
} else if (parent?.priceWithTax != null) {
|
||||||
|
taxUpdateData.priceWithTax = parent.priceWithTax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(taxUpdateData).length > 0) {
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: partSku._id,
|
||||||
|
updateData: taxUpdateData,
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
Object.assign(partSku, taxUpdateData);
|
||||||
|
}
|
||||||
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
const orderItemModel = mongoose.model('orderItem');
|
||||||
const skuId = partSku._id;
|
const skuId = partSku._id;
|
||||||
const draftOrderItems = await orderItemModel
|
const draftOrderItems = await orderItemModel
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { taxRateModel } from '../management/taxrate.schema.js';
|
||||||
|
import { editObject, getObject } from '../../database.js';
|
||||||
|
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
// Define the main product schema
|
// Define the main product schema
|
||||||
@ -35,6 +38,34 @@ productSchema.virtual('id').get(function () {
|
|||||||
productSchema.set('toJSON', { virtuals: true });
|
productSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
productSchema.statics.recalculate = async function (product, user) {
|
productSchema.statics.recalculate = async function (product, user) {
|
||||||
|
const costTaxRate = await resolveTaxRate(product.costTaxRate, getObject, taxRateModel);
|
||||||
|
const priceTaxRate = await resolveTaxRate(product.priceTaxRate, getObject, taxRateModel);
|
||||||
|
const taxUpdateData = {};
|
||||||
|
|
||||||
|
if (product.cost != null) {
|
||||||
|
taxUpdateData.costWithTax = amountWithTax(product.cost, costTaxRate);
|
||||||
|
}
|
||||||
|
if (product.price != null) {
|
||||||
|
taxUpdateData.priceWithTax = amountWithTax(product.price, priceTaxRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(taxUpdateData).length > 0) {
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: product._id,
|
||||||
|
updateData: taxUpdateData,
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
Object.assign(product, taxUpdateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const productSkuModel = mongoose.model('productSku');
|
||||||
|
const skus = await productSkuModel.find({ product: product._id }).select('_id').lean();
|
||||||
|
for (const sku of skus) {
|
||||||
|
await productSkuModel.recalculate(sku, user);
|
||||||
|
}
|
||||||
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
const orderItemModel = mongoose.model('orderItem');
|
||||||
const itemId = product._id;
|
const itemId = product._id;
|
||||||
const draftOrderItems = await orderItemModel
|
const draftOrderItems = await orderItemModel
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { productModel } from './product.schema.js';
|
||||||
|
import { taxRateModel } from './taxrate.schema.js';
|
||||||
|
import { editObject, getObject } from '../../database.js';
|
||||||
|
import {
|
||||||
|
amountWithTax,
|
||||||
|
effectiveMarginPrice,
|
||||||
|
resolveTaxRate,
|
||||||
|
} from '../../tax.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const partSkuUsageSchema = new Schema({
|
const partSkuUsageSchema = new Schema({
|
||||||
@ -43,6 +51,54 @@ productSkuSchema.virtual('id').get(function () {
|
|||||||
productSkuSchema.set('toJSON', { virtuals: true });
|
productSkuSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
productSkuSchema.statics.recalculate = async function (productSku, user) {
|
productSkuSchema.statics.recalculate = async function (productSku, user) {
|
||||||
|
const parent = await getObject({
|
||||||
|
model: productModel,
|
||||||
|
id: productSku.product?._id || productSku.product,
|
||||||
|
cached: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const taxUpdateData = {};
|
||||||
|
|
||||||
|
if (productSku.overrideCost) {
|
||||||
|
const costTaxRate = await resolveTaxRate(productSku.costTaxRate, getObject, taxRateModel);
|
||||||
|
if (productSku.cost != null) {
|
||||||
|
taxUpdateData.costWithTax = amountWithTax(productSku.cost, costTaxRate);
|
||||||
|
}
|
||||||
|
} else if (parent?.costWithTax != null) {
|
||||||
|
taxUpdateData.costWithTax = parent.costWithTax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productSku.overridePrice) {
|
||||||
|
const priceTaxRate = await resolveTaxRate(
|
||||||
|
productSku.priceTaxRate ?? parent?.priceTaxRate,
|
||||||
|
getObject,
|
||||||
|
taxRateModel
|
||||||
|
);
|
||||||
|
const cost = productSku.overrideCost ? productSku.cost : parent?.cost;
|
||||||
|
const price = effectiveMarginPrice({
|
||||||
|
priceMode: productSku.priceMode ?? parent?.priceMode,
|
||||||
|
price: productSku.price,
|
||||||
|
cost,
|
||||||
|
margin: productSku.margin ?? parent?.margin,
|
||||||
|
});
|
||||||
|
if (price != null) {
|
||||||
|
taxUpdateData.priceWithTax = amountWithTax(price, priceTaxRate);
|
||||||
|
}
|
||||||
|
} else if (parent?.priceWithTax != null) {
|
||||||
|
taxUpdateData.priceWithTax = parent.priceWithTax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(taxUpdateData).length > 0) {
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: productSku._id,
|
||||||
|
updateData: taxUpdateData,
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
Object.assign(productSku, taxUpdateData);
|
||||||
|
}
|
||||||
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
const orderItemModel = mongoose.model('orderItem');
|
||||||
const skuId = productSku._id;
|
const skuId = productSku._id;
|
||||||
const draftOrderItems = await orderItemModel
|
const draftOrderItems = await orderItemModel
|
||||||
|
|||||||
54
src/database/tax.js
Normal file
54
src/database/tax.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Tax calculation helpers mirroring farmcontrol-ui model value functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function isPopulatedTaxRate(taxRate) {
|
||||||
|
return taxRate != null && taxRate.rateType != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveTaxRate(taxRateRef, getObject, taxRateModel) {
|
||||||
|
if (!taxRateRef) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (isPopulatedTaxRate(taxRateRef)) {
|
||||||
|
return taxRateRef;
|
||||||
|
}
|
||||||
|
const id = taxRateRef._id ?? taxRateRef;
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof taxRateRef === 'object' &&
|
||||||
|
taxRateRef._id &&
|
||||||
|
Object.keys(taxRateRef).length === 1
|
||||||
|
) {
|
||||||
|
return await getObject({ model: taxRateModel, id, cached: true });
|
||||||
|
}
|
||||||
|
return await getObject({ model: taxRateModel, id, cached: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function amountWithTax(amount, taxRate) {
|
||||||
|
const base = Number.parseFloat(amount) || 0;
|
||||||
|
if (!base) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!taxRate) {
|
||||||
|
return Number.parseFloat(base.toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rate = Number.parseFloat(taxRate.rate) || 0;
|
||||||
|
if (taxRate.rateType === 'percentage') {
|
||||||
|
return Number.parseFloat((base * (1 + rate / 100)).toFixed(2));
|
||||||
|
}
|
||||||
|
if (taxRate.rateType === 'amount' || taxRate.rateType === 'fixed') {
|
||||||
|
return Number.parseFloat((base + rate).toFixed(2));
|
||||||
|
}
|
||||||
|
return Number.parseFloat(base.toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function effectiveMarginPrice({ priceMode, price, cost, margin }) {
|
||||||
|
if (priceMode === 'margin' && margin != null && cost != null) {
|
||||||
|
return cost * (1 + margin / 100);
|
||||||
|
}
|
||||||
|
return price;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user