- 21 Jul, 2026 21 commits
-
-
Fares authored
The cashier payment page was displaying the deceased husband's info (name, form number, membership number). For death_fee payments, the recipient is the wife — extracted from the notes JSON. Form/membership numbers are hidden since the wife doesn't have them yet. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
The board approval percentage calculation had fragile fallbacks that could miss the pricing_configs data and fall back to the old membership_value. Added cascading fallbacks: PricingEngine → branch+qual → branch-only → any active pricing → membership_value (last resort). Also fixed the JS preview in show.php which was using the stored membership_value (114,000) instead of the current plan price (150,000) for the real-time calculation hint. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Child separation is not a death/full transfer — the parent stays active with their original membership number. The separated child gets a brand new sequential number via MemberNumberGenerator::getNextMembershipNumber(). Only full_transfer type archives the source member and passes the same number to the new member. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Form now collects for spouses: date_of_birth, nationality, marriage_date, join_date, payment_date. For children: date_of_birth, join_date. Validates spouse minimum age (SPOUSE_MIN_AGE rule, default 21) and requires marriage_date. Non-Egyptian spouses get addition_fee calculated as SPOUSE_FOREIGN_FEE% of current plan price at completion time. TransferProcessor now uses submitted dates instead of defaults and stores nationality and addition_fee on the spouse record. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
The column was in the model's fillable but never had a migration. Causes 'Column not found' error when storing female child separation requests. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Empty form fields post as '' which ?? doesn't catch (only null). MySQL rejects '' for decimal columns. Switched to ?: so empty strings fall through to the '0.00' default. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Reflects that deceased is excluded from annual sub, trustee fee uses current plan price via PricingEngine, and Pricing module is now a dependency. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
The deceased member should not be charged annual subscription — the primary spouse becomes the new member. Trustee fee percentage now uses the current membership plan price from pricing_configs (e.g. 150,000) instead of the historical membership_value stored at enrollment time (e.g. 114,000). Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
The TransferProcessor::execute() was failing with "Field 'date_of_birth' doesn't have a default value" because: 1. Child's date_of_birth could be empty string (not null), bypassing ?? operator 2. Children table has no phone_mobile column, causing NOT NULL violation on members 3. Spouse creation was missing required fields (date_of_birth, marriage_date, join_date) 4. Temporary member creation was missing required fields (date_of_birth, gender, category) Changes: - Sanitize DOB: treat empty string and '0000-00-00' as null before NID fallback - Add phone_mobile fallback chain: subject → source member → placeholder - Spouse inserts now include all NOT NULL fields with sensible defaults - Children inserts include child_order - Temporary member inserts include category, date_of_birth, gender with NID parsing - Death module: fix secondary spouse phone_mobile NULL issue Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Death module: - Annual subscription now includes ALL family members (spouses, children, temps + dev fee) - Temporary members distribution UI added to create form - Payment receipt created in primary spouse's name (not deceased) - Form number field added to membership application - Deceased member name stored on new membership record - Full Arabic audit trail logging on completion Waiver module: - Mark ALL family subscriptions as paid after completion (not just member) - Set activated_by_payment_id on all dependents - Copy membership_value from source to target member - Add transferred_from_waiver_id and waived_from_member_name to members - Display waiver source badge and archive section on member profile Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Expanded the special discounts system to support: - Discount types: percentage, fixed amount, or free subscription - Date range (effective_from/effective_to) for time-bounded offers - Conditions: none (direct), full_payment, or min_payment threshold - Bonus: free subscription years granted when discount activates - Applies-to targeting: membership_fee, subscription, or all Added SpecialDiscountService for evaluating conditional discounts and applying free subscription bonuses on member.activated event. Updated form UI with dynamic visibility and validation. Migration: Phase_98_003 adds new columns to special_discounts table. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
The children table requires date_of_birth (NOT NULL). The dependent creation loop now derives DOB from national_id via NationalIdParser, falling back to a placeholder if neither is available. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
When creating the new member record, date_of_birth may be NULL on the child record. Now attempts to derive DOB and gender from national_id via NationalIdParser before falling back to a placeholder. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
1. isCurrentYearSubscriptionCovered() was comparing activation date against its own fiscal year (always true) — now correctly compares against the current fiscal year's July 1st. 2. Annual subscription in calculate() now includes all family members (spouses, children, temps) not just the member alone. 3. marriage_date and dep count columns are only included in INSERT when non-null, preventing column-not-found error if migration hasn't run. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
1. Subscriptions now marked as paid: after waiver completes, sync subscriptions for target member and mark current FY as paid since the fee was already collected in the waiver payment. 2. Membership date: set created_at to today on waiver completion so the member profile shows the actual membership start date. 3. Total paid shows correctly: BillingService now recognizes activated_by_payment_id as proof that form_fee and membership_fee are paid (covers waiver_fee and separation_fee payment types). 4. Waiver list shows names: fix $r['member_name'] → $r['source_name'] to match the SQL alias from WaiverRequest::search(). Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Each subscription row now independently stores: - receipt_number: denormalized from receipts table for quick audit access - paid_by: FK to employees — who processed the payment - paid_at: already existed Migration adds columns and backfills existing paid rows from payments/receipts. View shows "بيانات السداد" column with date, receipt number, and employee name. Model query now JOINs employees for paid_by_name display. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
feat(transfers): gender-based separation logic — males use work/grad/age25, females use marriage date - Males: effective_date = min(max(work_date, graduation_date), date_turned_25) — unchanged - Females: effective_date = marriage_date only (no work/graduation/age25 fields) - Add data-gender attribute to child select options for JS detection - Show male-specific fields (employment, graduation, work_date, date25) only for males - Show marriage_date field only for females - Females bypass age >= 25 filter in dropdown (eligible at any age via marriage) - Server-side age validation skipped for female children - Add marriage_date column to migration and model fillable - Frontend dynamically switches between male/female form based on selected child's gender Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
- Fix effective date: use min(max(work_date, graduation_date), date_turned_25) instead of min(work_date, date_turned_25) - Make graduation_date mandatory when child is employed - Annual subscription now includes all family members (member + spouses + children + temps) - Add dependents section to form: user specifies counts and details (name, national_id) for each person joining new membership - TransferProcessor creates dependent records (spouses, children, temporary_members) from notes JSON on completion - Migration adds target_spouses_count, target_children_count, target_temps_count columns - Updated architecture map Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Root causes of duplicate members in yearly subscriptions: 1. No DB-level unique constraint allowed race conditions between SubscriptionGenerator, SyncService, and RetroactiveMembershipService 2. SyncService set person_id=NULL for member rows vs Generator's person_id=memberId 3. RetroactiveMembershipService did blind INSERTs with no dedup check Fix: - Migration removes existing duplicates (keeps paid row, lowest ID tiebreak) - Normalizes NULL person_id on member rows - Adds UNIQUE INDEX (member_id, financial_year, person_type, person_id) - All insert paths catch Duplicate entry exceptions as race guard - SyncService now sets person_id=memberId matching Generator - RetroactiveMembershipService checks for existing row before INSERT Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Fares authored
Tests cover e(), money(), percentage(), arabic_date(), age_from_dob(), now(), and today() helpers. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 18 Jul, 2026 19 commits
-
-
Mahmoud Aglan authored
feat(transfers): skip annual subscription fee when member activated after July 1; fix bad financial_year data Business rule: members activated on/after July 1 of current FY have the annual subscription included in their membership fee — do not charge again. - SeparationFeeCalculator::isCurrentYearSubscriptionCovered(): returns true if activated_at >= July 1 of the member's current fiscal year - calculate() and calculateForChildSeparation(): set annual_subscription_fee to 0.00 when covered; return annual_sub_covered flag - create.php fee preview: shows "
✅ مشمول في العضوية" note when annual sub is waived; fee row shows 0.00 DB fix (applied directly): UPDATE subscriptions SET financial_year = '2025/2026' WHERE financial_year = '2025' — 168 rows corrected to proper YYYY/YYYY+1 format Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Generator now allows amt*count >= remaining (not forced equal). All cheques get the same amount; no last-cheque manipulation. Preview and guard alert only block when total < remaining. Server-side storeBatch already accepted grandTotal >= planTotal. Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
buildHiddenInputs now serializes cheque_amount as toFixed(2) string; renderTable shows amount as toFixed(2); prevents "المبلغ يجب أن يكون أكبر من صفر" error caused by JS sending 0 for the amount field Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- genPreview: compute lastAmt = remaining - amt*(needed-1) to show exact total; display grand total (existing + new) when cheques already exist; highlight preview red if lastAmt would be ≤ 0 - generateCheques: guard against per-cheque amount too large (lastAmt ≤ 0) with clear Arabic error showing max allowed per-cheque value; last cheque always = remaining - amt*(needed-1) so existing+new always = PLAN_TOTAL Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- ChequeService: add nextChequeNumberForPlan(planId) — returns next number scoped to the specific plan, not globally across all plans - ChequeController::index(): pass nextChequeNumForPlan to view - ChequeController::storeBatch(): load existing cheques before validation; check against existing numbers for duplicates; guard against exceeding requiredCount; coverage check uses existingTotal + batchTotal; only enforce full-coverage on the final batch - cheques.php JS: generator uses REMAINING_COUNT (not amount-math) for count, NEXT_NUM_FOR_PLAN for sequence start — correctly continues from cheque 6 if 5 already exist; preview shows "تكملة من #N" context note - cheques.php UI: yellow info banner when existing cheques present, showing count, remaining, and starting cheque number Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
The runner was calling splitStatements(string) directly on migration['up'] without checking whether it was a Closure, causing a fatal type error on all idempotent closure-based migrations (Phase_94_001, _002, Phase_96_001). Now checks is_callable() first and invokes the closure, falling back to string SQL splitting for plain-string migrations. Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
## Schema (Phase_96_001) - escalation_type / escalation_rate / escalation_tiers_json — flat or tiered annual rent increases - utilities_mode / utilities_rent_pct / utilities_facility_pct / facility_monthly_cost — support rent%, facility-cost%, or both - payment_due_day — configurable per-contract (default day 5) - late_fee_bank_rate — annual bank rate for daily penalty calculation - grace_period_months / early_termination_months — additional contract terms - Data migration: backfills utilities_rent_pct from utilities_percentage ## Service layer - RentalContractService: computes escalated total_amount across flat/tiered modes; handles all utilities modes; recalculates VAT and grand_total - RentalInvoiceService: calcBase() now escalation-aware (by period); bulkGenerate skips grace months and uses payment_due_day; calcLateFee supports bank-rate daily formula - Seeds RENTAL_LATE_FEE_BANK_RATE business rule (27.25% annual) ## UI - contract_form: new sections for escalation (dynamic tiers table), utilities mode, payment terms, extra contract conditions; year-by-year preview - contract_show: mode-aware utilities display, escalation card, grace/termination info Co-Authored-By:Claude Opus 4.8 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Installments: controller now computes pendingCount before passing to show view, fixing Undefined variable crash at show.php:4 - Transfers: add فصل أبناء employment-status workflow - Migration Phase_94_002 adds is_employed, graduation_date, work_date, date_turned_25, effective_transfer_date columns to transfer_requests - SeparationFeeCalculator: new calculateYearsFloor() (floor, never rounds up), computeChildSeparationDates() (effective = min(work_date, date_25) or date_25), calculateForChildSeparation() (uses floor years + current subscription price) - TransferController store(): reads employment fields, computes effective date, routes to new calculator for child_separation; saves all new fields - calculateFee API: supports transfer_type=child_separation with employment fields - create.php: new فصل أبناء section with employment radio, graduation date, work date (conditional), auto-computed date_turned_25, effective date display, live elapsed-years preview via AJAX Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Converts static ALTER TABLE to closure with information_schema checks per column so re-runs don't fail with "Duplicate column name". Co-Authored-By:Claude Opus 4.8 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- VAT % now reads default from RENTAL_VAT_PCT business rule (seeded at 1%); contract form pre-fills with live rule value instead of hardcoded 1 - Deposit row in contract_show now shows payment reference (receipt number) when deposit_payment_id is set - Bulk invoice generation: POST /contracts/{id}/invoices/bulk-generate generates all monthly invoices from start to end date, skipping existing; button added to page_actions and invoices table header with JS confirm Co-Authored-By:Claude Opus 4.8 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Contracts now store vat_percentage (1%), utilities_percentage, late_fee_type (none/daily/weekly/monthly), late_fee_rate, and grand_total - New rental_invoices table with per-invoice breakdown: base, utilities, VAT, late_fee, total; late fee is calculated at payment time based on days overdue - RentalInvoiceService handles generation, late-fee calc, and mark-paid - Accounting auto-posts on rental.invoice_paid: Dr. Cash, Cr. RentalRevenue (410521) + ServiceRevenue (410515) + TaxPayable (230804) + FineRevenue (410512) - contract_form has live preview calculator for monthly invoice totals - contract_show shows full financial breakdown and invoices table - Migrations: Phase_95_001 (alter contracts), Phase_95_002 (create invoices) Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Root cause: after WaiverProcessor::execute() completed, visiting /members/{id} triggered MembershipPaymentGuard::reconcile(), which did not recognise waiver_fee as a valid activation payment. It stripped the membership_number and then called deactivateAllDependents(), which crashed on spouses.join_date NOT NULL constraint. Fixes: - MembershipPaymentGuard::reconcile(): add waiver_fee path — looks up completed waiver_requests where target_member_id matches, preventing false deactivation - MembershipPaymentGuard::deactivateAllDependents(): spouses.join_date is NOT NULL; use sentinel date '1970-01-01' instead of NULL to avoid constraint violation - MembershipPaymentGuard::deactivateDependent(): same sentinel fix for spouses - WaiverProcessor::execute(): set activated_by_payment_id + activated_at on the target member so the existing fallback check in reconcile() catches future cases Also restored member #136 directly in DB (membership_number='1600', status='active', activated_by_payment_id=773) which was the live victim of this bug. Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- New status flow: board_review → board_approved → pending_form_fill → completed - Mandatory document uploads at case creation (death certificate + inheritance notice) - Board approval step: configurable trustee fee (% of membership_value or flat amount) - Payment request created only after board approval (not at case creation) - Cashier bootstrap fixed: death_fee for primary_member now sets pending_form_fill - Pre-completion validations: board approval, payment, both docs, wife form filled - Children transfer bug fixed: sweep remaining children to primary + renumber child_order - Source tracking: transferred_from_death_id on new member rows - Death-origin badge in member show page - Migration Phase_94_001: new columns on death_cases + members Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- AccountingIntegrationService: read payment_type key (dispatched) not type key (wrong fallback) that was silently routing every payment journal to the default catch-all account - InstallmentController: fire installment.plan_created (correct) not installment_plan.created so AR journal entry is created when a plan is made from the installments UI - AccountCodes: remap all constants to accounts that actually exist in chart_of_accounts DB - form_fee → 410103 (استمارات عضويات), membership_fee → 410101 (عضويات جديدة) - addition_fee → 410102 (إضافة عضويات) - installment → 410510 (الاقساط), down_payment → 410503 (مقدم عضويه) - waiver_fee/death_fee → 410515 (إيرادات متنوعه) - sports_registration/sa_form_fee → 410516 (استمارات نشاط) - fine → 410512 (غرامة تاخير), SERVICE_REVENUE → 410515 (exists, was 4110 which did not exist) - DB: corrected treasury 3 account_code from 12060103 (EUR label) to 12060101 (EGP) which was causing form_fee journals to debit the wrong sub-treasury cash account Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- InstallmentCalculator: calculateEarlySettlement() sums all pending principal and interest separately; settlement_amount = principal only, all interest waived - InstallmentController: earlySettlement() (confirmation page), processEarlySettlement() (executes: single PaymentService call, zeros interest on each settled row, marks plan completed + is_cash_settled=1, dispatches installment.early_settled), settlementReceipt() (print view) - Routes: GET/POST /installments/{id}/early-settlement, GET /installments/{id}/settlement-receipt/{receiptId} - show.php:⚡ تسوية مبكرة button (requires installment.pay permission, only when active + pending > 0) - early_settlement.php: breakdown table (original due / interest waived / principal to pay), pending items preview, mandatory confirmation checkbox, submit disabled until checked - settlement_receipt.php: print-ready receipt showing original balance, interest waived, amount paid, settled items list, amount in words, stamp/signature area - PaymentService: early_settlement payment type label - Architecture Map: section 5.6, new route rows, new event row Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- ChequeService: add nextChequeNumber() — sequential from last numeric cheque in DB - ChequeController: index() passes planTotal, uploadedTotal, nextChequeNum; storeBatch() validates total coverage + dedup + activates member; store() validates total coverage on final cheque - Routes: add POST /installments/{planId}/cheques/batch - cheques.php: full rewrite — KPI row, auto-generator panel (JS generates N editable rows from amount+bank+start-date), live total validation bar, submit disabled until total covered, editable-row table with per-row delete, existing cheques table with coverage status, single-upload form retained Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Add syncForMember(memberId) to SubscriptionSyncService: creates the member's own FY subscription row + syncs all currently-active dependents in one call. Uses same rate/discount/dedup logic as the batch generator. - Wire member.activated EventBus listener in Subscriptions bootstrap so any newly-activated member (and their dependents) gets a subscription row immediately, without waiting for the next annual batch generation. - Existing syncForDependent is unchanged; syncForMember delegates to it. Co-Authored-By:Claude Sonnet 4.6 <noreply@anthropic.com>
-