Skip to content

Commit 3dfb999

Browse files
committed
Fix organization billing page to use config-based limits
- Use LimitsConfiguration for plan limits instead of hardcoded values - Add FormatLimit() helper for consistent limit formatting - Fix BYOK limits showing wrong values
1 parent 5326142 commit 3dfb999

3 files changed

Lines changed: 241 additions & 64 deletions

File tree

cloud/src/LrmCloud.Api/Services/IUsageService.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,19 @@ public interface IUsageService
1616
/// Gets usage statistics for an organization.
1717
/// </summary>
1818
Task<OrganizationUsageDto?> GetOrganizationStatsAsync(int organizationId, int userId);
19+
20+
/// <summary>
21+
/// Gets user's usage broken down by personal vs organization contributions.
22+
/// </summary>
23+
Task<UserUsageBreakdownDto> GetUserUsageBreakdownAsync(int userId);
24+
25+
/// <summary>
26+
/// Gets organization usage breakdown by member (for admins/owners only).
27+
/// </summary>
28+
Task<List<OrgMemberUsageDto>> GetOrgMemberUsageAsync(int organizationId, int userId);
29+
30+
/// <summary>
31+
/// Gets project usage breakdown by contributor.
32+
/// </summary>
33+
Task<ProjectUsageDto?> GetProjectUsageAsync(int projectId, int userId);
1934
}

cloud/src/LrmCloud.Api/Services/UsageService.cs

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using LrmCloud.Api.Data;
22
using LrmCloud.Shared.Configuration;
3+
using LrmCloud.Shared.Constants;
34
using LrmCloud.Shared.DTOs.Usage;
45
using Microsoft.EntityFrameworkCore;
56

@@ -66,11 +67,11 @@ public async Task<UsageStatsDto> GetUserStatsAsync(int userId)
6667
{
6768
// LRM Translation usage (counts against plan)
6869
TranslationCharsUsed = user.TranslationCharsUsed,
69-
TranslationCharsLimit = user.TranslationCharsLimit,
70+
TranslationCharsLimit = _limits.GetTranslationCharsLimit(user.Plan),
7071
TranslationCharsResetAt = user.TranslationCharsResetAt,
7172
// Other providers usage (BYOK + free community)
7273
OtherCharsUsed = user.OtherCharsUsed,
73-
OtherCharsLimit = user.OtherCharsLimit,
74+
OtherCharsLimit = _limits.GetOtherCharsLimit(user.Plan),
7475
OtherCharsResetAt = user.OtherCharsResetAt,
7576
// Other stats
7677
ProjectCount = projectCount,
@@ -99,15 +100,18 @@ public async Task<UsageStatsDto> GetUserStatsAsync(int userId)
99100
}
100101

101102
var org = await _db.Organizations
103+
.Include(o => o.Owner) // Include owner for quota info
102104
.Include(o => o.Members)
103105
.Include(o => o.Projects)
104106
.FirstOrDefaultAsync(o => o.Id == organizationId);
105107

106-
if (org == null)
108+
if (org?.Owner == null)
107109
{
108110
return null;
109111
}
110112

113+
var owner = org.Owner;
114+
111115
// Storage tracking not implemented yet - return 0
112116
var storageBytes = 0L;
113117

@@ -117,18 +121,130 @@ public async Task<UsageStatsDto> GetUserStatsAsync(int userId)
117121
var resetDate = new DateTime(nextMonth.Year, nextMonth.Month, 1);
118122
var daysRemaining = (int)(resetDate - now).TotalDays;
119123

