Commit 31130c75 authored by Mahmoud Aglan's avatar Mahmoud Aglan

docs: update architecture maps for offer tiers and subscription rules

Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent cb63de72
# Cross-Module Dependency Graph
## Purpose
This graph maps how modules interact with each other — events, shared tables, data flows, and cascading changes. It answers: "If I change X, what else breaks?"
---
## Core Entity: `members` Table
The `members` table is the central hub. Nearly every module in the ERP references it.
### Direct Foreign Keys TO members
| Table | FK Column | Module |
|-------|-----------|--------|
| spouses | member_id | Spouses |
| children | member_id | Children |
| temporary_members | member_id | Temporary |
| subscriptions | member_id | Subscriptions |
| payments | member_id | Payments |
| payment_requests | member_id | Cashier |
| installment_plans | member_id | Installments |
| fines | member_id | Fines |
| violations | member_id | Fines |
| transfer_requests | source_member_id, target_member_id | Transfers |
| waiver_requests | source_member_id, target_member_id | Waiver |
| death_cases | member_id, transferred_to_member_id | Death |
| divorce_cases | member_id, spouse_new_member_id | Divorce |
| carnets | member_id | Carnets |
| receipts | member_id | Receipts |
| member_notes | member_id | Members |
| seasonal_memberships | member_id | Seasonal |
| honorary_members | member_id | Honorary |
| foreign_member_details | member_id | Foreign |
---
## Module → Module Dependencies
### Members Module
| Depends On | How |
|------------|-----|
| Rules (RuleEngine) | All fee calculations, limits, rates |
| Pricing (pricing_configs) | Membership value resolution |
| ServiceCatalog | Form fee price (SVC_NEW_FORM) |
| Branches | Branch selection, branch-specific pricing |
| Qualifications | Pricing tier determination |
| Workflow | New membership workflow instance |
| Forms | Form submission tracking |
| Cashier (PaymentRequestService) | Payment request creation |
### Subscriptions Module
| Depends On | How |
|------------|-----|
| Members | member_id FK, status checks, membership_type for exemptions |
| Spouses | person_type='spouse', active spouses list |
| Children | person_type='child', active children list |
| Temporary | person_type='temporary', active temps list |
| ServiceCatalog | Rate lookup (SVC_ANNUAL_MEMBER, SVC_ANNUAL_CHILD, etc.) |
| Rules (RuleEngine) | Discount percentages, fine rates, grace months, drop years |
| Payments | payment_id FK on paid subscriptions |
### Payments Module
| Depends On | How |
|------------|-----|
| Members | member_id FK |
| Receipts | receipt_id FK |
| Cashier (PaymentRequestService) | Payment request lifecycle |
| Installments | Plan creation on down_payment |
| MembershipPaymentGuard | Activation/deactivation on payment events |
| Workflow | Workflow state transitions on payment |
### Installments Module
| Depends On | How |
|------------|-----|
| Members | member_id, status transitions |
| Payments | payment_id per schedule item |
| Pricing (BoardOffer) | Offer-based terms (grace, interest override) |
| Rules (RuleEngine) | Default rates, min down, max months |
### Fines Module
| Depends On | How |
|------------|-----|
| Members | member_id, status changes (suspended/terminated) |
| Payments | Fine payment via payment_id |
| Cashier | Auto-queue monetary fines |
| Subscriptions | Overdue fine calculation |
### Children Module
| Depends On | How |
|------------|-----|
| Members | member_id FK, membership_value for fee calc |
| Pricing | Current price lookup for fee calculation |
| Rules (RuleEngine) | Fee percentages, age limits, included count |
| Cashier | Addition fee payment requests |
### Spouses Module
| Depends On | How |
|------------|-----|
| Members | member_id FK, membership_value, acquired_member flags |
| Pricing | Current price lookup for fee calculation |
| Rules (RuleEngine) | Fee percentages per order, annual fee, max spouse count |
| Cashier | Addition fee payment requests |
### Temporary Module
| Depends On | How |
|------------|-----|
| Members | member_id FK, membership_value |
| Rules (RuleEngine) | Fee percentages, category validation, age limits |
| Cashier | Addition fee payment requests |
### Waiver Module
| Depends On | How |
|------------|-----|
| Members | Source + target member records |
| Spouses/Children/Temporary | Dependent counts and comparison |
| Archive | Snapshot creation |
| Subscriptions | Cancel source subs, create target subs |
| Cashier | Waiver fee payment request |
| MembershipPaymentGuard | Target activation |
### Death Module
| Depends On | How |
|------------|-----|
| Members | Deceased member + new member creation |
| Spouses | Primary spouse becomes new member |
| Children | Child assignment to new member |
| Temporary | Temp assignment to new member |
| Archive | Snapshot, document transfer |
| Subscriptions | Transfer/create subscriptions for new member |
| Cashier | Death fee payment request |
| MembershipPaymentGuard | New member activation |
### Divorce Module
| Depends On | How |
|------------|-----|
| Members | Original member + new member creation |
| Spouses | Divorced spouse data → new member |
| Children | Child transfer to divorced spouse's new membership |
| Archive | Snapshot creation |
| Subscriptions | New member subscription creation |
| Cashier | Divorce fee payment |
| MemberNumberGenerator | New number for divorced spouse |
### Transfers Module
| Depends On | How |
|------------|-----|
| Members | Source + target member records |
| Children | Child being separated |
| Spouses | Spouse being transferred |
| Pricing | Separation fee = % of current pricing_configs price |
| Archive | Snapshot creation, number chain recording |
| Subscriptions | Sync after transfer completion |
| Cashier | Separation fee payment request |
| MemberNumberGenerator | New number assignment |
---
## Event Dependencies
### Events Dispatched → Listeners
| Event | Dispatched By | Listened By |
|-------|--------------|-------------|
| `member.created` | Members | — |
| `member.activated` | MembershipPaymentGuard | Subscriptions (syncForMember), Pricing (applyFreeSubscriptionBonus) |
| `member.dropped` | InstallmentDefaultJob, OverdueFineJob | — |
| `payment.completed` | PaymentService, RetroactiveMembershipService | Payments bootstrap (workflow transition) |
| `payment_request.completed` | Cashier | PaymentLifecycleService (activation logic) |
| `child.added` | Children | Subscriptions (syncForDependent if fee=0) |
| `child.fee_paid` | Cashier | Subscriptions (syncForDependent) |
| `spouse.added` | Spouses | Subscriptions (syncForDependent if fee=0) |
| `spouse.fee_paid` | Cashier | Subscriptions (syncForDependent) |
| `temporary.added` | Temporary | Subscriptions (syncForDependent if fee=0) |
| `temporary.fee_paid` | Cashier | Subscriptions (syncForDependent) |
| `transfer.completed` | TransferProcessor | Subscriptions (sync target, refresh source) |
| `death.completed` | DeathController | Subscriptions (refresh new member) |
| `divorce.completed` | DivorceController | — |
| `waiver.fee_paid` | Cashier | WaiverAutoCompleteJob (cron) |
---
## Database Dependencies (Shared Entities)
### `pricing_configs` (read by multiple modules)
- Members (fill-form: resolves membership_value)
- BillingService (working/sports bill calculation)
- Children/Spouses/Temporary (fee calculators use current price, NOT stored membership_value)
- Transfers (separation fee base amount)
- Divorce (fee base amount)
- Waiver (waiver fee base)
### `service_catalog` (rate lookup)
- Members (SVC_NEW_FORM for form fee)
- Subscriptions (SVC_ANNUAL_MEMBER, SVC_ANNUAL_SPOUSE, SVC_ANNUAL_CHILD, SVC_ANNUAL_TEMP, + year-specific variants)
- Transfers (FORM_TRANSFER_FEE)
### `system_config` (configuration store)
- MemberNumberGenerator (form_start_number, membership.number_start)
### `board_offers` (promotional pricing)
- Members/BillingService (show bill with offer)
- MemberController (payment request creation with offer snapshot)
- Installments (plan creation uses offer terms)
### `special_discounts`
- Members (assigned per member)
- BillingService (discount line item)
- Pricing/SpecialDiscountService (bonus free subscription years)
---
## Workflow Dependencies
### New Membership Workflow (workflow_definition: 'new_membership')
```
potential → under_review → [interview_scheduled] → accepted → payment_pending → active
```
- Created in MemberController::store()
- Transitions driven by PaymentLifecycleService on payment completion
- workflow_instance_id stored on members table
---
## Financial Year Dependencies
All modules that reference `financial_year`:
- Subscriptions (primary consumer)
- Members/MembershipRulesService (rate lookup)
- Members/MembershipValidationService (current FY subscription check)
- AutoFreezeService (subscription block check)
- OverdueFineApplicator (fine progression by year)
- SubscriptionGenerator (batch generation per year)
- MemberController::show() (current FY display)
**Critical**: FY starts July 1. Format: "2025/2026". Determined by `financial_year()` helper in `app/Core/Helpers.php`.
---
## Cascading Impact Analysis
### If `members.status` changes:
- Subscriptions: exempt types skip generation; active status required for dependent sync
- Payments: PaymentGuard validates status for activation
- Carnets: canPrintCarnet() checks status
- AccessMatrix: facility access gated by active status
- Installments: dropped status on default
### If `members.membership_value` changes:
- BillingService: recalculates bill
- Spouses/Children/Temporary fee calculators: use pricing_configs (NOT stored value), so impact is limited to display
- NOTE: Fee calculators use `pricing_configs` current price, not `members.membership_value`
### If `pricing_configs` changes:
- ALL new fee calculations use new prices (separation, waiver, divorce, child addition, spouse addition)
- Existing members' stored `membership_value` does NOT auto-update
- Impact: new members get new prices; existing member fees remain as calculated at time of creation
### If a dependent is added/removed:
- Subscriptions: syncForDependent creates/removes subscription rows
- BillingService: recalculates totals
- Transfers: dependent counts affect separation eligibility
- Waiver: dependent comparison between source and target
### If a payment is voided:
- MembershipPaymentGuard: may deactivate member + all dependents
- Installments: if down_payment voided, plan affected
- Subscriptions: if annual_subscription voided, status reverts
- Fines: if fine payment voided, fine status reverts to imposed
### If subscription rates change (RuleEngine/ServiceCatalog):
- Only affects NEXT year's generation (existing rows are immutable)
- SubscriptionDataMigration can retroactively fix amounts
### If an installment plan defaults:
- Member dropped immediately
- All pending schedule items marked overdue
- Carnet blocked, subscription generation skipped
---
## National ID Deduplication (Cross-Table Constraint)
National ID uniqueness is enforced ACROSS tables:
- `members.national_id` (unique index)
- `spouses.national_id` (checked against members + children)
- `children.national_id` (checked against members + spouses)
- `temporary_members.national_id` (checked against all above)
This is application-level validation, NOT database-level cross-table constraints.
---
## Permission Dependencies
### Shared Permission Patterns
- `member.*` → Members module
- `subscription.*` → Subscriptions module
- `installment.*` → Installments module
- `fine.*` → Fines module
- `transfer.*` / `separation.*` → Transfers module
- `waiver.*` → Waiver module
- `child.*` → Children module
- `spouse.*` → Spouses module
- `temp.*` → Temporary module
- `carnet.*` → Carnets module
- `payment.*` → Payments module
Super admin check: `employee_roles JOIN roles WHERE role_code = 'super_admin'`
# Members Module — Architecture Map
## Module Purpose
Core module of the club ERP. Manages the complete membership lifecycle from application to termination, including:
- Member registration and form submission
- Membership activation via payment
- Dependent management (spouses, children, temporary members)
- Status transitions (active, frozen, suspended, dropped, expired, terminated)
- Membership transfers (waiver, death, divorce, child separation)
- Financial obligations (form fee, membership value, annual subscriptions, fines)
- Carnet eligibility and access control
- Retroactive data entry for legacy members
## System Responsibilities
1. **Member Registration** — Create potential members with form numbers, validate NID, collect photos
2. **Form Fee Collection** — Gate form-filling behind form fee payment (505 EGP)
3. **Membership Pricing** — Calculate membership value based on branch + qualification (150K/225K/300K EGP)
4. **Payment Orchestration** — Send payment requests to Cashier module, handle activation on completion
5. **Activation Guard** — Single source of truth for member/dependent activation/deactivation
6. **Dependent Lifecycle** — Manage spouses, children, and temporary members with fee calculations
7. **Status Enforcement** — Block operations (carnet printing, subscriptions) based on member status
8. **Age Monitoring** — Auto-separate children at 25, expire temporary members at age limits
9. **Transfers** — Coordinate waiver, death, divorce, and separation transfers
10. **Reporting** — Age reports, subscription status, unpaid debts, children aging out
---
## File Structure
```
app/Modules/Members/
├── bootstrap.php — Menu + permissions registration
├── Routes.php — Web routes
├── ApiRoutes.php — API routes (player portal)
├── Controllers/
│ ├── MemberController.php — Main CRUD, payments, status changes
│ ├── MemberApiController.php — Internal API (NID parse, search, debts)
│ ├── Api/MemberApiV1Controller.php — External API v1
│ ├── MemberArchiveController.php — Archived members viewing
│ ├── ReportController.php — Reports (age, subscriptions, debts, waivers)
│ └── RetroactiveWizardController.php — Bulk retroactive member entry
├── Models/
│ ├── Member.php — Main model (table: members)
│ └── MemberNote.php — Notes model (table: member_notes)
├── Services/
│ ├── AutoFreezeService.php — Age-based freezing/separation/expiry
│ ├── BillingService.php — Calculate total bill (all items + dependents)
│ ├── BoardOfferService.php — Board discount offers (cash/installment)
│ ├── FormFeeService.php — Form fee calculations
│ ├── FormNumberGenerator.php — Form number sequencing
│ ├── MemberNumberGenerator.php — Membership number + form number assignment
│ ├── MemberSearchService.php — Unified search across members/dependents
│ ├── MembershipPaymentGuard.php — SOLE AUTHORITY for activation/deactivation
│ ├── MembershipRulesService.php — All business rules (fees, eligibility, penalties)
│ ├── MembershipValidationService.php — Validate membership for access (active + subscription paid)
│ ├── NationalIdParser.php — Parse Egyptian 14-digit NID (DOB, gender, governorate)
│ ├── RetroactiveMembershipService.php — Create members with historical data
│ └── WaiverService.php — Waiver fee calculation and request creation
└── Views/
├── index.php, create.php, show.php, edit.php
├── fill-form.php, search.php, changelog.php
├── insurance-record.php, retroactive-wizard.php
├── archive_index.php, archive_show.php
├── _partials/ (profile-header, family-tab, documents-tab, financial-summary, activity-timeline)
└── reports/ (index, age-report, children-aging, subscription-status, transfers, waivers, unpaid-debts)
```
---
## Database Schema (Primary Tables)
### `members` (main table)
| Column | Type | Notes |
|--------|------|-------|
| id | INT PK | |
| membership_number | VARCHAR UNIQUE | Assigned on activation |
| form_number | VARCHAR | Sequential at registration |
| form_date | DATE | |
| branch_id | INT FK→branches | |
| membership_type | ENUM | working/seasonal/sports/honorary/foreign |
| member_category | VARCHAR | |
| status | VARCHAR | See Status Flow below |
| full_name_ar / full_name_en | VARCHAR | |
| national_id | VARCHAR(14) UNIQUE | Egyptian NID |
| passport_number | VARCHAR | For foreigners |
| date_of_birth | DATE | |
| age_years / age_months | INT | |
| gender | VARCHAR | male/female |
| qualification_id | INT FK→qualifications | Determines pricing |
| membership_value | DECIMAL | Stored for reference |
| special_discount_id | INT FK→special_discounts | |
| discount_amount | DECIMAL | |
| payment_method | VARCHAR | cash/installment/check/visa/transfer |
| activated_by_payment_id | INT FK→payments | Links to activating payment |
| activated_at | DATETIME | |
| workflow_instance_id | INT FK→workflow_instances | |
| transferred_from_waiver_id | INT | |
| transferred_from_death_id | INT | |
| transferred_from_divorce_id | INT | |
| transferred_from_transfer_id | INT | |
| photo_path | VARCHAR | |
| is_archived | TINYINT | Soft delete |
| created_at / updated_at | DATETIME | |
| created_by | INT FK→employees | |
### Related Tables
- `spouses` — FK member_id, spouse_order, addition_fee, status, activated_by_payment_id
- `children` — FK member_id, child_order, classification, addition_fee, is_frozen, frozen_reason
- `temporary_members` — FK member_id, category, addition_fee, relationship_to_member
- `member_notes` — FK member_id, note text, created_by
- `subscriptions` — FK member_id, financial_year, person_type, person_id, status
- `payments` — FK member_id, payment_type, amount, receipt_id
- `payment_requests` — FK member_id, payment_type, status (pending→processing→completed)
- `installment_plans` — FK member_id, total_amount, down_payment, monthly_payment
- `installment_schedule` — FK installment_plan_id, installment_number, status
- `fines` — FK member_id, fine_type, penalty_type, amount, status
- `violations` — FK member_id, description, reported_by
- `waiver_requests` — source_member_id, target_member_id, status
- `death_cases` — FK member_id, transferred_to_member_id
- `transfer_requests` — source_member_id, child_id, target_member_id
- `divorce_cases` — FK member_id/original_member_id
- `board_offers` — is_active, effective_from/to, cash_discount_type/value
- `board_offer_tiers` — FK board_offer_id, tier_order, payment_type (cash/installment), cash_discount_pct, down_payment_pct, months, interest_rate
- `pricing_configs` — branch_id, qualification_id, membership_type, price
---
## Status Flow (Member Lifecycle)
```
potential → under_review → interview_scheduled → accepted → payment_pending → active
→ rejected
→ payment_pending → pending_cheques → active
→ dropped (installment default)
active → frozen (manual/auto)
active → suspended (violation)
active → dropped (5yr unpaid / installment default / board decision)
active → expired (honorary/seasonal end)
active → terminated (board decision / condition lost)
frozen → active (reinstatement)
dropped → active (within 1 year + board approval + payment)
```
### Activation Rules (MembershipPaymentGuard)
- **ONLY** `MembershipPaymentGuard::activateMember()` can set status = 'active'
- Requires non-voided payment of type: membership_fee, down_payment, foreign_membership_fee, sports_membership_fee, seasonal_fee
- Also recognizes: death_fee (death transfer), waiver_fee (waiver transfer), separation_fee
- On activation: assigns membership_number, sets activated_at + activated_by_payment_id
- On deactivation: clears membership_number, reverts to payment_pending, cascades to all dependents
### Dependent Activation
- Dependents are "included" (activated with parent's membership payment) OR "separate fee" (need own addition_fee)
- `activateIncludedDependents()` — auto-activates dependents without separate payment requests
- `activateDependent()` — activates specific dependent after their own fee is paid
---
## Business Rules (MembershipRulesService)
### Membership Types
| Type | Min Age | Dependents? | Pricing |
|------|---------|-------------|---------|
| working | 21 | Yes | By qualification (150K/225K/300K) |
| seasonal | 0 | Yes | Duration × rate × nationality |
| sports | 0 | Yes | 50% of working price |
| honorary | 0 | Yes | Free (board approval) |
| foreign | 21 | Yes | $10,000 USD |
### Fee Schedule
- **Form fee**: 505 EGP (500 form + 5 martyrs stamp), from ServiceCatalog SVC_NEW_FORM
- **Addition form fee**: 570 EGP + annual subscription (for adding dependents post-creation)
- **1st spouse**: Free (included)
- **2nd spouse**: 10% of membership value + 150/year retro
- **3rd spouse**: 20% + 200/year retro
- **4th+ spouse**: 30% + 300/year retro
- **First 3 children under 18**: Free (included)
- **4th child under 18**: 5% of membership value
- **Child 18**: 10%, **19**: 15%, **20**: 20%, **21+**: 15% (temporary until 25)
- **Temporary members**: 10% of membership value (default), 5% for nanny
- **Separation fee**: Tiered 30%→20%→15%→10%→5%→2.5% based on years since addition
### Annual Subscription Rates (Financial Year July→June)
| Year | Member | Spouse | Child | Temp | Dev Fee |
|------|--------|--------|-------|------|---------|
| 2023/2024 | 410 | 410 | 185 | 185 | 35 |
| 2024/2025 | 410 | 410 | 185 | 185 | 35 |
| 2025/2026+ | 492 | 492 | 222 | 222 | 35 |
### Installment Terms
- Min down payment: 25%
- Max months: 30
- Interest: 22% annual (flat)
- Default: 3+ items overdue by 90+ days → plan defaulted → member dropped
### Penalties & Violations
- Levels: attention → warning → fine (1K-10K) → suspension (≤6mo) → ban (≤6mo) → expulsion
- Appeal deadline: 15 days from penalty
- Fines accumulate for max 5 years → then membership dropped
- Reinstatement: within 1 year of drop, requires full payment + board approval
### Carnet Printing Eligibility
- Member status must be active (not frozen/suspended/dropped)
- Annual subscription paid for current FY
- No overdue installments
- No unpaid fines
---
## Cron Jobs (Automated Processes)
| Job | Schedule | Action |
|-----|----------|--------|
| AgeMonitorJob | Daily | Update children ages; alert at 18/25; auto-separate at 25; remove temps at 25 |
| SubscriptionGeneratorJob | July 1-7 | Generate annual subscriptions for all active members |
| OverdueFineJob | Daily | Mark past-FY subscriptions overdue; apply fines; drop members (5yr rule) |
| InstallmentDefaultJob | Daily | Default plans with 3+ items 90+ days overdue; drop member |
| HonoraryExpiryJob | Daily | Flag honorary memberships past end_date |
| SeasonalExpiryJob | Daily | Expire seasonal memberships past end_date |
| FormExpiryJob | Daily | Expire stale form submissions |
| WaiverAutoCompleteJob | Daily | Auto-complete waivers with fee_paid status |
---
## Event System
### Events Emitted
- `member.created` — after member insert (with retroactive flag if applicable)
- `member.dropped` — after member dropped (installment default / unpaid 5yr)
- `payment.completed` — after retroactive payment creation
### Events Consumed (via bootstrap.php)
- Listens via other module bootstraps (Accounting posts journal entries on payment events)
---
## Key Workflows
### 1. New Member Registration
```
1. Employee creates member (name, NID, phone, branch, type, photo) → status: potential
2. Pay form fee (505 EGP) → payment_request to Cashier
3. Cashier completes form_fee payment
4. Employee fills full form (qualification, address, employment) → status: under_review
5. [Optional] Interview → status: accepted
6. Pay membership value (cash/installment) → payment_request to Cashier
7. Cashier completes membership_fee/down_payment → MembershipPaymentGuard::activateMember()
8. Member assigned membership_number → status: active
9. Included dependents auto-activated
```
### 2. Dependent Addition (Post-Creation)
```
1. Add spouse/child/temp via form on member profile
2. System calculates addition_fee based on rules
3. Pay addition_fee → payment_request to Cashier
4. Cashier completes → MembershipPaymentGuard::activateDependent()
5. Dependent status → active
```
### 3. Child Separation (Age 25 / Voluntary)
```
1. AgeMonitorJob auto-separates male children at 25 OR
2. Manual transfer request created with separation fee calculation
3. Fee = tiered % of current membership value + form fee + annual subscription
4. Payment of separation_fee
5. New member created with transferred_from_transfer_id
6. Original child marked separated/frozen
```
### 4. Waiver Transfer
```
1. Source member requests waiver → WaiverService::processWaiver()
2. Board approval required
3. Fee = 30% of membership value
4. Target pays waiver_fee → Cashier
5. WaiverAutoCompleteJob processes: archives source, creates target with same number
6. Target activated via MembershipPaymentGuard
```
### 5. Death Transfer
```
1. Death case created for member
2. Membership transfers to spouse (same number, form fee + subscription only)
3. Payment of death_fee
4. New member created with transferred_from_death_id
5. Activated by MembershipPaymentGuard::reconcile()
```
### 6. Annual Subscription Cycle
```
1. July 1-7: SubscriptionGeneratorJob creates subscription records for all active members + dependents
2. Members pay via Cashier (subscription + dev fee)
3. Overdue after FY ends without payment
4. 5 consecutive unpaid years → membership dropped
5. Carnet printing blocked if current FY unpaid
```
### 7. Retroactive Entry
```
1. Super admin opens retroactive wizard
2. Enters member + dependents + payments + subscriptions + violations
3. All created in single transaction with historical dates
4. Payments create receipts; subscriptions auto-dedup by FY + person
```
---
## Integration Points
### Upstream Dependencies (consumed by Members)
| Module | Purpose |
|--------|---------|
| Rules (RuleEngine) | All configurable fees, rates, limits |
| Pricing | pricing_configs table → membership_value |
| ServiceCatalog | SVC_NEW_FORM price |
| Branches | Branch selection, branch-specific offers |
| Qualifications | Qualification-based pricing tiers |
### Downstream Dependencies (consumed from Members)
| Module | Purpose |
|--------|---------|
| Subscriptions | Annual subscription generation per member+dependents |
| Payments | Payment records, receipt generation |
| Cashier | PaymentRequestService → payment workflow |
| Installments | Installment plan management |
| Fines | Violation fines, overdue penalties |
| Carnets | Carnet eligibility check via canPrintCarnet() |
| Transfers | Separation/transfer request management |
| Waiver | Waiver processing and auto-completion |
| Death | Death case management and membership transfer |
| Divorce | Divorce-triggered membership transfer |
| Accounting | Journal entries on payment events |
| Notifications | SMS alerts for age milestones, overdue payments |
| Workflow | New membership workflow instance |
| Forms | Form submission tracking |
| Archive | Source data preservation on transfers |
| AccessMatrix | Membership validation for facility access |
| Children | Child records management |
| Spouses | Spouse records management |
| Temporary | Temporary member records |
| Foreign | Foreign member details (USD fee, exchange rate) |
| Honorary | Honorary member details (term dates) |
| Seasonal | Seasonal membership details (duration, nationality pricing) |
| Sports | Sports membership conversion details |
---
## Risk Areas
1. **MembershipPaymentGuard is the ONLY activation authority** — any bypass creates orphan states
2. **reconcile() runs on every show page** — performance concern with many members
3. **BillingService** queries across 6+ tables on every page load — no caching
4. **Installment default** → member drop is irreversible without board intervention
5. **Age monitoring** — if cron misses a day, children past 25 stay active until next run
6. **Financial year boundary** (July 1) — subscription generation only runs July 1-7
7. **Race condition** on membership_number assignment — handled with 3 retries but no locking
8. **Retroactive wizard** creates payments without Cashier flow — can mismatch accounting
9. **spouses.join_date is NOT NULL** — uses '1970-01-01' sentinel on deactivation
---
## Technical Debt
- No test coverage (no test framework configured)
- BillingService has 800 lines with significant duplication across membership types
- MemberController::show() loads 20+ queries per page view
- reconcile() on every page view is a hidden migration — should be event-driven
- Multiple financial year calculation functions duplicated across services
- Board offer logic tightly coupled to payment request creation
# Subscriptions Module — Architecture Map
## Module Purpose
Manages annual subscription billing for all active members and their dependents. Generates subscription records per financial year, tracks payment status, calculates late fines, and enforces the 5-year drop rule.
## System Responsibilities
1. **Annual Generation** — Create subscription rows for all active members + dependents each July
2. **Rate Resolution** — Look up year-specific rates from ServiceCatalog with RuleEngine fallback
3. **Payment Tracking** — Link subscriptions to payments when collected
4. **Overdue Management** — Mark unpaid past-FY subscriptions as overdue
5. **Late Fine Calculation** — Progressive fines for consecutive unpaid years
6. **Membership Drop** — Auto-drop members with 5+ consecutive unpaid years
7. **Reinstatement Window** — Track 12-month window for dropped member recovery
8. **Sync on Activation** — Auto-create subscription when member/dependent activated mid-year
9. **Transfer Sync** — Refresh subscriptions after transfers (remove separated, add new)
10. **Exemptions** — Skip generation for honorary and seasonal membership types
---
## File Structure
```
app/Modules/Subscriptions/
├── bootstrap.php — Event listeners for sync
├── Routes.php — 7 routes
├── Controllers/
│ └── SubscriptionController.php — CRUD, batch generate, pay, exempt
├── Models/
│ └── Subscription.php — Table: subscriptions
├── Services/
│ ├── SubscriptionGenerator.php — Batch generation for FY
│ ├── SubscriptionSyncService.php — Event-driven sync (member/dependent activation)
│ ├── SubscriptionCalculator.php — Late fine calculation, reinstatement check
│ ├── OverdueFineApplicator.php — Batch fine application + drop logic
│ └── SubscriptionDataMigration.php — One-time data repair tool
└── Views/
├── index.php — All subscriptions listing
├── batch-generate.php — Batch generation form
└── member-subscriptions.php — Per-member subscription view
```
---
## Database Schema
### `subscriptions` Table
| Column | Type | Notes |
|--------|------|-------|
| id | BIGINT UNSIGNED PK | Auto-increment |
| member_id | BIGINT UNSIGNED FK | References members(id) |
| financial_year | VARCHAR(10) | Format: "2025/2026" |
| person_type | VARCHAR(50) | 'member', 'spouse', 'child', 'temporary' |
| person_id | BIGINT UNSIGNED NULL | PK in respective table |
| person_name | VARCHAR(200) NULL | Denormalized for display |
| base_amount | DECIMAL(15,2) | Before discount |
| development_fee | DECIMAL(15,2) | Only on member row (35 EGP) |
| discount_amount | DECIMAL(15,2) | Year-specific discount |
| total_amount | DECIMAL(15,2) | base - discount |
| paid_amount | DECIMAL(15,2) | Actual paid (includes dev fee) |
| fine_amount | DECIMAL(15,2) | Late fine |
| status | VARCHAR(50) | pending/overdue/paid/exempt |
| paid_at | TIMESTAMP NULL | |
| payment_id | BIGINT UNSIGNED NULL FK | References payments(id) |
| receipt_number | VARCHAR(50) NULL | |
| paid_by | BIGINT UNSIGNED NULL | Employee |
| exempted_by | BIGINT UNSIGNED NULL | Employee who exempted |
| exemption_reason | TEXT NULL | |
| created_at, updated_at | TIMESTAMP | |
| created_by, updated_by | BIGINT UNSIGNED NULL | |
**Unique Index**: `uq_subscription_member_year_person (member_id, financial_year, person_type, person_id)`
### Related Tables
- `subscription_rate_overrides` — Per-membership-type rate/exemption overrides
- `service_catalog` — Rate source (SVC_ANNUAL_MEMBER, SVC_ANNUAL_SPOUSE, etc.)
---
## Financial Year Logic
- Starts July 1 (configurable via `app.financial_year_start_month`)
- Format: "2025/2026"
- Determined by `financial_year()` helper in `app/Core/Helpers.php`
- Subscription generation runs July 1-7 via cron
---
## Annual Rates (2025/2026+)
| Person Type | Base Amount | Dev Fee | Total |
|-------------|------------|---------|-------|
| Member | 492 | 35 | 527 |
| Spouse | 492 | 0 | 492 |
| Child | 222 | 0 | 222 |
| Temporary | 222 | 0 | 222 |
Rates resolved via: `service_catalog` (year-specific codes like `SVC_ANNUAL_MEMBER_2026`) → generic code (`SVC_ANNUAL_MEMBER`) → hardcoded defaults.
---
## Key Workflows
### 1. Annual Batch Generation (SubscriptionGenerator)
```
1. SubscriptionGeneratorJob runs July 1-7
2. Checks if rows exist for new FY (dedup guard)
3. Resolves rates from ServiceCatalog
4. Resolves exempt types from subscription_rate_overrides (default: honorary, seasonal)
5. Selects all active non-archived non-exempt members
6. SKIP members whose activated_at >= FY start (first-year, membership fee covers them)
7. For each eligible member:
a. Create member subscription row (status = 'pending')
b. Create spouse rows — SKIP if created_at >= FY start (first-year)
c. Create child rows — SKIP if created_at >= FY start (first-year)
d. Create temp rows — SKIP if created_at >= FY start (first-year)
8. All rows created with status = 'pending' (no more auto-'paid')
```
### 2. Event-Driven Sync (SubscriptionSyncService)
```
Events:
- member.activated → syncForMember() — SKIPPED for first-year members (activated_at >= FY start)
- spouse.fee_paid / child.fee_paid / temporary.fee_paid → syncForDependent() — SKIPPED for first-year (created_at >= FY start)
- spouse.added / child.added / temporary.added → syncForDependent() (only if fee=0) — SKIPPED for first-year
- transfer.completed → sync target member, mark current year paid, refresh source
- death.completed → refresh new member
Guards:
- Member must be active
- Membership type must NOT be exempt
- Member must NOT be waiver-acquired (transferred_from_waiver_id set)
- Dedup: skip if row already exists for (member_id, FY, person_type, person_id)
```
### 3. Overdue Fine Application (OverdueFineApplicator)
```
1. OverdueFineJob runs daily
2. Marks all unpaid subscriptions from previous FYs as 'overdue'
3. For each active member with overdue subscriptions:
a. Count consecutive unpaid years
b. Calculate fine per year (progressive: LATE_SUB_FINE_YEAR_1 through _N)
c. Apply fine to subscription row's fine_amount
d. If consecutive_years >= LATE_SUB_DROP_YEARS (default 5): drop member
4. expireReinstatements(): after 12 months, 'dropped' → 'permanently_dropped'
```
### 4. Payment Collection
```
1. PaymentService processes 'annual_subscription' payment
2. Subscription row updated: status='paid', paid_amount, paid_at, payment_id
3. Multiple rows can be paid in one batch (whole family for one year)
```
---
## Integration Points
### Events Listened (bootstrap.php)
| Event | Handler | Purpose |
|-------|---------|---------|
| member.activated | syncForMember | Create FY subscription for member + dependents |
| spouse.fee_paid | syncForDependent | Create spouse subscription row |
| child.fee_paid | syncForDependent | Create child subscription row |
| temporary.fee_paid | syncForDependent | Create temp subscription row |
| spouse.added | syncForDependent (if fee=0) | Immediate subscription for free additions |
| child.added | syncForDependent (if fee=0) | Immediate subscription for free additions |
| temporary.added | syncForDependent (if fee=0) | Immediate subscription for free additions |
| transfer.completed | sync target + refresh source | Reconcile after transfer |
| death.completed | refresh new member | Create subscriptions for inherited membership |
### Consumed By (Downstream)
| Module | How |
|--------|-----|
| Members/MembershipValidationService | Checks paid subscription for access validation |
| Members/AutoFreezeService | checkSubscriptionBlock() queries subscriptions |
| Members/MembershipRulesService | canPrintCarnet() checks annual payment |
| Carnets | Carnet printing requires paid current FY subscription |
| AccessMatrix | Facility access gated by subscription status |
| OverdueFineJob | Marks overdue, applies fines, drops members |
| MemberController::show() | Displays subscription status + overdue list |
---
## Business Rules
1. **One row per person per year** — enforced by unique index
2. **Dev fee only on member row** — not on dependents
3. **Exempt types skip generation** — honorary, seasonal (configurable)
4. **First-year members have NO subscription rows** — membership fee covers current FY; no rows created until next July
5. **First-year dependents have NO subscription rows** — addition fee covers current FY; no rows created until next July
6. **Waiver-acquired members skip initial sync** — their subscriptions handled separately
7. **All-or-nothing family payment** — subscription payment always covers ALL family members for the year (no individual row payment)
8. **Validation bypass for first-year** — MembershipValidationService, canPrintCarnet, checkSubscriptionBlock all bypass for members activated in current FY
9. **Grace period** — SUB_GRACE_MONTHS (default 3) before overdue fines start
10. **Progressive fines** — increase per consecutive unpaid year
11. **5-year drop rule** — 5 consecutive unpaid years = membership dropped
12. **Reinstatement window** — 12 months after drop to recover (requires board + full payment)
13. **Transfer handling** — target member gets current year marked paid; separated dependents' unpaid rows deleted
---
## Risk Areas
1. **SubscriptionGeneratorJob only runs July 1-7** — if missed, no subscriptions for the year until manual batch
2. **Dedup relies on unique index** — catches races but logs warnings
3. **fine_amount on subscription row** — not in a separate fines table, harder to audit
4. **syncForMember skips waiver-acquired** — potential gap if waiver logic doesn't create subs
5. **refreshForMember deletes unpaid rows** — if a dependent was incorrectly removed, subscription data is lost
6. **No partial payment** — paid_amount is either 0 or full amount
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment