# CC Soccer D11 - Migration Strategy

**Purpose:** Document the migration plan from Drupal 7 to Drupal 11

**Migration Timeline:** Summer 2025 (during season break, before August start)

**Last Updated:** March 16, 2026

---

## Migration Philosophy

**Clean Slate Approach (with lightweight registration history):**
- Migrate active user data, credits, and payment methods
- Migrate lightweight registration history (user + season + date only, not full 25-field records)
- New seasons start fresh in D11 (no teams, games, schedules)
- Preserve user accounts, preferences, credits, and payment methods
- Users log in to new site with existing credentials and continue as normal

**Why Clean Slate (mostly):**
- Simpler migration (less data, less complexity)
- Fresh start with better architecture
- Old seasons/games/teams don't inform new seasons
- But registration history IS operationally needed (see item 4 below)

---

## Migration Scope

### ✅ MIGRATE

#### 1. Users
**Filter:** Last login within X years (PENDING BOARD DECISION: 2-year? 3-year? 5-year?)

**Rationale:** 
- 5 years = generous, includes people who may have moved on
- 3 years = balanced, allows for people who skip a year or two  
- 2 years = aggressive cleanup, only very recent users

**D7 Source:** `users` table + `field_data_field_*` tables  
**D11 Target:** User entity with custom fields  
**Estimated Count:** 
- 2-year: ~1,000-1,500 users
- 3-year: ~1,500-2,000 users
- 5-year: ~2,200-2,500 users
(from 2,678 total)

#### 2. Active Credits
**Filter:** Expiration date > migration date

**D7 Source:** `node` (type: season_credit) + field tables  
**D11 Target:** Credits entity  
**Estimated Count:** ~20-40 credits (from 66 total)

#### 3. Lightweight Registration History (NEW - Feb 2026)
**Filter:** All registrations for migrated users

**Rationale:** Registration history is needed for:
- **Automated registration reminders** — "Send to people who played last season but haven't registered yet" requires knowing who was registered for previous seasons
- **Returning player identification** — Distinguishing between "signed up for account but never played" vs "played 3 seasons ago"
- **Admin context** — Quick view of player's history without referencing the old D7 site

**What to migrate (lightweight):**
- User reference (uid)
- Season/tournament reference (by name or ID — TBD how to represent historical seasons)
- Registration date
- Status (completed, cancelled)

**What NOT to migrate:**
- Full registration entity fields (team assignment, jersey selection, order details, etc.)
- Commerce order linkage
- Group/invitation data

**D7 Source:** `league_manager_reg_info` table (or registration nodes — TBD)
**D11 Target:** Registration entity (minimal fields populated) or a dedicated `registration_history` table
**Estimated Count:** ~17k records total, filtered to migrated users

**DECISION MADE (Feb 2026):** Using actual Registration entities with minimal fields populated. This gives query compatibility with existing views/reports. Only `player`, `season` (or `tournament`), `registration_type`, `status`, and `waiver_signed` are set. All other fields (team, order, jersey, group, etc.) remain empty.

**REQUIRES STUB SEASONS:** D11 Registration entity has a required `season` entity_reference field. Historical registrations need Season entities to point to. Solution: Create inactive Season stubs from D7 `commerce_product` data (5-year lookback window). These stubs have `active=FALSE` and `registration_visible=FALSE` so they don't appear in admin lists or public pages. Real dates and prices are preserved from D7 for reference.

**Stub creation order matters:** Leagues must exist first (Season requires league reference), then Seasons, then Users, then Credits, then Registrations. Tournament stubs may already exist in D11 if created during development.

---

#### 4. Saved Payment Methods
**Filter:** Only for migrated users

**D7 Source:** `commerce_cardonfile` table  
**D11 Target:** Commerce stored payment methods (Authorize.net tokens)  
**Status:** ✅ Authorize.net configured in D11 sandbox - tokens will transfer  
**Estimated Count:** ~50-100 cards

---

### ❌ DON'T MIGRATE

- ~~Historical registrations (17k+ records - not needed)~~ → Migrating lightweight version (see item 3 above)
- Old seasons (79 seasons - starting fresh, but season names/IDs needed as references for registration history)
- Games (2,751 games - not relevant)
- Teams (taxonomy terms - will recreate)
- Invitations (4,354 nodes - transient data)
- Waitlist (546 entries - no active seasons)
- Credits nodes (expired)
- Old orders/line items (not needed)
- Historical messages/notifications (not needed)

