From 33aa6b45a834b9971c219f26a7d384536d4ee9fb Mon Sep 17 00:00:00 2001 From: Jerry Phillips Date: Thu, 7 May 2026 19:50:11 -0400 Subject: [PATCH] chore(api): [AB#150] remove seat limit enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed all seat limit logic from the API — enforcement blocks invite flow and conflicts with the free trial plan. Changes: [1] Removed SeatLimitReached error and SetSeatLimitAsync from EmployeeInviteErrors + ISubscriptionRecordService [2] Removed seat limit enforcement block from EmployeeInviteService [3] Removed SetSeatLimitAsync and SeatLimit assignment from SubscriptionRecordService [4] Removed subscriptions DI and SetSeatLimitAsync call from OnboardingController [5] Removed SeatLimit from OrganizationDtos.SetTeamSizeRequest and OrganizationDto References: [1] JobFlow.Business/ModelErrors/EmployeeInviteErrors.cs:1 [2] JobFlow.Business/Services/EmployeeInviteService.cs:1 [3] JobFlow.Business/Services/SubscriptionRecordService.cs:1 [4] JobFlow.API/Controllers/OnboardingController.cs:1 [5] JobFlow.API/Models/OrganizationDtos.cs:1 --- .../Controllers/OnboardingController.cs | 8 +---- JobFlow.API/Models/OrganizationDtos.cs | 2 +- .../ModelErrors/EmployeeInviteErrors.cs | 6 ---- .../Models/DTOs/OrganizationDto.cs | 1 - .../Services/EmployeeInviteService.cs | 17 +--------- .../Services/OrganizationService.cs | 1 - .../ISubscriptionRecordService.cs | 1 - .../Services/SubscriptionRecordService.cs | 33 +------------------ 8 files changed, 4 insertions(+), 65 deletions(-) diff --git a/JobFlow.API/Controllers/OnboardingController.cs b/JobFlow.API/Controllers/OnboardingController.cs index 9113b91..8b230bd 100644 --- a/JobFlow.API/Controllers/OnboardingController.cs +++ b/JobFlow.API/Controllers/OnboardingController.cs @@ -16,13 +16,11 @@ public class OnboardingController : ControllerBase { private readonly IOnboardingService onboarding; private readonly IOrganizationService organizations; - private readonly ISubscriptionRecordService subscriptions; - public OnboardingController(IOnboardingService onboarding, IOrganizationService organizations, ISubscriptionRecordService subscriptions) + public OnboardingController(IOnboardingService onboarding, IOrganizationService organizations) { this.onboarding = onboarding; this.organizations = organizations; - this.subscriptions = subscriptions; } [HttpGet("{organizationId:guid}")] @@ -142,10 +140,6 @@ public async Task SetTeamSize([FromBody] SetTeamSizeRequest request) if (orgResult.IsFailure) return orgResult.ToProblemDetails(); - var subResult = await subscriptions.SetSeatLimitAsync(organizationId, request.SeatLimit); - if (subResult.IsFailure) - return subResult.ToProblemDetails(); - await onboarding.MarkStepCompleteAsync(organizationId, OnboardingStepKeys.ChooseTeamSize); return Results.Ok(); diff --git a/JobFlow.API/Models/OrganizationDtos.cs b/JobFlow.API/Models/OrganizationDtos.cs index 3ef3f2f..f29ed4b 100644 --- a/JobFlow.API/Models/OrganizationDtos.cs +++ b/JobFlow.API/Models/OrganizationDtos.cs @@ -1,4 +1,4 @@ namespace JobFlow.API.Models; public sealed record MarkMilestoneRequest(string Milestone); -public sealed record SetTeamSizeRequest(string OrgSize, int? SeatLimit); +public sealed record SetTeamSizeRequest(string OrgSize); diff --git a/JobFlow.Business/ModelErrors/EmployeeInviteErrors.cs b/JobFlow.Business/ModelErrors/EmployeeInviteErrors.cs index 8187147..c61cc5f 100644 --- a/JobFlow.Business/ModelErrors/EmployeeInviteErrors.cs +++ b/JobFlow.Business/ModelErrors/EmployeeInviteErrors.cs @@ -32,10 +32,4 @@ public static Error FailedToSendNotification(string recipient) "EmployeeInvites", $"Failed to send invite notification to {recipient}."); } - public static Error SeatLimitReached(int limit) - { - return Error.Conflict( - "EmployeeInvites.SeatLimitReached", - $"Your plan allows up to {limit} team member{(limit == 1 ? "" : "s")}. Upgrade your plan to add more."); - } } \ No newline at end of file diff --git a/JobFlow.Business/Models/DTOs/OrganizationDto.cs b/JobFlow.Business/Models/DTOs/OrganizationDto.cs index 68fd59c..eba9cc7 100644 --- a/JobFlow.Business/Models/DTOs/OrganizationDto.cs +++ b/JobFlow.Business/Models/DTOs/OrganizationDto.cs @@ -45,7 +45,6 @@ public class OrganizationDto public string? SubscriptionPlanName { get; set; } public string? SubscriptionStatus { get; set; } public DateTime? SubscriptionExpiresAt { get; set; } - public int? SeatLimit { get; set; } public string? IndustryKey { get; set; } public Guid OrganizationTypeId { get; set; } public PaymentProvider PaymentProvider { get; set; } diff --git a/JobFlow.Business/Services/EmployeeInviteService.cs b/JobFlow.Business/Services/EmployeeInviteService.cs index 92e6a99..446c57f 100644 --- a/JobFlow.Business/Services/EmployeeInviteService.cs +++ b/JobFlow.Business/Services/EmployeeInviteService.cs @@ -21,7 +21,6 @@ public class EmployeeInviteService : IEmployeeInviteService private readonly ILogger _logger; private readonly IMapper _mapper; private readonly INotificationService _notifications; - private readonly ISubscriptionRecordService _subscriptions; private readonly IUnitOfWork _unitOfWork; public EmployeeInviteService( @@ -29,15 +28,13 @@ public EmployeeInviteService( IUnitOfWork unitOfWork, INotificationService notifications, IFrontendSettings frontendSettings, - IMapper mapper, - ISubscriptionRecordService subscriptions) + IMapper mapper) { _logger = logger; _unitOfWork = unitOfWork; _notifications = notifications; _frontendSettings = frontendSettings; _mapper = mapper; - _subscriptions = subscriptions; _invites = unitOfWork.RepositoryOf(); } @@ -52,18 +49,6 @@ public async Task> InviteAsync(EmployeeInvite invite) if (string.IsNullOrWhiteSpace(invite.Email)) return Result.Failure(EmployeeInviteErrors.InvalidEmail(invite.Email ?? "unknown")); - // Enforce seat limit (grandfathered orgs with SeatLimit = null are exempt) - var subscriptionResult = await _subscriptions.GetLatestForOrganizationAsync(invite.OrganizationId); - if (subscriptionResult.IsSuccess && subscriptionResult.Value.SeatLimit is int seatLimit) - { - var activeEmployeeCount = await _unitOfWork.RepositoryOf() - .Query() - .CountAsync(e => e.OrganizationId == invite.OrganizationId && e.IsActive); - - if (activeEmployeeCount >= seatLimit) - return Result.Failure(EmployeeInviteErrors.SeatLimitReached(seatLimit)); - } - // Check for existing active invite var existingInvite = await _invites.Query() .FirstOrDefaultAsync(e => e.Email == invite.Email && invite.Status == EmployeeInviteStatus.Pending); diff --git a/JobFlow.Business/Services/OrganizationService.cs b/JobFlow.Business/Services/OrganizationService.cs index 1fac99f..8d34477 100644 --- a/JobFlow.Business/Services/OrganizationService.cs +++ b/JobFlow.Business/Services/OrganizationService.cs @@ -89,7 +89,6 @@ public async Task> GetOrganizationDtoById(Guid orgId) dto.SubscriptionPlanName = latestSubscription?.PlanName; dto.SubscriptionStatus = latestSubscription?.Status; - dto.SeatLimit = latestSubscription?.SeatLimit; dto.SubscriptionExpiresAt = org.SubscriptionExpiresAt; } else diff --git a/JobFlow.Business/Services/ServiceInterfaces/ISubscriptionRecordService.cs b/JobFlow.Business/Services/ServiceInterfaces/ISubscriptionRecordService.cs index b61d9e7..aa0f6aa 100644 --- a/JobFlow.Business/Services/ServiceInterfaces/ISubscriptionRecordService.cs +++ b/JobFlow.Business/Services/ServiceInterfaces/ISubscriptionRecordService.cs @@ -12,5 +12,4 @@ Task> CreateAsync(Guid paymentProfileId, string provi Task> GetLatestForOrganizationAsync(Guid organizationId, PaymentProvider? provider = null); Task CancelAsync(string providerSubscriptionId, DateTime canceledAt); Task UpdateAsync(SubscriptionRecord subscriptionRecord); - Task SetSeatLimitAsync(Guid organizationId, int? seatLimit); } \ No newline at end of file diff --git a/JobFlow.Business/Services/SubscriptionRecordService.cs b/JobFlow.Business/Services/SubscriptionRecordService.cs index 8ef65e6..24632ff 100644 --- a/JobFlow.Business/Services/SubscriptionRecordService.cs +++ b/JobFlow.Business/Services/SubscriptionRecordService.cs @@ -62,8 +62,7 @@ public async Task> CreateAsync(Guid paymentProfileId, ProviderPriceId = providerPriceId, Status = status, StartDate = DateTime.UtcNow, - PlanName = planName, - SeatLimit = ResolveDefaultSeatLimit(planName) + PlanName = planName }; unitOfWork.RepositoryOf().Add(subscription); @@ -142,34 +141,4 @@ public async Task UpdateAsync(SubscriptionRecord subscriptionRecord) return Result.Success(); } - public async Task SetSeatLimitAsync(Guid organizationId, int? seatLimit) - { - var profileIds = await paymentProfiles.Query() - .Where(p => p.OwnerId == organizationId && p.OwnerType == PaymentEntityType.Organization) - .Select(p => p.Id) - .ToListAsync(); - - if (profileIds.Count == 0) - return Result.Failure(SubscriptionErrors.NotFound); - - var latest = await subscriptions.Query() - .Where(s => profileIds.Contains(s.PaymentProfileId)) - .OrderByDescending(s => s.StartDate) - .FirstOrDefaultAsync(); - - if (latest is null) - return Result.Failure(SubscriptionErrors.NotFound); - - latest.SeatLimit = seatLimit; - await unitOfWork.SaveChangesAsync(); - return Result.Success(); - } - - private static int? ResolveDefaultSeatLimit(string planName) => - (planName ?? string.Empty).Trim().ToLowerInvariant() switch - { - "go" => 5, - "flow" => 15, - _ => null // Max and unknown plans: unlimited - }; } \ No newline at end of file