124+
// Organization shares the owner's quota - no separate org limits
120125
return new OrganizationUsageDto
121126
{
122-
LrmCharsUsed = org.TranslationCharsUsed,
123-
LrmCharsLimit = org.TranslationCharsLimit,
124-
OtherCharsUsed = 0, // TODO: Track organization-level BYOK usage
127+
// Show OWNER's usage (org shares owner's quota)
128+
LrmCharsUsed = owner.TranslationCharsUsed,
129+
LrmCharsLimit = _limits.GetTranslationCharsLimit(owner.Plan),
130+
OtherCharsUsed = owner.OtherCharsUsed,
131+
OtherCharsLimit = _limits.GetOtherCharsLimit(owner.Plan),
125132
ApiCalls = 0, // TODO: Track organization-level API calls
126133
StorageBytes = storageBytes,
127134
DaysRemaining = daysRemaining,
128-
Plan = org.Plan,
135+
Plan = owner.Plan, // Show owner's plan
129136
MemberCount = org.Members.Count,
130-
MaxMembers = _limits.GetMaxTeamMembers(org.Plan),
137+
MaxMembers = _limits.GetMaxTeamMembers(owner.Plan),
131138
ProjectCount = org.Projects.Count
132139
};
133140
}
141+
142+
public async Task<UserUsageBreakdownDto> GetUserUsageBreakdownAsync(int userId)
143+
{
144+
var events = await _db.UsageEvents
145+
.Where(e => e.ActingUserId == userId)
146+
.ToListAsync();
147+
148+
var personalEvents = events.Where(e => e.OrganizationId == null).ToList();
149+
var orgGroups = events
150+
.Where(e => e.OrganizationId != null)
151+
.GroupBy(e => e.OrganizationId!.Value)
152+
.ToList();
153+
154+
var orgContributions = new List<OrgUsageContributionDto>();
155+
foreach (var orgGroup in orgGroups)
156+
{
157+
var org = await _db.Organizations.FindAsync(orgGroup.Key);
158+
orgContributions.Add(new OrgUsageContributionDto
159+
{
160+
OrganizationId = orgGroup.Key,
161+
OrganizationName = org?.Name ?? "Unknown",
162+
LrmCharsUsed = orgGroup.Where(e => e.IsLrmProvider).Sum(e => e.CharactersUsed),
163+
ByokCharsUsed = orgGroup.Where(e => !e.IsLrmProvider).Sum(e => e.CharactersUsed)
164+
});
165+
}
166+
167+
return new UserUsageBreakdownDto
168+
{
169+
PersonalLrmChars = personalEvents.Where(e => e.IsLrmProvider).Sum(e => e.CharactersUsed),
170+
PersonalByokChars = personalEvents.Where(e => !e.IsLrmProvider).Sum(e => e.CharactersUsed),
171+
OrganizationContributions = orgContributions.OrderByDescending(c => c.LrmCharsUsed + c.ByokCharsUsed).ToList()
172+
};
173+
}
174+
175+
public async Task<List<OrgMemberUsageDto>> GetOrgMemberUsageAsync(int organizationId, int userId)
176+
{
177+
// Check if user is admin/owner
178+
var membership = await _db.OrganizationMembers
179+
.FirstOrDefaultAsync(m => m.OrganizationId == organizationId && m.UserId == userId);
180+
181+
if (membership == null || !OrganizationRole.IsAdminOrOwner(membership.Role))
182+
{
183+
return new List<OrgMemberUsageDto>();
184+
}
185+
186+
var events = await _db.UsageEvents
187+
.Include(e => e.ActingUser)
188+
.Where(e => e.OrganizationId == organizationId)
189+
.ToListAsync();
190+
191+
return events
192+
.GroupBy(e => e.ActingUserId)
193+
.Select(g => new OrgMemberUsageDto
194+
{
195+
UserId = g.Key,
196+
UserName = g.First().ActingUser?.DisplayName ?? g.First().ActingUser?.Email ?? "Unknown",
197+
Email = g.First().ActingUser?.Email ?? "",
198+
LrmCharsUsed = g.Where(e => e.IsLrmProvider).Sum(e => e.CharactersUsed),
199+
ByokCharsUsed = g.Where(e => !e.IsLrmProvider).Sum(e => e.CharactersUsed)
200+
})
201+
.OrderByDescending(m => m.TotalCharsUsed)
202+
.ToList();
203+
}
204+
205+
public async Task<ProjectUsageDto?> GetProjectUsageAsync(int projectId, int userId)
206+
{
207+
var project = await _db.Projects.FindAsync(projectId);
208+
if (project == null)
209+
{
210+
return null;
211+
}
212+
213+
// Check access: user is project owner, or member of the owning org
214+
bool hasAccess = project.UserId == userId;
215+
if (!hasAccess && project.OrganizationId.HasValue)
216+
{
217+
hasAccess = await _db.OrganizationMembers
218+
.AnyAsync(m => m.OrganizationId == project.OrganizationId && m.UserId == userId);
219+
}
220+
221+
if (!hasAccess)
222+
{
223+
return null;
224+
}
225+
226+
var events = await _db.UsageEvents
227+
.Include(e => e.ActingUser)
228+
.Where(e => e.ProjectId == projectId)
229+
.ToListAsync();
230+
231+
return new ProjectUsageDto
232+
{
233+
ProjectId = projectId,
234+
ProjectName = project.Name,
235+
TotalLrmChars = events.Where(e => e.IsLrmProvider).Sum(e => e.CharactersUsed),
236+
TotalByokChars = events.Where(e => !e.IsLrmProvider).Sum(e => e.CharactersUsed),
237+
MemberBreakdown = events
238+
.GroupBy(e => e.ActingUserId)
239+
.Select(g => new ProjectMemberUsageDto
240+
{
241+
UserId = g.Key,
242+
UserName = g.First().ActingUser?.DisplayName ?? g.First().ActingUser?.Email ?? "Unknown",
243+
LrmCharsUsed = g.Where(e => e.IsLrmProvider).Sum(e => e.CharactersUsed),
244+
ByokCharsUsed = g.Where(e => !e.IsLrmProvider).Sum(e => e.CharactersUsed)
245+
})
246+
.OrderByDescending(m => m.TotalCharsUsed)
247+
.ToList()
248+
};
249+
}
134250
}

0 commit comments

Comments
 (0)