---

## User Migration Details

### D7 to D11 Field Mapping

| D7 Field | D7 Location | D11 Field | Notes |
|----------|-------------|-----------|-------|
| **Core Fields** |
| `uid` | users.uid | User.uid | Preserve for data integrity |
| `name` | users.name | User.name | Username |
| `mail` | users.mail | User.mail | Email |
| `pass` | users.pass | User.pass | Password hash (transfer directly) |
| `created` | users.created | User.created | Account creation timestamp |
| `status` | users.status | User.status | Active/blocked |
| `login` | users.login | User.login | Last login timestamp |
| **Profile Fields** |
| `field_first_name` | field_data_field_first_name | field_first_name | Text |
| `field_last_name` | field_data_field_last_name | field_last_name | Text |
| `field_email` | field_data_field_email | field_email | **Note: May duplicate users.mail** |
| `field_gender` | field_data_field_gender | field_gender | Values: male, female, other |
| `field_league_manager_dob` | field_data_field_league_manager_dob | field_dob | Date field |
| `field_billing_zip_code` | field_data_field_billing_zip_code | field_zip_code | Stored on billing profile in D7 |
| **League Manager Fields** |
| `field_league_manager_jersey` | field_data_field_league_manager_jersey | **SKIP** | ✅ D7 stores unusable file IDs - no size data to migrate. New players prompted at first checkout; returning players (covered by reg-light history) skip the pane. Size is order data, not user data. |
| `field_league_manager_obs_skill` | field_data_field_league_manager_obs_skill | field_skill_level | Admin-observed skill (1-10) |
| `field_league_manager_self_skill` | field_data_field_league_manager_self_skill | field_self_score | Player self-assessment (1-10) |
| `field_league_manager_goalie` | field_data_field_league_manager_goalie | field_prefers_goalie | Boolean |
| `field_league_manager_show_email` | field_data_field_league_manager_show_email | ❓ Display preference? | May map to notification preferences |
| `field_league_manager_show_phone` | field_data_field_league_manager_show_phone | ❓ Display preference? | May map to notification preferences |
| `field_message_preference` | field_data_field_message_preference | field_notification_preferences | Email/SMS preferences |
| `field_display_name` | field_data_field_display_name | Derived from first_name + last_name | Or migrate if different |
| **Photo** |
| `picture` | users.picture (file ID) | field_player_picture | Migrate via file_managed table |
| **Credits** |
| N/A - calculated | N/A | field_credits_balance | Calculate from active Credits entities |
| **Discount/Override** |
| `field_player_discount` | field_data_field_player_discount | field_discount_percent | Percentage (0-100) |
| N/A in D7? | N/A | field_permanent_override | May not exist in D7 - set to FALSE |

---

### User Data Transformations

#### 1. Password Hashes
D7 uses different password hashing than D11. Options:
- **Option A:** Migrate hashes directly, force password reset on first login
- **Option B:** Use Drupal's user migration tools (preserve hashes)
- **Recommended:** Option B if possible, fallback to Option A

#### 2. Jersey Sizes
- D7: `field_league_manager_jersey` stores file IDs (not jersey sizes)
- D11: Jersey sizes handled properly with dedicated field
- **Migration:** Skip jersey field from D7, collect on first D11 registration
- **Status:** ✅ Not a concern - D11 handles better than D7

#### 3. Profile Photos
- D7: `users.picture` = file ID in `file_managed`
- D11: `field_player_picture` = managed file field
- **Migration:** Copy files from D7 files directory, import via file_managed migration

#### 4. Skill Scores
- D7 has two fields: `obs_skill` (admin-set) and `self_skill` (player-entered)
- D11 has same structure
- **Migration:** Direct copy, both fields

#### 5. Display Preferences
- D7: `show_email`, `show_phone` (boolean fields)
- D11: Might fold into general privacy settings
- **Decision needed:** Map to notification preferences or create privacy fields?

#### 6. Credits Balance
- D7: No single field (must calculate from season_credit nodes)
- D11: `field_credits_balance` (cached for performance)
- **Migration:** Calculate after Credits entities migrated

---

### Edge Cases to Handle

#### Users with Incomplete Profiles
- Missing DOB: **Action:** Flag for manual review, cannot register without
- Missing gender: **Action:** Set to "Prefer not to say"
- Missing photo: **Action:** Mark required on first login
- Missing skill scores: **Action:** Collect on first registration
- Missing jersey size: **Action:** ✅ Expected - collect on first D11 registration

