using FlatRender.IdentitySvc.Domain.Entities; using FlatRender.IdentitySvc.Domain.Enums; using FlatRender.IdentitySvc.Infrastructure.Data; using FlatRender.IdentitySvc.Models; using Microsoft.EntityFrameworkCore; namespace FlatRender.IdentitySvc.Application.Services; /// Admin CRM analytics, customer notes/tags, and per-user power-actions. public class AdminService(IdentityDbContext db) { // ── CRM acquisition / conversion analytics ────────────────────────────── public async Task GetCrmAnalyticsAsync(Guid tenantId, DateTime start, DateTime end) { // Query-bound dates arrive Kind=Unspecified; Npgsql requires UTC for timestamptz. start = DateTime.SpecifyKind(start.Date, DateTimeKind.Utc); end = DateTime.SpecifyKind(end.Date.AddDays(1), DateTimeKind.Utc); // inclusive of the end day if ((end - start).TotalDays > 366) end = start.AddDays(366); var signups = await db.Users .Where(u => u.TenantId == tenantId && u.DeletedAt == null && u.RegisterDate >= start && u.RegisterDate < end) .Select(u => new { u.Id, u.RegisterDate }) .ToListAsync(); var ids = signups.Select(s => s.Id).ToList(); var buyerIds = await db.Payments .Where(p => p.TenantId == tenantId && p.Status == PaymentStatus.Succeeded && ids.Contains(p.UserId)) .Select(p => p.UserId).Distinct().ToListAsync(); var pays = await db.Payments .Where(p => p.TenantId == tenantId && p.Status == PaymentStatus.Succeeded && p.CreatedAt >= start && p.CreatedAt < end) .Select(p => new { p.UserId, p.AmountMinor, p.CreatedAt }) .ToListAsync(); var payingAllTime = await db.Payments .Where(p => p.TenantId == tenantId && p.Status == PaymentStatus.Succeeded) .Select(p => p.UserId).Distinct().CountAsync(); var daily = new List(); for (var d = start; d < end; d = d.AddDays(1)) { var de = d.AddDays(1); var dayPays = pays.Where(p => p.CreatedAt >= d && p.CreatedAt < de).ToList(); daily.Add(new CrmDailyPoint( d.ToString("yyyy-MM-dd"), signups.Count(u => u.RegisterDate >= d && u.RegisterDate < de), dayPays.Select(p => p.UserId).Distinct().Count(), dayPays.Sum(p => p.AmountMinor))); } var total = signups.Count; var buyers = buyerIds.Count; return new CrmAnalyticsResponse( total, buyers, total - buyers, total > 0 ? Math.Round((double)buyers / total * 100, 1) : 0, pays.Sum(p => p.AmountMinor), payingAllTime, daily); } // ── Plan statistics ────────────────────────────────────────────────────── public async Task> GetPlanStatisticsAsync(Guid tenantId) { var now = DateTime.UtcNow; return await db.UserPlans .Where(p => p.TenantId == tenantId) .GroupBy(p => p.PlanName) .Select(g => new PlanStatRow( g.Key, g.Count(), g.Count(x => x.ExpiresAt > now && x.CancelledAt == null), g.Sum(x => x.PriceMinorPaid))) .OrderByDescending(r => r.RevenueMinor) .ToListAsync(); } // ── CRM notes / tags ──────────────────────────────────────────────────── public async Task GetUserCrmAsync(Guid userId) { var c = await db.Set().FindAsync(userId); return c == null ? new UserCrmResponse([], null, "new") : new UserCrmResponse(c.Tags, c.Note, c.Status); } public async Task UpsertUserCrmAsync(Guid userId, UpsertUserCrmRequest req) { var c = await db.Set().FindAsync(userId); var isNew = c == null; c ??= new UserCrm { UserId = userId }; if (req.Tags != null) c.Tags = req.Tags; if (req.Note != null) c.Note = req.Note; if (req.Status != null) c.Status = req.Status; c.UpdatedAt = DateTime.UtcNow; if (isNew) db.Add(c); await db.SaveChangesAsync(); return new UserCrmResponse(c.Tags, c.Note, c.Status); } // ── Per-user power-actions ────────────────────────────────────────────── private async Task RequireUser(Guid id) => await db.Users.FindAsync(id) ?? throw new KeyNotFoundException("User not found"); public async Task SetBalanceAsync(Guid userId, long amountMinor, bool add) { var u = await RequireUser(userId); u.BalanceMinor = add ? u.BalanceMinor + amountMinor : amountMinor; u.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task ResetPasswordAsync(Guid userId, string newPassword) { var u = await RequireUser(userId); u.PasswordHash = BCrypt.Net.BCrypt.HashPassword(newPassword); u.PasswordSetAt = DateTime.UtcNow; u.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task AddChargeAsync(Guid userId, int seconds, int renderCount) { var u = await RequireUser(userId); if (seconds != 0) u.UserDailyFreeChargeSec += seconds; if (renderCount != 0) { u.MaxDailyRenderCount += renderCount; u.DailyRemainRenderCount += renderCount; } u.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task SetModeratorAsync(Guid userId, bool enabled) { var u = await RequireUser(userId); u.IsTenantAdmin = enabled; u.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task GrantPlanDaysAsync(Guid userId, Guid planId, int days) { var plan = await db.UserPlans .Where(p => p.UserId == userId && (planId == Guid.Empty || p.PlanId == planId)) .OrderByDescending(p => p.ExpiresAt).FirstOrDefaultAsync() ?? throw new KeyNotFoundException("No plan found for this user to extend"); var baseDate = plan.ExpiresAt > DateTime.UtcNow ? plan.ExpiresAt : DateTime.UtcNow; plan.ExpiresAt = baseDate.AddDays(days); plan.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } }