Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions JobFlow.API/Controllers/OnboardingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand Down Expand Up @@ -142,10 +140,6 @@ public async Task<IResult> 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();
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.API/Models/OrganizationDtos.cs
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 0 additions & 6 deletions JobFlow.Business/ModelErrors/EmployeeInviteErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
}
1 change: 0 additions & 1 deletion JobFlow.Business/Models/DTOs/OrganizationDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
17 changes: 1 addition & 16 deletions JobFlow.Business/Services/EmployeeInviteService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,20 @@ public class EmployeeInviteService : IEmployeeInviteService
private readonly ILogger<EmployeeInviteService> _logger;
private readonly IMapper _mapper;
private readonly INotificationService _notifications;
private readonly ISubscriptionRecordService _subscriptions;
private readonly IUnitOfWork _unitOfWork;

public EmployeeInviteService(
ILogger<EmployeeInviteService> logger,
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<EmployeeInvite>();
}

Expand All @@ -52,18 +49,6 @@ public async Task<Result<EmployeeInviteDto>> InviteAsync(EmployeeInvite invite)
if (string.IsNullOrWhiteSpace(invite.Email))
return Result.Failure<EmployeeInviteDto>(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<Employee>()
.Query()
.CountAsync(e => e.OrganizationId == invite.OrganizationId && e.IsActive);

if (activeEmployeeCount >= seatLimit)
return Result.Failure<EmployeeInviteDto>(EmployeeInviteErrors.SeatLimitReached(seatLimit));
}

// Check for existing active invite
var existingInvite = await _invites.Query()
.FirstOrDefaultAsync(e => e.Email == invite.Email && invite.Status == EmployeeInviteStatus.Pending);
Expand Down
1 change: 0 additions & 1 deletion JobFlow.Business/Services/OrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ public async Task<Result<OrganizationDto>> GetOrganizationDtoById(Guid orgId)

dto.SubscriptionPlanName = latestSubscription?.PlanName;
dto.SubscriptionStatus = latestSubscription?.Status;
dto.SeatLimit = latestSubscription?.SeatLimit;
dto.SubscriptionExpiresAt = org.SubscriptionExpiresAt;
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ Task<Result<SubscriptionRecord>> CreateAsync(Guid paymentProfileId, string provi
Task<Result<SubscriptionRecord>> GetLatestForOrganizationAsync(Guid organizationId, PaymentProvider? provider = null);
Task<Result> CancelAsync(string providerSubscriptionId, DateTime canceledAt);
Task<Result> UpdateAsync(SubscriptionRecord subscriptionRecord);
Task<Result> SetSeatLimitAsync(Guid organizationId, int? seatLimit);
}
33 changes: 1 addition & 32 deletions JobFlow.Business/Services/SubscriptionRecordService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ public async Task<Result<SubscriptionRecord>> CreateAsync(Guid paymentProfileId,
ProviderPriceId = providerPriceId,
Status = status,
StartDate = DateTime.UtcNow,
PlanName = planName,
SeatLimit = ResolveDefaultSeatLimit(planName)
PlanName = planName
};

unitOfWork.RepositoryOf<SubscriptionRecord>().Add(subscription);
Expand Down Expand Up @@ -142,34 +141,4 @@ public async Task<Result> UpdateAsync(SubscriptionRecord subscriptionRecord)
return Result.Success();
}

public async Task<Result> 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
};
}
Loading