#### Duplicate Emails
Check for:
```sql
SELECT mail, COUNT(*) as count
FROM users
GROUP BY mail
HAVING count > 1;
```

**Action:** Merge or deactivate duplicates before migration

#### Users with Zero Registrations
If user has never registered (only created account):
- Keep if logged in recently (might register in future)
- Prune if inactive for X+ years (based on Board decision)

---

## Credits Migration Details

### D7 Source Structure

**Entity Type:** Node (bundle: season_credit)

**Fields:**
- `nid` - Credit node ID
- `uid` - Owner (from node.uid)
- `title` - Description (e.g., "Rainout Coed 01/24")
- `field_amount` - Credit amount (decimal)
- `field_season` - Related season (commerce_product reference)
- `field_expiration_date` - When credit expires
- `field_apply_dates` - When credit was applied? (unclear)
- `field_revoke_dates` - When credit was revoked? (unclear)

### D11 Target Structure

**Entity Type:** Credits (custom entity)

**Fields:**
- `id` - Auto-increment
- `user` - Entity reference to User
- `amount` - Decimal
- `type` - List (earned, used, expired, adjusted)
- `source` - List (registration_cancelled, rain_out, admin_adjustment)
- `created` - Timestamp
- `expires` - Timestamp (1 year from created)
- `status` - List (active, used, expired)
- `reason` - Text (explanation)

### Migration Mapping

| D7 Field | D11 Field | Transformation |
|----------|-----------|----------------|
| `node.uid` | Credits.user | Direct map |
| `field_amount` | Credits.amount | Direct copy |
| `node.title` | Credits.reason | Copy as explanation |
| `field_expiration_date` | Credits.expires | Direct copy |
| `node.created` | Credits.created | Direct copy |
| N/A | Credits.type | **Set to "earned"** (assume all migrated credits were earned) |
| N/A | Credits.source | **Set to "admin_adjustment"** (migration) |
| N/A | Credits.status | **Calculate:** expires > NOW ? "active" : "expired" |

### Credits Migration Logic

```php
// Pseudocode
foreach (season_credit nodes WHERE expiration_date > migration_date) {
  Credit::create([
    'user' => node.uid,
    'amount' => field_amount,
    'type' => 'earned',
    'source' => 'admin_adjustment',
    'created' => node.created,
    'expires' => field_expiration_date,
    'status' => (field_expiration_date > NOW) ? 'active' : 'expired',
    'reason' => 'Migrated from D7: ' . node.title,
  ])->save();
}
```

### Post-Migration: Update User Credit Balances

After Credits entities created:
```php
// For each user
$user->field_credits_balance = sum of active credits for user;
$user->save();
```

---

## Payment Methods Migration

### D7 Source Structure

**Table:** `commerce_cardonfile`

**Key Fields:**
- `card_id` - Card ID
- `uid` - Owner
- `payment_method` - Gateway (e.g., authnet)
- `instance_id` - Gateway instance
- `remote_id` - Token from payment gateway
- `card_type` - Visa, Mastercard, etc.
- `card_name` - Name on card
- `card_number` - Last 4 digits (stored as "XXXX-XXXX-XXXX-1234")
- `card_exp_month` - Expiration month
- `card_exp_year` - Expiration year
- `status` - Active/inactive
- `created` - When added
- `changed` - Last updated

### D11 Target Structure

**D11 Commerce:** Uses same structure (minimal changes D7→D11)

**Approach:** Use Commerce migration tools (already handles this)

### Migration Notes

**Gateway Status:** ✅ Keeping Authorize.net (already configured in D11 sandbox)

**Impact:** Payment method tokens (`remote_id`) will transfer cleanly since we're using the same gateway

**Migration Process:** Commerce migration tools handle card-on-file data migration automatically

---

## Pre-Migration Cleanup

### 1. Identify Users to Migrate

**Decision Pending:** 🔴 BOARD DECISION NEEDED - User pruning cutoff

