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();
}
}