151 lines
6.0 KiB
C#
151 lines
6.0 KiB
C#
using System.Globalization;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Growth;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Domain.Commerce;
|
|
using Tiku.Domain.Common;
|
|
using Tiku.Domain.Growth;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Tenancy;
|
|
using Tiku.Infrastructure.Persistence;
|
|
|
|
namespace Tiku.Infrastructure.Growth;
|
|
|
|
public sealed partial class ReferralService
|
|
{
|
|
public async Task<ReferralStatsItem> GetStatsAsync(
|
|
ReferralAdminActor actor,
|
|
ReferralStatsQuery query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await AssertAdminAsync(actor, cancellationToken);
|
|
var referrerUserId = query.ReferrerUserId ?? actor.UserId;
|
|
await AssertActiveMemberAsync(actor.TenantId, referrerUserId, cancellationToken);
|
|
return await BuildStatsAsync(actor.TenantId, referrerUserId, cancellationToken);
|
|
}
|
|
|
|
public async Task<ReferralList<ReferralStatsItem>> GetSalesStatsAsync(
|
|
ReferralAdminActor actor,
|
|
ReferralStatsQuery query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await AssertAdminAsync(actor, cancellationToken);
|
|
var limit = Math.Clamp(query.Limit ?? 100, 1, 500);
|
|
var referrerIdsQuery = dbContext.ReferralLeads
|
|
.AsNoTracking()
|
|
.Where(item => item.TenantId == actor.TenantId && item.ReferrerUserId != null);
|
|
if (query.ReferrerUserId.HasValue)
|
|
{
|
|
referrerIdsQuery = referrerIdsQuery.Where(item => item.ReferrerUserId == query.ReferrerUserId.Value);
|
|
}
|
|
|
|
var referrerIds = await referrerIdsQuery
|
|
.Select(item => item.ReferrerUserId!.Value)
|
|
.Distinct()
|
|
.Take(limit)
|
|
.ToArrayAsync(cancellationToken);
|
|
var stats = new List<ReferralStatsItem>(referrerIds.Length);
|
|
foreach (var referrerId in referrerIds)
|
|
{
|
|
stats.Add(await BuildStatsAsync(actor.TenantId, referrerId, cancellationToken));
|
|
}
|
|
|
|
return new ReferralList<ReferralStatsItem>(
|
|
stats
|
|
.OrderByDescending(item => item.PaidAmountCents)
|
|
.ThenByDescending(item => item.LeadCount)
|
|
.Take(limit)
|
|
.ToArray());
|
|
}
|
|
|
|
public async Task<ReferralConversionReport> GetConversionReportAsync(
|
|
ReferralAdminActor actor,
|
|
ReferralConversionQuery query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await AssertAdminAsync(actor, cancellationToken);
|
|
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var endDate = query.EndDate ?? today;
|
|
var startDate = query.StartDate ?? endDate.AddDays(-Math.Clamp(query.Days ?? 30, 1, 365) + 1);
|
|
if (endDate < startDate)
|
|
{
|
|
throw new ReferralException("Referral date range was invalid.", "invalid_date_range");
|
|
}
|
|
|
|
var start = startDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
|
var endExclusive = endDate.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
|
var leadsQuery = dbContext.ReferralLeads
|
|
.AsNoTracking()
|
|
.Where(item =>
|
|
item.TenantId == actor.TenantId &&
|
|
item.BoundAt >= start &&
|
|
item.BoundAt < endExclusive);
|
|
if (query.ReferrerUserId.HasValue)
|
|
{
|
|
leadsQuery = leadsQuery.Where(item => item.ReferrerUserId == query.ReferrerUserId.Value);
|
|
}
|
|
|
|
var leads = await leadsQuery
|
|
.OrderByDescending(item => item.BoundAt)
|
|
.ToArrayAsync(cancellationToken);
|
|
var paidStudentIds = await dbContext.Orders.AsNoTracking()
|
|
.Where(item =>
|
|
item.TenantId == actor.TenantId &&
|
|
item.Status == OrderStatus.Paid &&
|
|
item.UserId != null &&
|
|
item.PaidAt != null &&
|
|
item.PaidAt >= start &&
|
|
item.PaidAt < endExclusive)
|
|
.GroupBy(item => item.UserId!.Value)
|
|
.Select(group => new
|
|
{
|
|
UserId = group.Key,
|
|
AmountCents = group.Sum(item => item.AmountCents)
|
|
})
|
|
.ToDictionaryAsync(item => item.UserId, item => item.AmountCents, cancellationToken);
|
|
var paidLeadCount = leads.Count(item => paidStudentIds.ContainsKey(item.StudentUserId));
|
|
var paidAmountCents = leads.Sum(item => paidStudentIds.GetValueOrDefault(item.StudentUserId));
|
|
var top = await GetSalesStatsAsync(
|
|
actor,
|
|
new ReferralStatsQuery(query.ReferrerUserId, query.Limit ?? 20),
|
|
cancellationToken);
|
|
|
|
return new ReferralConversionReport(
|
|
startDate,
|
|
endDate,
|
|
leads.Length,
|
|
paidLeadCount,
|
|
paidAmountCents,
|
|
leads.Length == 0 ? 0m : Math.Round(paidLeadCount * 100m / leads.Length, 2),
|
|
top.Items,
|
|
leads
|
|
.Where(item => !paidStudentIds.ContainsKey(item.StudentUserId))
|
|
.Take(Math.Clamp(query.Limit ?? 20, 1, 100))
|
|
.Select(item => ToLeadItem(item, false))
|
|
.ToArray());
|
|
}
|
|
|
|
public async Task<ReferralList<ReferralLeadItem>> GetClientsAsync(
|
|
ReferralAdminActor actor,
|
|
ReferralStatsQuery query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await AssertAdminAsync(actor, cancellationToken);
|
|
var referrerUserId = query.ReferrerUserId ?? actor.UserId;
|
|
await AssertActiveMemberAsync(actor.TenantId, referrerUserId, cancellationToken);
|
|
var leads = await dbContext.ReferralLeads
|
|
.AsNoTracking()
|
|
.Where(item =>
|
|
item.TenantId == actor.TenantId &&
|
|
item.ReferrerUserId == referrerUserId)
|
|
.OrderByDescending(item => item.BoundAt)
|
|
.Take(Math.Clamp(query.Limit ?? 100, 1, 500))
|
|
.ToArrayAsync(cancellationToken);
|
|
return new ReferralList<ReferralLeadItem>(leads.Select(item => ToLeadItem(item, false)).ToArray());
|
|
}
|
|
|
|
|
|
}
|