```sql
-- 3-year cutoff (recommended balanced approach)
SELECT uid, name, mail, login, created
FROM users
WHERE login > UNIX_TIMESTAMP('2023-01-01')
  AND status = 1
ORDER BY login DESC;

-- Alternative: 2-year cutoff (aggressive cleanup)
SELECT uid, name, mail, login, created
FROM users
WHERE login > UNIX_TIMESTAMP('2024-01-01')
  AND status = 1
ORDER BY login DESC;

-- Alternative: 5-year cutoff (generous, keep more users)
SELECT uid, name, mail, login, created
FROM users
WHERE login > UNIX_TIMESTAMP('2021-01-01')
  AND status = 1
ORDER BY login DESC;
```

**Expected output:** 
- 2-year: ~1,000-1,500 users
- 3-year: ~1,500-2,000 users
- 5-year: ~2,200-2,500 users

### 2. Identify Active Credits

```sql
-- Get unexpired credits
SELECT n.nid, n.uid, n.title, n.created,
       amount.field_amount_value as amount,
       exp.field_expiration_date_value as expires
FROM node n
LEFT JOIN field_data_field_amount amount 
  ON n.nid = amount.entity_id
LEFT JOIN field_data_field_expiration_date exp
  ON n.nid = exp.entity_id
WHERE n.type = 'season_credit'
  AND exp.field_expiration_date_value > UNIX_TIMESTAMP(NOW())
ORDER BY exp.field_expiration_date_value;
```

**Expected output:** ~20-40 credits

### 3. Check for Data Quality Issues

**Duplicate emails:**
```sql
SELECT mail, COUNT(*) as count
FROM users
GROUP BY mail
HAVING count > 1;
```

**Users missing critical data:**
```sql
-- Users without DOB
SELECT u.uid, u.name, u.mail
FROM users u
LEFT JOIN field_data_field_league_manager_dob dob
  ON u.uid = dob.entity_id
WHERE u.status = 1
  AND dob.entity_id IS NULL;
```

**Users without photos:**
```sql
SELECT u.uid, u.name, u.mail
FROM users u
WHERE u.status = 1
  AND (u.picture = 0 OR u.picture IS NULL);
```

---

## Migration Approach

### Tools to Use

**Custom Drush Command (DECIDED Feb 2026):**
- `drush ccsoccer:migrate-d7` — single command handles all entity types
- Reads from D7 database via secondary connection (`$databases['d7']` in settings.local.php)
- Migrate API deemed overkill for 2-3 entity types with straightforward field mapping
- Command supports `--dry-run`, `--limit=N`, `--step=seasons|users|credits|registrations|all`
- Idempotent: skips existing seasons (by name) and users (by UID)

**File:** `src/Drush/Commands/MigrateCommands.php`

**Payment Methods:** Still TBD — may use Commerce migration tools separately

### Test Run Results (Feb 13, 2026)

| Entity | Migrated | Errors | Notes |
|--------|----------|--------|-------|
| Seasons | 36 | 0 | 5-year window, inactive stubs |
| Users | 200 | 0 | Most recent by login |
| Credits | 64 | 0 | 2 active balances |
| Registrations | 1,633 | 0 | Season + tournament |

See `MIGRATION_STEPS.md` for the full runbook.

### Migration Process

#### Phase 1: Preparation (1-2 weeks before)

1. **Audit D7 data:**
   - Run cleanup queries
   - Document edge cases
   - Export user list for validation

2. **Set up D11:**
   - All entities created
   - All fields configured
   - Test environment ready

3. **Write migration scripts:**
   - User migration configuration
   - Credits migration script
   - Payment method migration (Commerce tools)

4. **Test on subset:**
   - Migrate 50 test users
   - Verify all fields populated
   - Test login/password preservation
   - Verify credits calculated correctly

#### Phase 1.5: Security Hardening (Before Production)

Complete these before going live. Full details in `CC_Soccer_Security_Assessment_2026_03_16.md`.

1. **Credentials & secrets (do at production deployment):**
   - Remove/rotate Authorize.net API credentials from version control; move to `settings.local.php` config overrides
   - Set a strong `hash_salt` in production `settings.local.php`
   - Delete the SQL database dump (`ccsoccer-d11-migrated-200users.sql`) from the repo root

2. **Server configuration (do at production deployment):**
   - Disable and uninstall the Devel module (`drush pm:uninstall devel devel_generate`)
   - Configure `trusted_host_patterns` in `settings.local.php` — do this now on test (`'^test\.ccsoccer\.com$'`), then again on production (`'^ccsoccer\.com$'`, `'^www\.ccsoccer\.com$'`)
   - Ensure `development.services.yml` is NOT loaded in production

