fix: sync subscription tier on admin override (refs #58)

Add adminOverrideTier() method to SubscriptionsService that atomically
updates both subscriptions.tier and user_profiles.subscription_tier
using database transactions.

Changes:
- SubscriptionsRepository: Add updateTierByUserId() and
  createForAdminOverride() methods with transaction support
- SubscriptionsService: Add adminOverrideTier() method with transaction
  wrapping for atomic dual-table updates
- UsersController: Replace userProfileService.updateSubscriptionTier()
  with subscriptionsService.adminOverrideTier()

This ensures admin tier changes properly sync to both database tables,
fixing the Settings page "Current Plan" display mismatch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-19 09:03:50 -06:00
parent 5707391864
commit 2c0cbd5bf7
3 changed files with 193 additions and 6 deletions

View File

@@ -7,6 +7,9 @@ import { FastifyRequest, FastifyReply } from 'fastify';
import { UserProfileService } from '../../user-profile/domain/user-profile.service';
import { UserProfileRepository } from '../../user-profile/data/user-profile.repository';
import { AdminRepository } from '../data/admin.repository';
import { SubscriptionsService } from '../../subscriptions/domain/subscriptions.service';
import { SubscriptionsRepository } from '../../subscriptions/data/subscriptions.repository';
import { StripeClient } from '../../subscriptions/external/stripe/stripe.client';
import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import {
@@ -28,15 +31,22 @@ import { AdminService } from '../domain/admin.service';
export class UsersController {
private userProfileService: UserProfileService;
private adminService: AdminService;
private subscriptionsService: SubscriptionsService;
private userProfileRepository: UserProfileRepository;
private adminRepository: AdminRepository;
constructor() {
this.userProfileRepository = new UserProfileRepository(pool);
const adminRepository = new AdminRepository(pool);
this.adminRepository = new AdminRepository(pool);
const subscriptionsRepository = new SubscriptionsRepository(pool);
const stripeClient = new StripeClient();
this.userProfileService = new UserProfileService(this.userProfileRepository);
this.userProfileService.setAdminRepository(adminRepository);
this.adminService = new AdminService(adminRepository);
this.userProfileService.setAdminRepository(this.adminRepository);
this.adminService = new AdminService(this.adminRepository);
// Admin feature depends on Subscriptions for tier management
// This is intentional - admin has oversight capabilities
this.subscriptionsService = new SubscriptionsService(subscriptionsRepository, stripeClient, pool);
}
/**
@@ -226,6 +236,8 @@ export class UsersController {
/**
* PATCH /api/admin/users/:auth0Sub/tier - Update subscription tier
* Uses subscriptionsService.adminOverrideTier() to sync both subscriptions.tier
* and user_profiles.subscription_tier atomically
*/
async updateTier(
request: FastifyRequest<{ Params: UserAuth0SubInput; Body: UpdateTierInput }>,
@@ -261,12 +273,39 @@ export class UsersController {
const { auth0Sub } = paramsResult.data;
const { subscriptionTier } = bodyResult.data;
const updatedUser = await this.userProfileService.updateSubscriptionTier(
// Verify user exists before attempting tier change
const currentUser = await this.userProfileService.getUserDetails(auth0Sub);
if (!currentUser) {
return reply.code(404).send({
error: 'Not found',
message: 'User not found',
});
}
const previousTier = currentUser.subscriptionTier;
// Use subscriptionsService to update both tables atomically
await this.subscriptionsService.adminOverrideTier(auth0Sub, subscriptionTier);
// Log audit action
await this.adminRepository.logAuditAction(
actorId,
'UPDATE_TIER',
auth0Sub,
subscriptionTier,
actorId
'user_profile',
currentUser.id,
{ previousTier, newTier: subscriptionTier }
);
logger.info('User subscription tier updated via admin', {
auth0Sub,
previousTier,
newTier: subscriptionTier,
actorAuth0Sub: actorId,
});
// Return updated user profile
const updatedUser = await this.userProfileService.getUserDetails(auth0Sub);
return reply.code(200).send(updatedUser);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';