Implement tax handling for branches by adding tax percent to regions, updating product sell costs, and enhancing UI for tax summaries in BranchView

This commit is contained in:
Torsten Schulz (local)
2025-12-09 16:16:08 +01:00
parent 25d7c70058
commit 43d86cce18
15 changed files with 477 additions and 19 deletions

View File

@@ -110,6 +110,29 @@ async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPe
return min + (max - min) * (knowledgeFactor / 100);
}
// Sum tax_percent for a region and its ancestors (upwards). Returns numeric percent (e.g. 7.5)
async function getCumulativeTaxPercent(regionId) {
if (!regionId) return 0;
const rows = await sequelize.query(
`WITH RECURSIVE ancestors AS (
SELECT id, parent_id, tax_percent
FROM falukant_data.region r
WHERE id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent
FROM falukant_data.region reg
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;`,
{
replacements: { id: regionId },
type: sequelize.QueryTypes.SELECT
}
);
const val = rows?.[0]?.total ?? 0;
return parseFloat(val) || 0;
}
function calculateMarriageCost(titleOfNobility, age) {
const minTitle = 1;
const adjustedTitle = titleOfNobility - minTitle + 1;
@@ -1502,10 +1525,28 @@ class FalukantService extends BaseService {
if (available < quantity) throw new Error('Not enough inventory available');
const item = inventory[0].productType;
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
const revenue = quantity * pricePerUnit;
const moneyResult = await updateFalukantUserMoney(user.id, revenue, 'Product sale', user.id);
if (!moneyResult.success) throw new Error('Failed to update money');
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
// compute cumulative tax (region + ancestors) and inflate price so seller net is unchanged
const cumulativeTax = await getCumulativeTaxPercent(branch.regionId);
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
const revenue = quantity * adjustedPricePerUnit;
// compute tax and net
const taxValue = Math.round((revenue * cumulativeTax / 100) * 100) / 100;
const net = Math.round((revenue - taxValue) * 100) / 100;
// Book net to seller
const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id);
if (!moneyResult.success) throw new Error('Failed to update money for seller');
// Book tax to treasury (if configured)
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
if (treasuryId && taxValue > 0) {
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id);
if (!taxResult.success) throw new Error('Failed to update money for treasury');
}
let remaining = quantity;
for (const inv of inventory) {
if (inv.quantity <= remaining) {
@@ -1573,16 +1614,41 @@ class FalukantService extends BaseService {
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
const regionId = item.stock.branch.regionId;
const pricePerUnit = await calcRegionalSellPrice(item.productType, knowledgeVal, regionId);
total += item.quantity * pricePerUnit;
const cumulativeTax = await getCumulativeTaxPercent(regionId);
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
total += item.quantity * adjustedPricePerUnit;
await this.addSellItem(item.stock.branch.id, falukantUser.id, item.productType.id, item.quantity);
}
// compute tax per region (using cumulative tax per region) and aggregate
let totalTax = 0;
for (const item of inventory) {
const regionId = item.stock.branch.regionId;
const region = await RegionData.findOne({ where: { id: regionId } });
const cumulativeTax = await getCumulativeTaxPercent(regionId);
const pricePerUnit = await calcRegionalSellPrice(item.productType, item.productType.knowledges?.[0]?.knowledge || 0, regionId);
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
const itemRevenue = item.quantity * adjustedPricePerUnit;
const itemTax = Math.round((itemRevenue * cumulativeTax / 100) * 100) / 100;
totalTax += itemTax;
}
const totalNet = Math.round((total - totalTax) * 100) / 100;
const moneyResult = await updateFalukantUserMoney(
falukantUser.id,
total,
'Sell all products',
totalNet,
'Sell all products (net)',
falukantUser.id
);
if (!moneyResult.success) throw new Error('Failed to update money');
if (!moneyResult.success) throw new Error('Failed to update money for seller');
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
if (treasuryId && totalTax > 0) {
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), Math.round(totalTax * 100) / 100, `Sales tax (aggregate)`, falukantUser.id);
if (!taxResult.success) throw new Error('Failed to update money for treasury');
}
for (const item of inventory) {
await Inventory.destroy({ where: { id: item.id } });
}
@@ -1617,6 +1683,32 @@ class FalukantService extends BaseService {
}
}
// Return tax summary for a branch: total cumulative tax and breakdown per region (region -> parent chain)
async getBranchTaxes(hashedUserId, branchId) {
const user = await getFalukantUserOrFail(hashedUserId);
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id }, include: [{ model: RegionData, as: 'region', attributes: ['id','name','parentId','taxPercent'] }] });
if (!branch) throw new Error('Branch not found');
const regionId = branch.regionId;
// gather region + ancestors with taxPercent
const rows = await sequelize.query(`
WITH RECURSIVE ancestors AS (
SELECT id, parent_id, tax_percent, name FROM falukant_data.region WHERE id = :id
UNION ALL
SELECT r.id, r.parent_id, r.tax_percent, r.name FROM falukant_data.region r JOIN ancestors a ON r.id = a.parent_id
) SELECT id, name, parent_id, COALESCE(tax_percent,0) AS tax_percent FROM ancestors;
`, { replacements: { id: regionId }, type: sequelize.QueryTypes.SELECT });
// compute total
const total = rows.reduce((sum, r) => sum + parseFloat(r.tax_percent || 0), 0);
return {
regionId,
total: Math.round(total * 100) / 100,
breakdown: rows.map(r => ({ id: r.id, name: r.name, parentId: r.parent_id, taxPercent: parseFloat(r.tax_percent || 0) }))
};
}
async moneyHistory(hashedUserId, page = 1, filter = '') {
const u = await getFalukantUserOrFail(hashedUserId);
const limit = 25, offset = (page - 1) * limit;