1. 18 Jul, 2026 15 commits
    • Mahmoud Aglan's avatar
      feat(rentals): add VAT, utilities, late fees, and monthly invoices · e6a79887
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      e6a79887
    • Mahmoud Aglan's avatar
      fix(waiver): prevent reconcile() from revoking membership after successful transfer · ba9d9f81
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      ba9d9f81
    • Mahmoud Aglan's avatar
      feat(death): full board-approval workflow, mandatory docs, trustee fee, children-transfer fix · 910e8606
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      910e8606
    • Mahmoud Aglan's avatar
      fix(accounting): align account codes with live chart of accounts and fix 4 journal-entry bugs · e6708441
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      e6708441
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
      feat(installments): add early settlement — full principal-only payoff with interest waiver · a64e13fa
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      a64e13fa
    • Mahmoud Aglan's avatar
      feat(installments): add cheque auto-generator with live validation · 69f09d80
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      69f09d80
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
      feat(subscriptions): auto-sync on member activation + syncForMember method · 724a37ca
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      724a37ca
    • Mahmoud Aglan's avatar
      fix(members): correct installment panel formula and add live preview · c492f45a
      Mahmoud Aglan authored
      - Installment panel in member show page now uses flat simple interest:
        remaining × (rate/100) × (months/12) instead of hardcoded 22% × 30mo
      - Live breakdown table updates in real-time as user changes down payment
        or months: shows سعر العضوية, المقدم, المبلغ المتبقي, الفائدة,
        الإجمالي مع الفائدة, القسط الشهري
      - Pass installInterestRate and installMaxMonths from RuleEngine to show view
      - Fix pay-membership action to respect RuleEngine max months (not hardcoded 30)
      Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      c492f45a
    • Mahmoud Aglan's avatar
      fix(installments): correct last-row rounding in recalculate action · b7d24084
      Mahmoud Aglan authored
      The last pending row was absorbing full rounding relative to
      (pendingCount-1), but the plan may have 30 paid rows already.
      Now computes the pending interest pool (total - paid interest),
      distributes flat per row, and absorbs only the pending rounding
      on the final pending row.
      Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      b7d24084
    • Mahmoud Aglan's avatar
      feat(installments): business-intelligence show page + recalculate action · 6beb4544
      Mahmoud Aglan authored
      - Complete rewrite of show.php: 6 KPI cards (original amount, down payment,
        remaining balance, interest, monthly payment, member's outstanding balance),
        progress bar showing paid vs total installments, financial breakdown panels
        (paid vs remaining split), grand total formula ribbon, overdue alerts,
        next-due-date card with countdown, enhanced schedule table with status
        highlighting and per-row pay forms
      - Add recalculate() action to InstallmentController: corrects pending-only
        installment rows to flat simple-interest formula without touching paid rows
      - Register POST /installments/{id}/recalculate route
      Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      6beb4544
    • Mahmoud Aglan's avatar
      feat(subscriptions): auto-sync FY subscription row when dependent is activated · 58084696
      Mahmoud Aglan authored
      SubscriptionSyncService::syncForDependent() inserts a pending subscription
      row for the current financial year whenever a spouse, child, or temporary
      member becomes active. Triggered via two paths in Subscriptions bootstrap:
      
      - Cashier path: spouse.fee_paid / child.fee_paid / temporary.fee_paid
      - Zero-fee path: *.added events where fee = 0 (immediate activation)
      
      Guards: member must be active + non-exempt type; dedup prevents duplicates.
      Rates resolved identically to SubscriptionGenerator (year-specific catalog
      code → generic code → hard fallback). Never throws — safe in event context.
      Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      58084696
    • Mahmoud Aglan's avatar
      fix(installments): correct interest formula to flat simple interest prorated by duration · 8b7ccbd6
      Mahmoud Aglan authored
      All calculation sites now use: interest = remaining × (rate/100) × (months/12)
      instead of diminishing-balance amortization.
      
      Changes across all 6 sites:
      - InstallmentCalculator: flat interest, equal monthly instalments
      - PricingEngine: same formula
      - RetroactiveMembershipService: same formula
      - retroactive-wizard JS: updated preview + shows المبلغ المتبقي in summary
      - Members/show.php: preview panel now includes months factor (was missing)
      - Installments/create.php: added live المبلغ المتبقي preview panel
      Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      8b7ccbd6
    • Mahmoud Aglan's avatar
      fix(members): add novalidate to retroactive wizard form to suppress browser validation errors · 0d8aafd7
      Mahmoud Aglan authored
      Native HTML5 constraint validation fires on hidden fields (display:none panels),
      causing "not focusable" errors on inst_months. Custom validateStep() handles all
      validation — novalidate disables the duplicate browser check.
      Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      0d8aafd7
  2. 09 Jul, 2026 1 commit
  3. 07 Jul, 2026 1 commit
  4. 06 Jul, 2026 1 commit
  5. 04 Jul, 2026 6 commits
  6. 03 Jul, 2026 9 commits
  7. 01 Jul, 2026 1 commit
    • Mahmoud Aglan's avatar
      feat(waiver): live target debt check on create page + require waiver doc · 99e7632a
      Mahmoud Aglan authored
      - Added GET /api/members/{id}/debts endpoint returning comprehensive
        debt check as JSON (clear, debts array, total)
      - Create page now fetches and displays target member debts live after
        selection via AJAX — shows detailed debt table or green "clear" badge
      - Form submission blocked if target has outstanding debts, with error
        message and direct link to payment page
      - Made waiver request document upload required (HTML validation)
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      99e7632a
  8. 28 Jun, 2026 1 commit
    • Mahmoud Aglan's avatar
      feat(waiver): auto-complete waiver after fee payment instead of manual approval · d92f1d38
      Mahmoud Aglan authored
      After the cashier collects the waiver fee, the system now automatically
      executes the waiver completion (membership transfer) without requiring
      a manual "إتمام التنازل" button click.
      
      The auto-complete validates all conditions first:
      - Fee paid (status = fee_paid)
      - Target member specified
      - Board approval exists
      - No debts on source or target member
      - Excess dependent fees properly set
      
      If any condition fails, the waiver stays in fee_paid status with a
      clear checklist showing what's still needed, plus a manual fallback
      button for edge cases.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      d92f1d38
  9. 26 Jun, 2026 4 commits
    • Mahmoud Aglan's avatar
      fix(waiver): handle empty date_of_birth strings in individual fee insert · 26838f17
      Mahmoud Aglan authored
      MySQL rejects empty strings for DATE columns. The approve form sends
      empty strings for persons without a DOB (spouses, temporary members).
      Convert empty strings to null for date_of_birth, relationship, status,
      and notes fields before INSERT.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      26838f17
    • Mahmoud Aglan's avatar
      fix(payments): actually update subscription/fine/installment records when paid from process page · 9044313e
      Mahmoud Aglan authored
      The generic payment process page (linked from waiver "الانتقال إلى السداد") was
      recording payments in the payments table but never updating the source records
      (subscriptions.paid_amount, fines.paid_amount, installment_schedule.paid_amount).
      This caused the debt check to keep showing debts as unpaid after payment.
      
      Added handleSubscriptionPayment, handleFinePayment, and handleInstallmentPayment
      methods that mark the underlying records as paid (oldest first).
      
      Also fixed waiver debt check using wrong fines status ('pending' instead of
      'imposed'/'appeal_upheld') which is what fines actually use.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      9044313e
    • Mahmoud Aglan's avatar
      feat(waiver): per-individual fee assignment + fix 404 payment button · c8fae48c
      Mahmoud Aglan authored
      - New table `waiver_individual_fees` stores fee per person (not per category)
      - Board approval screen shows each excess person as a separate card with:
        name, DOB, age, age category, relationship, independent fee type/rate
      - Children 25+ flagged with warning and "فصل العضوية" button
      - Live JS calculates per-person amounts and updates grand total instantly
      - Fee breakdown section shows individual names when individual fees exist
      - Fix: /members/{id}/financial → /payments/process/{id} (was 404)
      - WaiverProcessor::getExcessIndividuals() identifies the specific excess persons
      - WaiverProcessor::saveIndividualFees() persists per-person board decisions
      - Age categories expanded: under_12, 12_to_16, 16_to_18, 18_to_25, 25_plus
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      c8fae48c
    • Mahmoud Aglan's avatar
      feat(waiver): Round 2 UX — comprehensive debt check, children details, payment in target's name · 83f24d7a
      Mahmoud Aglan authored
      - checkDebtsComprehensive() returns per-person breakdown (name, type, debt_type, period, amount)
      - getDependentDetails() calculates age, DOB, age category for children
      - New sendToCashier route creates payment request in TARGET member's name (buyer pays)
      - Detailed receipt breakdown with both member names and per-category fees
      - show.php: per-person debt table, children comparison, status indicators, go-to-payment button
      - create.php: detailed debt display with person labels, children age table
      - Status flow: requested → approved → send to cashier → fee_paid → complete
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      83f24d7a
  10. 23 Jun, 2026 1 commit