diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index d34ac71a..1baaa58d 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -50,6 +50,7 @@ {"id":"zakapp-o1w","title":"Refine Docker workflow: Main=Latest, Tags=Release, Remove Noise","description":"Update CI/CD to only build on main/releases. Remove SHA tags. Ensure main maps to latest and tags map to semver.","status":"closed","priority":2,"issue_type":"task","owner":"agent@zakapp.dev","created_at":"2026-02-07T12:37:40.045500964Z","created_by":"ZakApp Agent","updated_at":"2026-02-07T12:38:31.859470573Z","closed_at":"2026-02-07T12:38:31.859470573Z","close_reason":"Closed"} {"id":"zakapp-o8e","title":"CRITICAL: Fix failing test suite - 170+ failing tests (74% pass rate)","description":"Test suite has 170+ failing tests preventing reliable deployment. Critical tests failing include authentication, asset management, and payment processing.\n\nFailing test categories:\n1. Asset creation/management tests failing due to auth token issues\n2. Rate limiting tests inconsistent\n3. Contract tests missing required fields\n4. Payment record validation failing\n\nRequired fixes:\n1. Fix authentication token generation in tests\n2. Implement missing validation in payment endpoints\n3. Resolve rate limiting test inconsistencies\n4. Update test data and assertions\n\nVerification: npm test passes with \u003e95% success rate.","notes":"Major progress: Fixed 5 more contract test files. Current status: 56 failed test files (down from 77), 150 failed tests (down from 170+), 726/899 tests passing (81% pass rate). Fixed: encryption.test.ts async bug, assets-post/put/get contract tests, auth-register contract tests, SimpleValidation non-negative value check.","status":"closed","priority":1,"issue_type":"task","owner":"agent@zakapp.dev","created_at":"2026-02-08T00:55:24.240278535Z","created_by":"ZakApp Agent","updated_at":"2026-02-09T16:54:31.874242431Z","closed_at":"2026-02-09T16:54:31.874242431Z","close_reason":"Closed","labels":["critical","quality","testing"]} {"id":"zakapp-o95","title":"Resolve merge conflicts in feature/prebuilt-docker-images","description":"Resolve merge conflicts in README.md, docker-compose.prod.yml, and docker-compose.yml on feature/prebuilt-docker-images branch.","status":"closed","priority":1,"issue_type":"task","owner":"agent@zakapp.dev","created_at":"2026-02-06T22:02:31.190750175Z","created_by":"ZakApp Agent","updated_at":"2026-02-06T22:03:51.382804555Z","closed_at":"2026-02-06T22:03:51.382804555Z","close_reason":"Closed"} +{"id":"zakapp-qz0","title":"Advanced Retirement Zakat Logic (Fiqh Compliance)","description":"Implement specific calculation methodologies for retirement assets - 'Collectible Value' vs 'Invested Growth' based on scholarly analysis.","status":"in_progress","priority":1,"issue_type":"feature","owner":"agent@zakapp.dev","created_at":"2026-02-13T15:18:58.778767861Z","created_by":"ZakApp Agent","updated_at":"2026-02-13T15:19:06.271771896Z"} {"id":"zakapp-tba","title":"CLEANUP: Resolve 100+ TODO/FIXME comments indicating incomplete features","description":"Codebase contains 100+ TODO/FIXME comments indicating rushed development and incomplete core functionality.\n\nCritical TODOs to resolve:\n- AnnualSummaryService missing payment calculations\n- Zakat routes with stubbed liability handling\n- Missing unique recipient counting in summaries\n- Incomplete methodology handling\n- Push notification service database schema\n- Missing export functionality\n\nApproach:\n1. Prioritize business-critical TODOs\n2. Implement missing functionality or remove dead code\n3. Add proper error handling for incomplete features\n4. Update documentation\n\nVerification: TODO/FIXME count reduced to \u003c20, all critical features implemented.","status":"open","priority":2,"issue_type":"task","owner":"agent@zakapp.dev","created_at":"2026-02-08T00:55:28.830212251Z","created_by":"ZakApp Agent","updated_at":"2026-02-08T00:55:28.830212251Z","labels":["cleanup","maintainability"]} {"id":"zakapp-vz1","title":"Finalize deployment script cleanup","status":"closed","priority":2,"issue_type":"task","owner":"agent@zakapp.dev","created_at":"2026-02-06T20:33:18.776788604Z","created_by":"ZakApp Agent","updated_at":"2026-02-06T20:33:23.061741955Z","closed_at":"2026-02-06T20:33:23.061741955Z","close_reason":"Closed"} {"id":"zakapp-wlu","title":"SECURITY: Remove exposed production secrets from git history","description":"Multiple .env.backup.* files containing live JWT secrets, encryption keys, and database credentials are committed to the repository despite being in .gitignore. This represents an immediate security breach requiring emergency remediation.\n\nImmediate actions needed:\n1. Remove .env.backup.* files from git tracking\n2. Rotate all exposed secrets in production\n3. Clean git history if secrets were exposed\n4. Update .gitignore to prevent future occurrences\n\nVerification: Run 'git log --all --full-history -- .env*' and confirm no secret exposure.","status":"closed","priority":0,"issue_type":"task","owner":"agent@zakapp.dev","created_at":"2026-02-08T00:55:17.971892975Z","created_by":"ZakApp Agent","updated_at":"2026-02-09T13:51:02.209817035Z","closed_at":"2026-02-09T13:51:02.209817035Z","close_reason":"Closed","labels":["critical","emergency","security"]} diff --git a/.env.dev.example b/.env.dev.example new file mode 100644 index 00000000..e2854689 --- /dev/null +++ b/.env.dev.example @@ -0,0 +1,61 @@ +# ZakApp Development Environment Configuration +# Copy this file to .env.dev and customize as needed +# ========================================= + +# ===================================================== +# PORT CONFIGURATION (Dev defaults - different from prod) +# ===================================================== +FRONTEND_PORT=3002 +FRONTEND_PORT_SSL=3444 + +# ===================================================== +# ACCESS CONFIGURATION +# ===================================================== +# Client URL (used for CORS) +CLIENT_URL=http://localhost:3002 + +# Backend CORS allowed origins +ALLOWED_ORIGINS=http://localhost:3002 + +# Backend allowed hosts +ALLOWED_HOSTS=localhost + +# App URL +APP_URL=http://localhost:3002 + +# ===================================================== +# SECURITY SECRETS (REQUIRED) +# ===================================================== +# Generate these with: openssl rand -hex 32 +JWT_SECRET=REPLACE_WITH_SECURE_SECRET +JWT_REFRESH_SECRET=REPLACE_WITH_SECURE_SECRET +ENCRYPTION_KEY=REPLACE_WITH_SECURE_SECRET + +# ===================================================== +# OPTIONAL CONFIGURATION +# ===================================================== +# CouchDB credentials +COUCHDB_USER=admin +COUCHDB_PASSWORD=REPLACE_WITH_SECURE_SECRET +COUCHDB_JWT_SECRET=REPLACE_WITH_SECURE_SECRET + +# Admin emails (comma-separated) +ADMIN_EMAILS=admin@example.com + +# Gold API Key (optional) +# GOLD_API_KEY=your_api_key_here + +# ===================================================== +# QUICK START +# ===================================================== +# 1. Run the deployment script: +# ./deploy-dev-build.sh +# +# 2. Access the application: +# - Frontend: http://localhost:3002 +# - Backend API: http://localhost:3002/api +# +# 3. For network access: +# - HTTPS: https://YOUR_IP:3444 +# +# ===================================================== diff --git a/.env.easy.example b/.env.easy.example index d69a62e7..73eba850 100644 --- a/.env.easy.example +++ b/.env.easy.example @@ -60,6 +60,7 @@ FRONTEND_PORT_SSL=3443 # These are auto-detected from APP_URL # Only modify if you have specific requirements +CLIENT_URL= ALLOWED_ORIGINS= ALLOWED_HOSTS= diff --git a/.gitignore b/.gitignore index bfef9db5..0c5d185d 100644 --- a/.gitignore +++ b/.gitignore @@ -81,10 +81,14 @@ Thumbs.db # Temporary files tmp/ temp/ +.tmp/ # Package lock files (keep package-lock.json for server) # package-lock.json (uncomment if you want to exclude) +# OpenCode context and sessions +.opencode/ + # Testing jest.config.js coverage/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c6adcdf1..d615b0aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [0.10.1] - 2026-02-16 + +### 🐛 Bug Fixes - Deployment Scripts + +**Fixed:** +- Fixed `deploy-dev-build.sh` white screen issue caused by volume mount in `docker-compose.dev.yml` that overwrote built frontend assets with unprocessed `client/public` files +- Fixed deployment scripts not properly reading `.env.dev` file (added `--env-file .env.dev` to docker-compose commands) +- Fixed missing `ALLOWED_ORIGINS`, `ALLOWED_HOSTS`, and `APP_URL` variables in newly generated `.env` files during deployment +- Improved `deploy-easy.sh` with fallback to create minimal `.env` when `.env.easy.example` doesn't exist + +**Technical Details:** +- Removed broken volume mount `./client/public:/usr/share/nginx/html:ro` from `docker-compose.dev.yml` frontend service +- This mount was serving raw `client/public/index.html` (with `%PUBLIC_URL%` placeholders) instead of the built `client/dist/index.html` +- The `/assets/` directory (Vite's compiled JS/CSS output) was missing, causing 404 errors and blank page + +--- + ## [0.10.0] - 2026-02-11 ### 🚀 Release: Test Infrastructure & Deployment Improvements diff --git a/client/package-lock.json b/client/package-lock.json index 7cb6fe0e..c6d643a3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "client", - "version": "0.9.1", + "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "client", - "version": "0.9.1", + "version": "0.10.0", "dependencies": { "@headlessui/react": "^2.2.9", "@heroicons/react": "^2.2.0", @@ -90,7 +90,7 @@ }, "../shared": { "name": "@zakapp/shared", - "version": "0.9.1", + "version": "0.9.2", "dependencies": { "js-sha256": "^0.10.0", "zod": "^3.22.4" diff --git a/client/package.json b/client/package.json index f1af7e27..d3eefd8b 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.10.0", + "version": "0.10.1", "private": true, "dependencies": { "@headlessui/react": "^2.2.9", diff --git a/client/src/components/assets/AssetForm.tsx b/client/src/components/assets/AssetForm.tsx index 4bd13be9..81e1a5fd 100644 --- a/client/src/components/assets/AssetForm.tsx +++ b/client/src/components/assets/AssetForm.tsx @@ -22,6 +22,7 @@ import { useAssetRepository } from '../../hooks/useAssetRepository'; import { useAuth } from '../../contexts/AuthContext'; import { METHODOLOGIES, MethodologyName } from '../../core/calculations/methodology'; import type { Asset, AssetType } from '../../types'; +import type { RetirementConfig } from '../../types/asset.types'; import { Button, Input } from '../ui'; import { EncryptedBadge } from '../ui/EncryptedBadge'; import { @@ -76,23 +77,6 @@ export const AssetForm: React.FC = ({ asset, onSuccess, onCancel return map[assetType] || 'cash'; }; - // Parse initial retirement treatment from metadata - const getInitialRetirementTreatment = (asset: Asset | undefined): string => { - if (!asset || !asset.metadata) return 'net_value'; - try { - const meta = typeof asset.metadata === 'string' ? JSON.parse(asset.metadata) : asset.metadata; - const details = meta.retirementDetails; - if (!details) return 'net_value'; - - if (details.withdrawalPenalty === 1.0) return 'deferred'; - if (details.withdrawalPenalty === 0.7) return 'passive'; - if (details.withdrawalPenalty === 0 && details.taxRate === 0) return 'full'; - return 'net_value'; - } catch (e) { - return 'net_value'; - } - }; - const [formData, setFormData] = useState({ name: asset?.name || '', category: getInitialCategory(asset?.type as unknown as string), @@ -105,7 +89,6 @@ export const AssetForm: React.FC = ({ asset, onSuccess, onCancel isPassiveInvestment: (asset as any)?.isPassiveInvestment || false, isRestrictedAccount: (asset as any)?.isRestrictedAccount || false, isEligibilityManual: (asset as any)?.isEligibilityManual || false, - retirementTreatment: getInitialRetirementTreatment(asset), }); const [errors, setErrors] = useState>({}); @@ -113,6 +96,16 @@ export const AssetForm: React.FC = ({ asset, onSuccess, onCancel const [calculationModifier, setCalculationModifier] = useState( (asset as any)?.calculationModifier || 1.0 ); + const [retirementConfig, setRetirementConfig] = useState(() => { + // Parse initial retirement config from asset metadata + if (!asset?.metadata) return undefined; + try { + const meta = typeof asset.metadata === 'string' ? JSON.parse(asset.metadata) : asset.metadata; + return meta.retirementConfig; + } catch { + return undefined; + } + }); const { addAsset, updateAsset } = useAssetRepository(); @@ -141,12 +134,18 @@ export const AssetForm: React.FC = ({ asset, onSuccess, onCancel let newModifier = 1.0; - if (isRetirement) { - switch (formData.retirementTreatment) { - case 'deferred': newModifier = 0.0; break; - case 'passive': newModifier = 0.3; break; - case 'net_value': newModifier = 0.7; break; // Approx - case 'full': default: newModifier = 1.0; break; + if (isRetirement && retirementConfig) { + // Use the methodology from retirementConfig to determine modifier + switch (retirementConfig.methodology) { + case 'preserved_growth': newModifier = 0.20; break; // 0.5% is equivalent to 2.5% on 20% + case 'collectible_value': { + // Calculate net factor: 1 - penalty - tax + const penalty = retirementConfig.withdrawalPenalty || 0; + const tax = retirementConfig.estimatedTaxRate || 0; + newModifier = Math.max(0, 1 - penalty - tax); + break; + } + case 'manual': default: newModifier = 1.0; break; } } else if (formData.isRestrictedAccount) { newModifier = 0.0; @@ -168,7 +167,7 @@ export const AssetForm: React.FC = ({ asset, onSuccess, onCancel if (!RESTRICTED_ACCOUNT_TYPES.includes(formData.category as any)) { setFormData(prev => ({ ...prev, isRestrictedAccount: false })); } - }, [formData.category, formData.subCategory, formData.isRestrictedAccount, formData.isPassiveInvestment, formData.retirementTreatment]); + }, [formData.category, formData.subCategory, formData.isRestrictedAccount, formData.isPassiveInvestment, retirementConfig]); const handleChange = (field: string, value: string | number | boolean) => { setFormData(prev => ({ ...prev, [field]: value })); @@ -254,17 +253,9 @@ export const AssetForm: React.FC = ({ asset, onSuccess, onCancel isEligibilityManual: formData.isEligibilityManual }; - if (isRetirement) { - const treatment = formData.retirementTreatment || 'net_value'; - if (treatment === 'net_value') { - metadataObj.retirementDetails = { withdrawalPenalty: 0.1, taxRate: 0.2 }; - } else if (treatment === 'deferred') { - metadataObj.retirementDetails = { withdrawalPenalty: 1.0, taxRate: 0 }; - } else if (treatment === 'passive') { - metadataObj.retirementDetails = { withdrawalPenalty: 0.7, taxRate: 0 }; - } else { // Full - metadataObj.retirementDetails = { withdrawalPenalty: 0, taxRate: 0 }; - } + // Include retirement config if present + if (isRetirement && retirementConfig) { + metadataObj.retirementConfig = retirementConfig; } const commonData = { @@ -626,10 +617,9 @@ export const AssetForm: React.FC = ({ asset, onSuccess, onCancel {/* Modifier Section: Retirement Treatment (Radio Group) */} {formData.category === 'stocks' && formData.subCategory?.startsWith('retirement') && ( handleChange('retirementTreatment', treatment)} + onConfigChange={setRetirementConfig} /> )} diff --git a/client/src/components/assets/form-sections/RetirementTreatmentSection.tsx b/client/src/components/assets/form-sections/RetirementTreatmentSection.tsx index a84fb6ca..81b6752b 100644 --- a/client/src/components/assets/form-sections/RetirementTreatmentSection.tsx +++ b/client/src/components/assets/form-sections/RetirementTreatmentSection.tsx @@ -1,94 +1,221 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Decimal } from 'decimal.js'; +import type { RetirementMethodology, RetirementConfig } from '../../../types/asset.types'; interface RetirementTreatmentSectionProps { - retirementTreatment: string; + retirementConfig?: RetirementConfig; value: string; - calculationModifier: number; - onTreatmentChange: (treatment: string) => void; + onConfigChange: (config: RetirementConfig | undefined) => void; } +// Tax bracket options (simplified from full table) +const TAX_BRACKET_OPTIONS = [ + { value: 0.10, label: '10%' }, + { value: 0.12, label: '12%' }, + { value: 0.22, label: '22%' }, + { value: 0.24, label: '24%' }, + { value: 0.32, label: '32%' }, + { value: 0.35, label: '35%' }, + { value: 0.37, label: '37%' }, +]; + +// Penalty rate options +const PENALTY_OPTIONS = [ + { value: 0.0, label: '0%' }, + { value: 0.10, label: '10%' }, + { value: 0.20, label: '20%' }, + { value: 0.30, label: '30%' }, + { value: 0.35, label: '35%' }, +]; + export const RetirementTreatmentSection: React.FC = ({ - retirementTreatment, + retirementConfig, value, - calculationModifier, - onTreatmentChange + onConfigChange }) => { + const [methodology, setMethodology] = useState( + retirementConfig?.methodology || 'collectible_value' + ); + const [withdrawalPenalty, setWithdrawalPenalty] = useState( + retirementConfig?.withdrawalPenalty ?? 0.10 + ); + const [estimatedTaxRate, setEstimatedTaxRate] = useState( + retirementConfig?.estimatedTaxRate ?? 0.25 + ); + + // Update parent when values change + useEffect(() => { + if (methodology === 'manual') { + onConfigChange(undefined); + } else if (methodology === 'preserved_growth') { + onConfigChange({ + methodology: 'preserved_growth', + withdrawalPenalty: 0, + estimatedTaxRate: 0, + }); + } else { + // collectible_value + onConfigChange({ + methodology: 'collectible_value', + withdrawalPenalty, + estimatedTaxRate, + }); + } + }, [methodology, withdrawalPenalty, estimatedTaxRate]); + + // Calculate preview values + const numericValue = parseFloat(String(value)) || 0; + const gross = new Decimal(numericValue); + + 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); + return ( -
-

Zakat Treatment:

+
+
+

How should we calculate Zakat on this retirement account?

+

+ Based on scholarly analysis of US retirement plans +

+
-