1. 21 Jul, 2026 6 commits
    • Fares's avatar
      fix(waiver): fix 4 issues after waiver completion · 2e4f8575
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      2e4f8575
    • Fares's avatar
      feat(subscriptions): add per-row payment audit trail (receipt_number, paid_by, paid_at) · 0a2372df
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      0a2372df
    • Fares's avatar
      feat(transfers): gender-based separation logic — males use work/grad/age25,... · 316c58eb
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      316c58eb
    • Fares's avatar
      feat(transfers): complete child separation with dependents, fix effective date logic · 1c8ee16d
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      1c8ee16d
    • Fares's avatar
      fix(subscriptions): prevent duplicate subscription rows with unique constraint · 5a5aba46
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      5a5aba46
    • Fares's avatar
      test(core): add unit tests for global helper functions · a8c964ee
      Fares authored
      Tests cover e(), money(), percentage(), arabic_date(), age_from_dob(), now(), and today() helpers.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      a8c964ee
  2. 18 Jul, 2026 25 commits
    • Mahmoud Aglan's avatar
      feat(transfers): skip annual subscription fee when member activated after July... · ee47e057
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      ee47e057
    • Mahmoud Aglan's avatar
      fix(installments): allow cheque total to exceed installment amount · 2e51bbb2
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      2e51bbb2
    • Mahmoud Aglan's avatar
      fix(installments): ensure cheque_amount always sent as decimal, never 0 · 1ec0c056
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      1ec0c056
    • Mahmoud Aglan's avatar
      fix(installments): guarantee new cheques exactly cover remaining installment amount · d46bd960
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      d46bd960
    • Mahmoud Aglan's avatar
      feat(installments): smart cheque generator continues from existing cheques · 7b060cb6
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      7b060cb6
    • Mahmoud Aglan's avatar
      fix(migrations): support Closure-based 'up'/'down' in MigrationRunner · 0bcd5891
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      0bcd5891
    • Mahmoud Aglan's avatar
      feat(rentals): escalation tiers, dual utilities modes, bank-rate late fee, grace period · 8a66d739
      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: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      8a66d739
    • Mahmoud Aglan's avatar
      fix(installments): pass pendingCount to show view; feat(transfers): فصل أبناء employment workflow · 8ae05b0b
      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: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
      8ae05b0b
    • Mahmoud Aglan's avatar
      fix(migrations): make Phase_94_001 idempotent — skip existing columns · 8b5a876b
      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: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      8b5a876b
    • Mahmoud Aglan's avatar
      feat(rentals): configurable VAT, deposit payment ref, bulk invoice generation · c5610491
      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: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      c5610491
    • 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
  3. 09 Jul, 2026 1 commit
  4. 07 Jul, 2026 1 commit
  5. 06 Jul, 2026 1 commit
  6. 04 Jul, 2026 6 commits