Skip to content

Comments

feat: Advanced Retirement Zakat Logic (zakapp-qz0)#289

Merged
slimatic merged 5 commits intomainfrom
feature/zakapp-qz0-retirement-zakat-logic
Feb 16, 2026
Merged

feat: Advanced Retirement Zakat Logic (zakapp-qz0)#289
slimatic merged 5 commits intomainfrom
feature/zakapp-qz0-retirement-zakat-logic

Conversation

@slimatic
Copy link
Owner

Summary

  • Add retirement Zakat methodology selection with three options:
    • Option 1: Withdrawal Value (collectible_value) - net after penalty/tax × 2.5%
    • Option 2: Invested Growth (preserved_growth) - 0.5% rule (Dr. Salah Al-Sawy)
    • Option 3: Standard Calculation (manual) - 2.5% on full value
  • Tax bracket dropdown (10%-37%) and penalty rate dropdown (0%-35%)
  • Real-time Zakat preview showing calculation breakdown
  • Fix process.env.NODE_ENV undefined error in Vite

Changes

  • client/src/types/asset.types.ts - Added RetirementMethodology and RetirementConfig types
  • client/src/core/calculations/zakat.ts - Added calculation logic for all methodologies
  • client/src/components/assets/AssetForm.tsx - Updated to use new retirementConfig
  • client/src/components/assets/form-sections/RetirementTreatmentSection.tsx - New UI
  • client/vite.config.ts - Fix process.env.NODE_ENV

Testing

  • All client tests pass (411 passed)
  • Manual testing: Add retirement asset → Select methodology → See preview

Notes

  • Phase 2 (Qada Calculator) was discussed but not implemented

ZakApp Agent added 3 commits February 13, 2026 16:17
- Add RetirementMethodology type: collectible_value, preserved_growth, manual
- Add RetirementConfig interface with methodology, withdrawalPenalty, estimatedTaxRate
- Implement calculateRetirementZakat() supporting all three methodologies
- Add parseRetirementConfig() for metadata parsing
- Preserve backward compatibility with legacy retirementDetails format

Opinion A (collectible_value): Net after penalty/tax * 2.5%
Opinion B (preserved_growth): 0.5% rule (Dr. Salah Al-Sawy)
Default: manual - standard 2.5% on full value
- Replace old retirementTreatment with new retirementConfig approach
- Add new RetirementTreatmentSection with three methodology options:
  - Option 1: Withdrawal Value (collectible_value) with penalty/tax inputs
  - Option 2: Invested Growth (preserved_growth) - 0.5% rule
  - Option 3: Standard Calculation (manual) - 2.5% on full
- Add tax bracket dropdown (10%-37%) and penalty dropdown (0%-35%)
- Add real-time calculation preview showing Zakatable amount and due
- Update AssetForm to use retirementConfig state and parse from metadata
- Preserve backward compatibility with legacy retirementDetails format
Copilot AI review requested due to automatic review settings February 13, 2026 17:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements advanced retirement Zakat calculation methodologies based on scholarly analysis, allowing users to choose between three approaches: (1) Withdrawal Value method accounting for taxes and penalties, (2) Invested Growth method (0.5% rule by Dr. Salah Al-Sawy), and (3) Standard 2.5% calculation. The implementation also fixes a Vite configuration issue where process.env.NODE_ENV was undefined.

Changes:

  • Added RetirementMethodology type and RetirementConfig interface to support three calculation methodologies
  • Implemented calculation logic for all three methodologies with legacy support for the old retirementDetails structure
  • Created new UI component with methodology selection, tax bracket dropdowns, and real-time Zakat preview
  • Fixed Vite build configuration to define process.env.NODE_ENV

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
client/vite.config.ts Adds process.env.NODE_ENV definition to fix undefined environment variable
client/src/types/asset.types.ts Defines RetirementMethodology type and RetirementConfig interface, migrating from retirementDetails
client/src/core/calculations/zakat.ts Adds parseRetirementConfig, calculateRetirementZakat (unused), and updates calculateNetWithdrawable with legacy support
client/src/components/assets/form-sections/RetirementTreatmentSection.tsx Complete redesign with three methodology options, dropdowns for tax/penalty rates, and calculation preview
client/src/components/assets/AssetForm.tsx Integrates retirementConfig, removes old retirementTreatment, updates modifier calculation

