1. 22 Jul, 2026 7 commits
    • Fares's avatar
      fix(waiver): remove 'الانتقال إلى السداد' buttons from all waiver views · 08335cb4
      Fares authored
      Debt tables still show for reference but the payment navigation button
      is removed per user request — from create, show, and JS debt checker.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      08335cb4
    • Fares's avatar
      feat(waiver): transfer all data to target member, preserve full archive · 619c24d2
      Fares authored
      WaiverProcessor now transfers ALL active data (dependents, payments,
      subscriptions, installments, fines, documents) from source to target
      member during waiver execution. Snapshot is taken first so archive
      retains the complete historical state.
      
      Changes:
      - WaiverProcessor::transferActiveData() moves spouses, children, temps,
        payments, subscriptions, installment_plans, fines, documents, and
        payment_requests from source to target member
      - ArchiveService snapshot now includes installment_cheques and
        installment_schedule for complete financial history
      - Archive show page displays cheques per installment plan
      - Member show page adds "عرض العضوية المؤرشفة" button for waiver transfers
      - Archive page adds "الانتقال إلى العضوية الحالية" link to current holder
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      619c24d2
    • Fares's avatar
      fix(death): add form_number to DeathCase model fillable array · 19806b6c
      Fares authored
      form_number was being stripped by the model's mass-assignment guard,
      so it was never saved to the database despite being in the POST data.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      19806b6c
    • Fares's avatar
      fix(cashier): show death case form_number by falling back to death_cases table · c10fa4a7
      Fares authored
      The form_number wasn't showing because earlier payment requests had notes
      without form_number. Now both cashier process and queue views fall back to
      reading form_number directly from death_cases table when not in notes JSON.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      c10fa4a7
    • Fares's avatar
      feat(death): collect form number at start, show in receipt, auto-complete after form fill · 537ed628
      Fares authored
      - Form number input added to death registration (shown for primary_member type)
      - Stored in death_cases.form_number column (new migration Phase_99_002)
      - Passed through payment request notes JSON to cashier
      - Cashier process/queue views now display form_number from notes for death_fee
      - saveFillForm() auto-triggers complete() — no manual button needed
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      537ed628
    • Fares's avatar
      fix(archive): show complete member data on archived membership page · 6f55a167
      Fares authored
      The archive show page now displays ALL original data:
      - Dependents from snapshot (not live DB where they may have been moved)
      - Full payments history with amounts and receipt numbers
      - All annual subscriptions with status and amounts
      - Installment plans with expandable payment schedules
      - Fines with amounts and reasons
      - Documents with file details
      - Financial summary box (membership value + total paid)
      - Insurance record link for death case audit trail
      
      Previously, dependents showed empty because children/temps are moved
      during death transfer. Now uses snapshot related_data as source of truth.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      6f55a167
    • Fares's avatar
      feat(death): complete overhaul of death case execution process · 21595929
      Fares authored
      - Add DeathAuditService for full Arabic audit logging of every step
      - Create death_case_audit_log table (migration Phase_99_001)
      - Transfer active installment plans to new membership on completion
      - Duplicate documents to new membership (originals preserved on archived)
      - Create annual subscriptions for new membership (current financial year)
      - Set membership_value to current plan price (not old historical value)
      - Add "عرض العضوية الأصلية" button linking to archived member
      - Add "السجل التأميني" (Insurance Record) page on member profile
      - Add insurance-record route and view with timeline UI
      - Update architecture map with new services, tables, and flows
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      21595929
  2. 21 Jul, 2026 21 commits
    • Fares's avatar
      fix(cashier): show wife's name (recipient) for death_fee payments, not deceased · 8f9a4b37
      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: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      8f9a4b37
    • 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
  3. 18 Jul, 2026 12 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