3. **~~Code fixes~~ — COMPLETED (March 16, 2026):**
   - ~~Add `X-CSRF-Token` headers to all AJAX POST requests in JS files~~ ✅ Done (8 JS files)
   - ~~Add controller-level entity access checks to AJAX endpoints~~ ✅ Done (8 PHP controllers)
   - ~~Replace `innerHTML` with safe DOM methods in JS files handling user-controlled data~~ ✅ Done (4 JS files)
   - ~~Add rate limiting to player-facing endpoints~~ ✅ Done (GroupController: userSearch, invite, nudge)

#### Phase 2: Migration Weekend

1. **Friday evening:**
   - Put D7 site in maintenance mode
   - Take final database backup
   - Export final user list

2. **Saturday:**
   - Run user migration
   - Run credits migration
   - Run payment methods migration
   - Verify data integrity

3. **Sunday:**
   - User testing
   - Admin testing
   - Fix any issues

4. **Monday morning:**
   - Launch D11 site
   - Send email to all users (credentials, new URL)
   - Monitor for issues

#### Phase 3: Post-Migration (1 week after)

1. **Monitor:**
   - User login success rate
   - Payment processing
   - Support requests

2. **Manual fixes:**
   - Users who can't log in (password resets)
   - Missing photos (prompt users)
   - Credit discrepancies (manual adjustments)

3. **Archive D7:**
   - Keep D7 database backup (read-only)
   - Redirect old URL to new site
   - Reference for 6 months, then decommission

---

## Testing Plan

### Pre-Migration Tests

**Test Migration Scripts:**
1. Migrate 10 test users → Verify all fields
2. Migrate 5 credits → Verify amounts, dates, status
3. Test password preservation → Users can log in
4. Test photo migration → Images display correctly

**Data Validation Queries:**
```sql
-- D11: Count migrated users
SELECT COUNT(*) FROM users WHERE uid > 1;

-- D11: Count active credits
SELECT COUNT(*) FROM credits WHERE status = 'active';

-- D11: Verify credit balances match
SELECT u.uid, u.name, u.field_credits_balance, SUM(c.amount) as calculated
FROM users u
LEFT JOIN credits c ON c.user = u.uid AND c.status = 'active'
GROUP BY u.uid;
```

### Post-Migration Tests

**User Tests:**
- [ ] Can log in with existing credentials
- [ ] Profile shows all fields correctly
- [ ] Photo displays
- [ ] Credits balance shows correctly
- [ ] Can update profile

**Admin Tests:**
- [ ] Can view all users
- [ ] Can search users
- [ ] Can manually adjust credits
- [ ] Can assign roles

**Payment Tests:**
- [ ] Saved cards visible (if migrated)
- [ ] Can add new payment method
- [ ] Test registration payment flow

---

## Known Issues and Gotchas

### Jersey Sizes - RESOLVED ✅

**Issue:** D7 `field_league_manager_jersey` stores unusable file IDs — no jersey size data exists to migrate.

**Solution:** Skip migration entirely. No data to bring over.

**How it works in D11:**
- **Returning players (have registration history via reg-light migration):** `PlayerInfoPane` is skipped entirely — they already own a jersey. Jersey size is captured per-order at checkout if they ever buy another.
- **New players (no registration history):** `PlayerInfoPane` is shown at first checkout — they select a jersey size which is added to their cart. Size lives on the order, not the user record.
- **No user-level jersey size field is used for any business logic.** Size is order data only.

**Status:** No further action needed.

### Payment Gateway Compatibility - RESOLVED ✅

**Decision:** Keeping Authorize.net (already configured in D11 sandbox)

**Impact:** Payment method tokens will migrate cleanly

**Action:** Use Commerce migration tools for card data

### Password Hash Compatibility

**Issue:** D7 and D11 may use different hash algorithms

**Solution:** Use Migrate Upgrade module (handles this)

**Fallback:** Force password reset on first login (email all users)

### Credits Source Unknown

**Issue:** Can't determine if credits were from cancellation, rainout, or manual adjustment

**Solution:** Mark all migrated credits as `source = 'admin_adjustment'`

**Acceptable:** Historical source doesn't affect current value

### User Role Assignments

**Issue:** D7 has "manager" role (7 users), unclear mapping to D11 roles

**Solution:** 
- Migrate all users as "Player" role initially
- Manually assign Board Member, Slofriendly roles after migration
- Email the 7 managers to log in and confirm roles

### Timezone Issues

