1. 21 Jul, 2026 20 commits
    • Fares's avatar
      fix(death): trustee fee percentage now reliably uses current plan price · 447dfbce
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      447dfbce
    • Fares's avatar
      fix(transfers): child separation gives NEW number, parent keeps theirs · 93215a48
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      93215a48
    • Fares's avatar
      feat(transfers): enhance child separation dependent inputs with full fields · 9ae45ad7
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      9ae45ad7
    • Fares's avatar
      fix(transfers): add missing marriage_date column to transfer_requests · ffbaa774
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      ffbaa774
    • Fares's avatar
      fix(members): prevent empty string for decimal addition_fee in retroactive wizard · a4e9cc61
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      a4e9cc61
    • Fares's avatar
      docs(death): update architecture map with corrected fee logic · 9d2a1e86
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      9d2a1e86
    • Fares's avatar
      fix(death): exclude deceased from annual sub; use current plan price for trustee fee · d48cc140
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      d48cc140
    • Fares's avatar
      fix(transfers): resolve NOT NULL field errors during child separation completion · a5b513bb
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      a5b513bb
    • Fares's avatar
    • Fares's avatar
      feat(death,waiver): enhance death module with 6 changes; fix waiver post-completion issues · e4340b36
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      e4340b36
    • Fares's avatar
      feat(pricing): enhance special discounts with conditions, bonuses, and date ranges · 9a8823b4
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      9a8823b4
    • Fares's avatar
      fix(transfers): include date_of_birth when creating dependent children · e0e2fdd8
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      e0e2fdd8
    • Fares's avatar
      fix(transfers): handle missing date_of_birth when completing transfer · 67907b67
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      67907b67
    • Fares's avatar
      fix(transfers): fix annual subscription calculation and marriage_date column error · 705bbcbe
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      705bbcbe
    • 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 20 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