Comment on lines +115 to +145
// Calculate retirement Zakat based on methodology
function calculateRetirementZakat(asset: Asset): number {
const config = parseRetirementConfig(asset);
const grossBalance = new Decimal(asset.value || 0);

// If no config, default to 'manual' (treat as regular asset)
if (!config || config.methodology === 'manual') {
// Fall back to standard 2.5% calculation (or use calculationModifier if set)
if (typeof asset.calculationModifier === 'number' && asset.calculationModifier !== 1.0) {
return grossBalance.times(asset.calculationModifier).times(STANDARD_ZAKAT_RATE).toNumber();
}
return grossBalance.times(STANDARD_ZAKAT_RATE).toNumber();
}

// Opinion B: Preserved Growth (0.5% Rule - Dr. Salah Al-Sawy)
if (config.methodology === 'preserved_growth') {
return grossBalance.times(PRESERVED_GROWTH_RATE).toNumber();
}

// Opinion A: Collectible Value (Withdrawal Method)
// Formula: (GrossBalance - (GrossBalance * (Penalty + Tax))) * 0.025
const penalty = new Decimal(config.withdrawalPenalty || 0);
const tax = new Decimal(config.estimatedTaxRate || 0);
const one = new Decimal(1);
const netFactor = one.minus(penalty).minus(tax);
const factor = Decimal.max(0, netFactor);
const netBalance = grossBalance.times(factor);

return netBalance.times(STANDARD_ZAKAT_RATE).toNumber();
}

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function calculateRetirementZakat is defined but never used anywhere in the codebase. The actual Zakat calculation flow uses getAssetZakatableValue, which calls the legacy calculateNetWithdrawable function for retirement assets. This means the new methodology-based calculation logic in calculateRetirementZakat is not being applied.

Either:

  1. Update getAssetZakatableValue to call calculateRetirementZakat instead of calculateNetWithdrawable for retirement assets, OR
  2. Remove the unused calculateRetirementZakat function if it's not needed

The current state suggests incomplete integration of the new retirement methodology logic.

