# 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
