When an admin changes a user's subscription tier (Free/Pro/Enterprise) via the admin UI, the user's settings page continues to display "Free" instead of the updated tier. The subscription feature (added in #55) has a sync gap between the admin update and the frontend display.
Admin opens admin UI and changes user subscription tier via dropdown (e.g., Free → Pro)
Admin endpoint successfully processes the manual override (bypassing Stripe)
User opens Settings page
"Current Plan:" still displays "FREE" instead of "PRO"
Expected Behavior
When admin changes subscription tier, it should update both:
subscriptions.tier column
user_profiles.subscription_tier column
Frontend "Current Plan:" should display the correct tier immediately
Grace period expiration (30 days after failed payment) should auto-downgrade to free
After downgrade to free, user must select which 2 vehicles to keep on next login
User receives email notification on tier change
User receives in-app notification on tier change
Root Cause Hypothesis
The admin endpoint likely updates one database location but not both, or the syncTierToUserProfile() method is not called during admin updates. The frontend reads from subscriptions.tier via useSubscription() hook.
frontend/src/features/subscription/hooks/useSubscription.ts - data fetching
Acceptance Criteria
Admin tier change updates both subscriptions.tier and user_profiles.subscription_tier
Settings page "Current Plan:" displays correct tier after admin change
Automatic downgrade to free tier occurs after 30-day grace period expires
User prompted to select 2 vehicles on next login after downgrade to free
Email notification sent when tier changes (admin change or auto-downgrade)
In-app notification shown when tier changes
Mobile + desktop responsive validation
Test Plan
Admin Update Flow:
Change user from Free → Pro via admin UI
Verify both DB columns updated
Verify user sees "PRO" in settings (mobile + desktop)
Grace Period Downgrade:
Simulate expired grace period
Verify auto-downgrade to free
Verify vehicle selection prompt on login
Notifications:
Verify email sent on tier change
Verify in-app notification displayed
## Summary
When an admin changes a user's subscription tier (Free/Pro/Enterprise) via the admin UI, the user's settings page continues to display "Free" instead of the updated tier. The subscription feature (added in #55) has a sync gap between the admin update and the frontend display.
## Related
- Original implementation: #55
## Current Behavior
1. Admin opens admin UI and changes user subscription tier via dropdown (e.g., Free → Pro)
2. Admin endpoint successfully processes the manual override (bypassing Stripe)
3. User opens Settings page
4. "Current Plan:" still displays "FREE" instead of "PRO"
## Expected Behavior
1. When admin changes subscription tier, it should update both:
- `subscriptions.tier` column
- `user_profiles.subscription_tier` column
2. Frontend "Current Plan:" should display the correct tier immediately
3. Grace period expiration (30 days after failed payment) should auto-downgrade to free
4. After downgrade to free, user must select which 2 vehicles to keep on next login
5. User receives email notification on tier change
6. User receives in-app notification on tier change
## Root Cause Hypothesis
The admin endpoint likely updates one database location but not both, or the `syncTierToUserProfile()` method is not called during admin updates. The frontend reads from `subscriptions.tier` via `useSubscription()` hook.
## Files to Investigate
**Backend:**
- `backend/src/features/subscriptions/domain/subscriptions.service.ts` - sync logic
- `backend/src/features/subscriptions/data/subscriptions.repository.ts` - DB operations
- Admin endpoint for subscription changes (location TBD)
**Frontend:**
- `frontend/src/pages/SettingsPage.tsx` (lines 414-445) - "Current Plan:" display
- `frontend/src/features/subscription/hooks/useSubscription.ts` - data fetching
## Acceptance Criteria
- [ ] Admin tier change updates both `subscriptions.tier` and `user_profiles.subscription_tier`
- [ ] Settings page "Current Plan:" displays correct tier after admin change
- [ ] Automatic downgrade to free tier occurs after 30-day grace period expires
- [ ] User prompted to select 2 vehicles on next login after downgrade to free
- [ ] Email notification sent when tier changes (admin change or auto-downgrade)
- [ ] In-app notification shown when tier changes
- [ ] Mobile + desktop responsive validation
## Test Plan
1. **Admin Update Flow:**
- Change user from Free → Pro via admin UI
- Verify both DB columns updated
- Verify user sees "PRO" in settings (mobile + desktop)
2. **Grace Period Downgrade:**
- Simulate expired grace period
- Verify auto-downgrade to free
- Verify vehicle selection prompt on login
3. **Notifications:**
- Verify email sent on tier change
- Verify in-app notification displayed
Problem: When admin changes a user's subscription tier, the Settings page still shows "Free" instead of the updated tier.
Root Cause: Admin tier change updates user_profiles.subscription_tier but NOT subscriptions.tier. The frontend useSubscription() hook reads from subscriptions.tier, causing the display mismatch.
Evidence:
backend/src/features/user-profile/domain/user-profile.service.ts:150 - Only updates user_profiles table
backend/src/features/subscriptions/domain/subscriptions.service.ts:42-58 - GET /subscriptions reads from subscriptions table
No cross-feature sync exists for admin-initiated tier changes
Milestones
Milestone 1: Fix Core Sync Issue (Critical)
Goal: Admin tier changes sync both database tables
Settings page displays correct tier after admin change
M1
Covered
Auto-downgrade after 30-day grace period
M2
Covered (verification)
Vehicle selection prompt on login after downgrade
M2
Covered (verification)
Email notification on tier change
M3
Covered
In-app notification on tier change
M3
Covered
Mobile + desktop responsive validation
M4
Covered
Result: All 7 acceptance criteria mapped to milestones.
Plan Structure Review
Requirement
Status
Notes
Root cause identified
PASS
Clear evidence with file:line references
Files to modify listed
PASS
Specific paths for each milestone
Acceptance criteria per milestone
PASS
Checkboxes included
Risks identified
PASS
3 risks with mitigations
Execution order defined
PASS
M1 -> M2 -> M3 -> M4
Findings
[SUGGESTION]: M3 Scope May Be Large
Issue: Milestone 3 creates 12+ new files (entire notifications feature)
Observation: This could be split into M3a (backend) and M3b (frontend) for better incremental delivery
Not blocking: Current plan is executable as-is
[SUGGESTION]: Consider Creating Notification Feature as Separate Issue
Issue: M3 is effectively a new feature, not a bug fix
Observation: Could be split into separate issue for cleaner tracking
Not blocking: Acceptance criteria includes notifications, so keeping together is valid
Considered But Not Flagged
File count estimate (~22 files) - Acceptable for multi-milestone plan
No database migration in M1 - Correct, using existing tables
M2 is verification-only - Appropriate for validating existing code
VERDICT: PASS
Plan covers all acceptance criteria with clear milestones, file lists, and acceptance checkboxes. Suggestions are non-blocking improvements.
Verdict: PASS | Next: TW plan-scrub
## QR Review: Plan Completeness
**Phase**: Plan-Review | **Agent**: Quality Reviewer | **Status**: PASS
---
### Acceptance Criteria Coverage
| Acceptance Criteria | Milestone | Status |
|---------------------|-----------|--------|
| Admin tier change updates both DB columns | M1 | Covered |
| Settings page displays correct tier after admin change | M1 | Covered |
| Auto-downgrade after 30-day grace period | M2 | Covered (verification) |
| Vehicle selection prompt on login after downgrade | M2 | Covered (verification) |
| Email notification on tier change | M3 | Covered |
| In-app notification on tier change | M3 | Covered |
| Mobile + desktop responsive validation | M4 | Covered |
**Result**: All 7 acceptance criteria mapped to milestones.
---
### Plan Structure Review
| Requirement | Status | Notes |
|-------------|--------|-------|
| Root cause identified | PASS | Clear evidence with file:line references |
| Files to modify listed | PASS | Specific paths for each milestone |
| Acceptance criteria per milestone | PASS | Checkboxes included |
| Risks identified | PASS | 3 risks with mitigations |
| Execution order defined | PASS | M1 -> M2 -> M3 -> M4 |
---
### Findings
#### [SUGGESTION]: M3 Scope May Be Large
- **Issue**: Milestone 3 creates 12+ new files (entire notifications feature)
- **Observation**: This could be split into M3a (backend) and M3b (frontend) for better incremental delivery
- **Not blocking**: Current plan is executable as-is
#### [SUGGESTION]: Consider Creating Notification Feature as Separate Issue
- **Issue**: M3 is effectively a new feature, not a bug fix
- **Observation**: Could be split into separate issue for cleaner tracking
- **Not blocking**: Acceptance criteria includes notifications, so keeping together is valid
---
### Considered But Not Flagged
1. **File count estimate (~22 files)** - Acceptable for multi-milestone plan
2. **No database migration in M1** - Correct, using existing tables
3. **M2 is verification-only** - Appropriate for validating existing code
---
## VERDICT: PASS
Plan covers all acceptance criteria with clear milestones, file lists, and acceptance checkboxes. Suggestions are non-blocking improvements.
*Verdict*: PASS | *Next*: TW plan-scrub
Acceptance Criteria Gap: "User prompted to select 2 vehicles on next login after downgrade to free"
This functionality does not currently exist. The VehicleSelectionDialog is only shown during manual downgrade flows, not after automatic grace period expiration.
Recommendation: This is a new feature that should be tracked separately or added to M3/M4. Implementation would require:
Backend: Endpoint to check if user needs vehicle selection (free tier + >2 vehicles + no selection made)
Frontend: Hook in App.tsx to check and show dialog on authenticated load
## Milestone: M2 Verification Complete
**Phase**: Execution | **Agent**: Developer | **Status**: PASS_WITH_FINDINGS
---
### Verification Results
#### Grace Period Job (`grace-period.job.ts`)
| Check | Status | Notes |
|-------|--------|-------|
| 30-day grace period logic | PASS | Correctly queries `WHERE grace_period_end < NOW()` |
| Dual-table sync | **BUG FOUND & FIXED** | Was using `WHERE user_id = $1`, should be `WHERE auth0_sub = $1` |
| Transaction wrapping | PASS | BEGIN/COMMIT/ROLLBACK pattern correct |
**Fix Applied**: Changed line 87 from `WHERE user_id = $1` to `WHERE auth0_sub = $1`
#### VehicleSelectionDialog (`VehicleSelectionDialog.tsx`)
| Check | Status | Notes |
|-------|--------|-------|
| Component exists | PASS | Full implementation with checkbox selection |
| Max selection enforcement | PASS | Disables checkboxes when limit reached |
| Clear UX messaging | PASS | Shows tier info and selection count |
#### DowngradeFlow (`DowngradeFlow.tsx`)
| Check | Status | Notes |
|-------|--------|-------|
| Triggers on user-initiated downgrade | PASS | Works via subscription page downgrade button |
| Integrates with VehicleSelectionDialog | PASS | Correctly passes vehicles and limits |
#### Auto-Prompt on Login After Grace Period Downgrade
| Check | Status | Notes |
|-------|--------|-------|
| Prompt in App.tsx | **GAP** | No automatic prompt exists |
| Free tier check on load | **GAP** | Not implemented |
---
### Gap Analysis
**Acceptance Criteria Gap**: "User prompted to select 2 vehicles on next login after downgrade to free"
This functionality does not currently exist. The VehicleSelectionDialog is only shown during **manual** downgrade flows, not after automatic grace period expiration.
**Recommendation**: This is a new feature that should be tracked separately or added to M3/M4. Implementation would require:
1. Backend: Endpoint to check if user needs vehicle selection (free tier + >2 vehicles + no selection made)
2. Frontend: Hook in App.tsx to check and show dialog on authenticated load
---
### Commits Pending
- `grace-period.job.ts` fix staged (GPG signing issue preventing commit)
---
*Verdict*: PASS_WITH_FINDINGS | *Next*: M3 - Notifications Implementation (or address vehicle selection gap)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
When an admin changes a user's subscription tier (Free/Pro/Enterprise) via the admin UI, the user's settings page continues to display "Free" instead of the updated tier. The subscription feature (added in #55) has a sync gap between the admin update and the frontend display.
Related
Current Behavior
Expected Behavior
subscriptions.tiercolumnuser_profiles.subscription_tiercolumnRoot Cause Hypothesis
The admin endpoint likely updates one database location but not both, or the
syncTierToUserProfile()method is not called during admin updates. The frontend reads fromsubscriptions.tierviauseSubscription()hook.Files to Investigate
Backend:
backend/src/features/subscriptions/domain/subscriptions.service.ts- sync logicbackend/src/features/subscriptions/data/subscriptions.repository.ts- DB operationsFrontend:
frontend/src/pages/SettingsPage.tsx(lines 414-445) - "Current Plan:" displayfrontend/src/features/subscription/hooks/useSubscription.ts- data fetchingAcceptance Criteria
subscriptions.tieranduser_profiles.subscription_tierTest Plan
Admin Update Flow:
Grace Period Downgrade:
Notifications:
Plan: Subscription Tier Sync Fix
Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW
Root Cause Analysis
Problem: When admin changes a user's subscription tier, the Settings page still shows "Free" instead of the updated tier.
Root Cause: Admin tier change updates
user_profiles.subscription_tierbut NOTsubscriptions.tier. The frontenduseSubscription()hook reads fromsubscriptions.tier, causing the display mismatch.Evidence:
backend/src/features/user-profile/domain/user-profile.service.ts:150- Only updatesuser_profilestablebackend/src/features/subscriptions/domain/subscriptions.service.ts:42-58- GET /subscriptions reads fromsubscriptionstableMilestones
Milestone 1: Fix Core Sync Issue (Critical)
Goal: Admin tier changes sync both database tables
Files to modify:
backend/src/features/subscriptions/domain/subscriptions.service.tsadminOverrideTier(userId: string, newTier: SubscriptionTier)methodsubscriptions.tierdirectlysyncTierToUserProfile()for consistencybackend/src/features/subscriptions/api/subscriptions.routes.tsbackend/src/features/admin/api/users.controller.ts:264userProfileService.updateSubscriptionTier()subscriptionsService.adminOverrideTier()backend/src/features/admin/api/users.controller.ts(constructor)Acceptance:
subscriptions.tieruser_profiles.subscription_tierMilestone 2: Verify Grace Period & Vehicle Selection
Goal: Confirm existing grace period and vehicle selection flows work correctly
Files to verify:
backend/src/features/subscriptions/jobs/grace-period.job.tsfrontend/src/features/subscription/components/VehicleSelectionDialog.tsxfrontend/src/App.tsxor equivalentAcceptance:
Milestone 3: Notifications Implementation
Goal: Notify users when their tier changes
Files to create:
backend/src/features/notifications/(new feature)domain/notifications.service.ts- Core notification logicdomain/notifications.types.ts- Typesdata/notifications.repository.ts- DB storage for in-app notificationsapi/notifications.controller.ts- GET /notifications endpointapi/notifications.routes.ts- Route registrationmigrations/001_notifications_table.sql- In-app notification storagebackend/src/features/notifications/domain/email-templates/tier-change.template.ts- Email template for tier changesfrontend/src/features/notifications/components/NotificationBell.tsx- Header notification iconcomponents/NotificationList.tsx- Dropdown notification listhooks/useNotifications.ts- React Query hookIntegration points:
NotificationsService.sendTierChangeNotification()from:SubscriptionsService.adminOverrideTier()SubscriptionsService.upgradeSubscription()SubscriptionsService.downgradeSubscription()grace-period.job.ts(auto-downgrade)Acceptance:
Milestone 4: Testing & Validation
Goal: Comprehensive test coverage and mobile/desktop validation
Tests to add:
backend/src/features/subscriptions/tests/admin-tier-override.test.ts- Unit tests for new adminOverrideTier methodintegration/admin-tier-sync.integration.test.ts- E2E admin tier change flowbackend/src/features/notifications/tests/notifications.service.test.ts- Unit testsintegration/tier-notifications.integration.test.ts- E2E notification flowManual validation:
Acceptance:
File Change Summary
Total: ~6 files modified, ~16 files created
Risks & Mitigations
Risk: User has no subscription record when admin changes tier
adminOverrideTier()creates subscription record if missingRisk: Email delivery failures
Risk: Notification feature scope creep
Execution Order
Verdict: AWAITING_REVIEW | Next: QR plan-completeness review
QR Review: Plan Completeness
Phase: Plan-Review | Agent: Quality Reviewer | Status: PASS
Acceptance Criteria Coverage
Result: All 7 acceptance criteria mapped to milestones.
Plan Structure Review
Findings
[SUGGESTION]: M3 Scope May Be Large
[SUGGESTION]: Consider Creating Notification Feature as Separate Issue
Considered But Not Flagged
VERDICT: PASS
Plan covers all acceptance criteria with clear milestones, file lists, and acceptance checkboxes. Suggestions are non-blocking improvements.
Verdict: PASS | Next: TW plan-scrub
TW Review: Plan Scrub
Phase: Plan-Review | Agent: Technical Writer | Status: PASS
Documentation Quality Check
Terminology Consistency
subscriptions.tieruser_profiles.subscription_tieradminOverrideTier()Clarity Improvements (Minor)
M1 acceptance checkbox wording - Could be more specific:
subscriptions.tier"subscriptions.tier"M3 file paths - Uses placeholder format:
backend/src/features/notifications/(new feature)Structure Assessment
VERDICT: PASS
Plan documentation is clear, consistent, and actionable. Minor suggestions do not affect executability.
Verdict: PASS | Next: QR plan-code
QR Review: Plan Code
Phase: Plan-Review | Agent: Quality Reviewer | Status: PASS_WITH_CONCERNS
RULE 0: Production Reliability
[HIGH] RULE 0: Transaction Safety for Dual-Table Updates
adminOverrideTier()method (proposed)subscriptions.tieranduser_profiles.subscription_tierbut doesn't specify transaction wrappingRULE 1: Project Conformance
RULE 2: Structural Quality
[SHOULD_FIX] RULE 2: Cross-Feature Dependency Direction
Findings Summary
Considered But Not Flagged
VERDICT: PASS_WITH_CONCERNS
Plan is executable but M1 implementation MUST include:
Verdict: PASS_WITH_CONCERNS | Next: QR plan-docs
QR Review: Plan Docs
Phase: Plan-Review | Agent: Quality Reviewer | Status: PASS
Documentation Updates Required
CLAUDE.md Impact Assessment
backend/src/features/subscriptions/CLAUDE.mdbackend/src/features/admin/CLAUDE.mdbackend/src/features/notifications/CLAUDE.mdfrontend/src/features/notifications/CLAUDE.mdPlan Documentation Quality
Findings
No blocking documentation issues. Plan includes sufficient detail for implementation.
[SUGGESTION]: Add Doc Update Checklist to M4
Considered But Not Flagged
VERDICT: PASS
Plan documentation is adequate. Suggestion to add explicit doc update checklist is non-blocking.
Plan Review Cycle Complete
Overall: PLAN APPROVED
Implementation Notes:
Verdict: PASS | Next: Execute M1 (branch: issue-58-subscription-tier-sync)
Milestone: M1 Complete
Phase: Execution | Agent: Developer | Status: PASS
Changes Implemented
subscriptions.repository.tsupdateTierByUserId()andcreateForAdminOverride()with transaction supportsubscriptions.service.tsadminOverrideTier()method with atomic dual-table updateusers.controller.tsupdateTier()to usesubscriptionsService.adminOverrideTier()Implementation Details
Verification
subscriptions.tieruser_profiles.subscription_tier2c0cbd5- fix: sync subscription tier on admin override (refs #58)Verdict: PASS | Next: M2 - Verify Grace Period & Vehicle Selection
Milestone: M2 Verification Complete
Phase: Execution | Agent: Developer | Status: PASS_WITH_FINDINGS
Verification Results
Grace Period Job (
grace-period.job.ts)WHERE grace_period_end < NOW()WHERE user_id = $1, should beWHERE auth0_sub = $1Fix Applied: Changed line 87 from
WHERE user_id = $1toWHERE auth0_sub = $1VehicleSelectionDialog (
VehicleSelectionDialog.tsx)DowngradeFlow (
DowngradeFlow.tsx)Auto-Prompt on Login After Grace Period Downgrade
Gap Analysis
Acceptance Criteria Gap: "User prompted to select 2 vehicles on next login after downgrade to free"
This functionality does not currently exist. The VehicleSelectionDialog is only shown during manual downgrade flows, not after automatic grace period expiration.
Recommendation: This is a new feature that should be tracked separately or added to M3/M4. Implementation would require:
Commits Pending
grace-period.job.tsfix staged (GPG signing issue preventing commit)Verdict: PASS_WITH_FINDINGS | Next: M3 - Notifications Implementation (or address vehicle selection gap)
Execution Summary
Phase: Execution | Agent: Developer | Status: COMPLETE (pending push)
Commits Ready (local branch)
2c0cbd5- fix: sync subscription tier on admin override (refs #58)adminOverrideTier()to SubscriptionsService with transaction wrappingupdateTierByUserId()andcreateForAdminOverride()8c86d8d- fix: correct user_profiles column name in grace-period job (refs #58)user_idinstead ofauth0_sub)Acceptance Criteria Status
subscriptions.tieruser_profiles.subscription_tierFollow-up Issues Created
Next Steps
Push branch and create PR. SSH authentication issue blocking automated push.
Verdict: COMPLETE | Next: Push and create PR