Suggested change
// Calculate retirement Zakat based on methodology
function calculateRetirementZakat(asset: Asset): number {
const config = parseRetirementConfig(asset);
const grossBalance = new Decimal(asset.value || 0);
// If no config, default to 'manual' (treat as regular asset)
if (!config || config.methodology === 'manual') {
// Fall back to standard 2.5% calculation (or use calculationModifier if set)
if (typeof asset.calculationModifier === 'number' && asset.calculationModifier !== 1.0) {
return grossBalance.times(asset.calculationModifier).times(STANDARD_ZAKAT_RATE).toNumber();
}
return grossBalance.times(STANDARD_ZAKAT_RATE).toNumber();
}
// Opinion B: Preserved Growth (0.5% Rule - Dr. Salah Al-Sawy)
if (config.methodology === 'preserved_growth') {
return grossBalance.times(PRESERVED_GROWTH_RATE).toNumber();
}
// Opinion A: Collectible Value (Withdrawal Method)
// Formula: (GrossBalance - (GrossBalance * (Penalty + Tax))) * 0.025
const penalty = new Decimal(config.withdrawalPenalty || 0);
const tax = new Decimal(config.estimatedTaxRate || 0);
const one = new Decimal(1);
const netFactor = one.minus(penalty).minus(tax);
const factor = Decimal.max(0, netFactor);
const netBalance = grossBalance.times(factor);
return netBalance.times(STANDARD_ZAKAT_RATE).toNumber();
}

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +167
// Legacy function kept for backward compatibility
function calculateNetWithdrawable(asset: Asset): number {
const config = parseRetirementConfig(asset);
const grossBalance = new Decimal(asset.value || 0);

// If no config or manual, return full value (will get 2.5% applied elsewhere)
if (!config || config.methodology === 'manual') {
return grossBalance.toNumber();
}

// If preserved growth, return 20% of value (equivalent to 0.5% vs 2.5%)
if (config.methodology === 'preserved_growth') {
return grossBalance.times(0.20).toNumber();
}

// Collectible value: apply penalty and tax
const penalty = new Decimal(config.withdrawalPenalty || 0);
const tax = new Decimal(config.estimatedTaxRate || 0);
const one = new Decimal(1);
const netFactor = one.minus(penalty).minus(tax);
const factor = Decimal.max(0, netFactor);
return grossBalance.times(factor).toNumber();
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calculationModifier is being applied in addition to the retirement-specific logic, which could result in double application of the modifier. In getAssetZakatableValue (lines 177-179), if calculationModifier is set and not 1.0, it returns early without calling calculateNetWithdrawable. However, in AssetForm.tsx, the modifier is being calculated based on the retirement config.

This creates a potential issue: the actual Zakat calculation path uses either the modifier OR the retirement-specific logic, but not both. The flow should be clarified to ensure consistency between what's previewed in the UI and what's calculated.

Copilot uses AI. Check for mistakes.
</div>
</div>
<p className="text-xs text-gray-600 italic">
{(withdrawalPenalty * 100).toFixed(0)}% penalty + {(estimatedTaxRate * 100).toFixed(0)}% tax = {(gross.minus(gross.times(withdrawalPenalty + estimatedTaxRate)).div(gross).times(100).toNumber()).toFixed(0)}% net
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calculation formula for displaying net percentage is overly complex and could be simplified. The current formula:
gross.minus(gross.times(withdrawalPenalty + estimatedTaxRate)).div(gross).times(100)

simplifies to: (1 - (withdrawalPenalty + estimatedTaxRate)) * 100

Using Decimal operations for this simple percentage calculation is unnecessary and makes the code harder to read.

Suggested change
{(withdrawalPenalty * 100).toFixed(0)}% penalty + {(estimatedTaxRate * 100).toFixed(0)}% tax = {(gross.minus(gross.times(withdrawalPenalty + estimatedTaxRate)).div(gross).times(100).toNumber()).toFixed(0)}% net
{(withdrawalPenalty * 100).toFixed(0)}% penalty + {(estimatedTaxRate * 100).toFixed(0)}% tax = {((1 - (withdrawalPenalty + estimatedTaxRate)) * 100).toFixed(0)}% net

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +90
let zatakatableValue = gross;
let zatakatRate = 0.025;
let explanation = '';

if (methodology === 'preserved_growth') {
// 0.5% rule
zatakatableValue = gross.times(0.20); // 20% is equivalent to 0.5% vs 2.5%
zatakatRate = 0.005;
explanation = 'Based on Dr. Salah Al-Sawy\'s opinion: Zakat is due on the liquid assets of the underlying companies (0.5% of total).';
} else if (methodology === 'collectible_value') {
// Net after penalty/tax * 2.5%
const penalty = new Decimal(withdrawalPenalty);
const tax = new Decimal(estimatedTaxRate);
const netFactor = Decimal.max(0, new Decimal(1).minus(penalty).minus(tax));
zatakatableValue = gross.times(netFactor);
explanation = `Zakat is due only on what you would receive after paying ${(withdrawalPenalty * 100).toFixed(0)}% penalty and ${(estimatedTaxRate * 100).toFixed(0)}% taxes.`;
} else {
explanation = 'Standard 2.5% Zakat applied to the full balance.';
}

const zatakatDue = zatakatableValue.times(zatakatRate);
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable names contain typos: "zatakatableValue" and "zatakatRate" should be "zakatableValue" and "zakatRate". These typos are used consistently throughout the component, including in lines 84, 90, 206, 210, and 214.

Suggested change
let zatakatableValue = gross;
let zatakatRate = 0.025;
let explanation = '';
if (methodology === 'preserved_growth') {
// 0.5% rule
zatakatableValue = gross.times(0.20); // 20% is equivalent to 0.5% vs 2.5%
zatakatRate = 0.005;
explanation = 'Based on Dr. Salah Al-Sawy\'s opinion: Zakat is due on the liquid assets of the underlying companies (0.5% of total).';
} else if (methodology === 'collectible_value') {
// Net after penalty/tax * 2.5%
const penalty = new Decimal(withdrawalPenalty);
const tax = new Decimal(estimatedTaxRate);
const netFactor = Decimal.max(0, new Decimal(1).minus(penalty).minus(tax));
zatakatableValue = gross.times(netFactor);
explanation = `Zakat is due only on what you would receive after paying ${(withdrawalPenalty * 100).toFixed(0)}% penalty and ${(estimatedTaxRate * 100).toFixed(0)}% taxes.`;
} else {
explanation = 'Standard 2.5% Zakat applied to the full balance.';
}
const zatakatDue = zatakatableValue.times(zatakatRate);
let zakatableValue = gross;
let zakatRate = 0.025;
let explanation = '';
if (methodology === 'preserved_growth') {
// 0.5% rule
zakatableValue = gross.times(0.20); // 20% is equivalent to 0.5% vs 2.5%
zakatRate = 0.005;
explanation = 'Based on Dr. Salah Al-Sawy\'s opinion: Zakat is due on the liquid assets of the underlying companies (0.5% of total).';
} else if (methodology === 'collectible_value') {
// Net after penalty/tax * 2.5%
const penalty = new Decimal(withdrawalPenalty);
const tax = new Decimal(estimatedTaxRate);
const netFactor = Decimal.max(0, new Decimal(1).minus(penalty).minus(tax));
zakatableValue = gross.times(netFactor);
explanation = `Zakat is due only on what you would receive after paying ${(withdrawalPenalty * 100).toFixed(0)}% penalty and ${(estimatedTaxRate * 100).toFixed(0)}% taxes.`;
} else {
explanation = 'Standard 2.5% Zakat applied to the full balance.';
}
const zakatDue = zakatableValue.times(zakatRate);

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +77
let zatakatRate = 0.025;
let explanation = '';

if (methodology === 'preserved_growth') {
// 0.5% rule
zatakatableValue = gross.times(0.20); // 20% is equivalent to 0.5% vs 2.5%
zatakatRate = 0.005;
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name contains a typo: "zatakatRate" should be "zakatRate". This typo is used in lines 71, 77, 90, and 210.

Suggested change
let zatakatRate = 0.025;
let explanation = '';
if (methodology === 'preserved_growth') {
// 0.5% rule
zatakatableValue = gross.times(0.20); // 20% is equivalent to 0.5% vs 2.5%
zatakatRate = 0.005;
let zakatRate = 0.025;
let explanation = '';
if (methodology === 'preserved_growth') {
// 0.5% rule
zatakatableValue = gross.times(0.20); // 20% is equivalent to 0.5% vs 2.5%
zakatRate = 0.005;

Copilot uses AI. Check for mistakes.
estimatedTaxRate,
});
}
}, [methodology, withdrawalPenalty, estimatedTaxRate]);
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect hook is missing onConfigChange in its dependency array. This could lead to stale closures if the callback function changes. Add onConfigChange to the dependency array, or if it's a stable callback from the parent, document why it's intentionally omitted.