**Issue:** Timestamps in D7 may be stored in different timezone

**Solution:** Verify timezone settings, adjust if needed during migration

---

## Rollback Plan

### If Migration Fails

**Before cutover:**
- Keep D7 site running (maintenance mode)
- Can abort and revert at any time

**After cutover (if critical issues found):**
1. Put D11 in maintenance mode
2. Reactivate D7 site
3. Email users about temporary rollback
4. Fix issues, re-attempt migration following weekend

**Point of No Return:**
- Once users start registering for new seasons in D11
- Then forward-only (no rollback to D7)

---

## Success Criteria

### Migration is successful if:

- [ ] 95%+ of active users migrated
- [ ] All credits migrated and balanced
- [ ] Users can log in without password resets
- [ ] Profile data complete and accurate
- [ ] Photos display correctly
- [ ] Credits balances match D7 calculations
- [ ] Payment methods migrated (if applicable)
- [ ] No critical bugs found within 48 hours
- [ ] User support requests < 5% of migrated users

---

## Questions to Answer Before Migration

### High Priority

- [ ] 🔴 **User pruning cutoff:** BOARD DECISION NEEDED - 2-year? 3-year? 5-year?
- [x] ✅ **Jersey field:** Skip migration - collect fresh in D11
- [x] ✅ **Payment gateway:** Keeping Authorize.net
- [ ] **Display preferences:** Map `show_email`/`show_phone` to what in D11?
- [ ] **Duplicate emails:** How many? How to handle?
- [ ] **User photo requirement:** Enforce immediately or allow grace period?

### Medium Priority

- [ ] **Credit types:** Can we determine earned vs. issued from D7 data?
- [ ] **Role mapping:** Email 7 "managers" before migration?
- [ ] **Timezone:** Verify D7 and D11 using same timezone

### Low Priority

- [ ] **Historical registrations:** Any value in keeping for stats? (Probably not)
- [ ] **Team names:** Worth migrating vocabulary? (No)
- [ ] **Message history:** Keep for reference? (No)

---

## Timeline

**8 weeks before (June 2025):**
- Finalize D11 development
- Write migration scripts
- Test on subset

**4 weeks before (July 2025):**
- Audit D7 data
- Answer all questions above
- Dry run full migration

**2 weeks before (Late July 2025):**
- Final testing
- User communication (migration announcement)
- Prep support docs

**Migration Weekend (Early August 2025):**
- Execute migration
- Testing
- Launch

**1 week after:**
- Monitor and support
- Fix edge cases

**New season starts (Mid-August 2025):**
- First registrations in D11
- Point of no return

---

## Communication Plan

### Before Migration

**Email to all active users (2 weeks before):**
```
Subject: CC Soccer is Upgrading - New Website Coming August!

Hi [name],

We're excited to announce that CC Soccer is getting a brand new website! 

What you need to know:
- Your account and credits will transfer automatically
- You'll use the same login credentials
- New URL: [new-site-url.com]
- Migration weekend: [dates]
- Site will be down briefly during migration

No action needed from you. We'll email again when the new site is live.

Questions? Reply to this email.

Thanks for playing!
CC Soccer Team
```

### After Migration

**Email to all migrated users (day after launch):**
```
Subject: CC Soccer New Site is LIVE!

Hi [name],

Our new website is now live at [new-site-url.com]

Log in with your existing credentials:
- Username: [username]
- Password: [same as before]

Your account has been migrated with:
✓ Your profile information
✓ Your credits: $[balance]
✓ Your saved payment methods (if applicable)

Please log in and:
1. Verify your profile is correct
2. Add/update your photo if needed
3. Check your credit balance

Registration for Fall 2025 season opens [date].

Questions? Contact us at [email]

See you on the field!
CC Soccer Team
```

---

## Next Steps

1. ✅ ~~Investigate jersey field~~ - RESOLVED: Skip migration
2. ✅ ~~Decide on payment gateway~~ - RESOLVED: Keeping Authorize.net
3. 🔴 **Get Board approval on user pruning cutoff** (2-year? 3-year? 5-year?)
4. **Complete D11 development** (all entities and fields)
5. **Write migration scripts** (users, credits, payment methods)
6. **Test migration** (subset of data)
7. **Plan migration weekend** (schedule, team, communication)

---

**Document Owner:** Caleb  
**Last Reviewed:** January 3, 2026  
**Next Review:** Before migration scripts written