Suggested change
}, [methodology, withdrawalPenalty, estimatedTaxRate]);
}, [methodology, withdrawalPenalty, estimatedTaxRate, onConfigChange]);

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +78
// 0.5% rule
zatakatableValue = gross.times(0.20); // 20% is equivalent to 0.5% vs 2.5%
zatakatRate = 0.005;
explanation = 'Based on Dr. Salah Al-Sawy\'s opinion: Zakat is due on the liquid assets of the underlying companies (0.5% of total).';
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preserved_growth preview calculation is confusing and mathematically inconsistent. The code sets zakatableValue to gross.times(0.20) and zakatRate to 0.005, which results in the correct final Zakat amount (0.5% of gross), but the display shows "Zakatable Amount" as 20% of the balance with a "Zakat Rate" of 0.5%. This is misleading to users.

For clarity, it should either:

  1. Show the full balance as zakatable with a 0.5% rate, OR
  2. Show 20% as zakatable but label it differently (e.g., "Equivalent Base" instead of "Zakatable Amount")

The current approach makes it appear that only 20% of the balance is subject to Zakat, which misrepresents the scholarly opinion.

Suggested change
// 0.5% rule
zatakatableValue = gross.times(0.20); // 20% is equivalent to 0.5% vs 2.5%
zatakatRate = 0.005;
explanation = 'Based on Dr. Salah Al-Sawy\'s opinion: Zakat is due on the liquid assets of the underlying companies (0.5% of total).';
// 0.5% rule applied to the total balance
zatakatableValue = gross;
zatakatRate = 0.005;
explanation = 'Based on Dr. Salah Al-Sawy\'s opinion: Zakat is due at 0.5% of the total balance, approximating the liquid assets of the underlying companies.';

Copilot uses AI. Check for mistakes.
ZakApp Agent and others added 2 commits February 16, 2026 22:53
…10.1)

- Remove broken volume mount from docker-compose.dev.yml that overwrote
  built frontend assets with unprocessed client/public files
- Add --env-file .env.dev to deploy-dev-build.sh docker-compose commands
- Add missing ALLOWED_ORIGINS, ALLOWED_HOSTS, APP_URL to generated .env
- Improve deploy-easy.sh with fallback when .env.easy.example missing
- Add Caddyfile configs for dev and prod deployments
- Bump version to 0.10.1 across package.json files
@slimatic slimatic merged commit a1bffef into main Feb 16, 2026
2 checks passed
@slimatic slimatic deleted the feature/zakapp-qz0-retirement-zakat-logic branch February 16, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant